docs(skill): smbCloud auth_user.id is per-AuthApp; reconcile local users by email
Document the identity-reconciliation rule in the smbcloud-auth skill: smbCloud's auth_user.id is per-AuthApp and changes when sigit-si is repointed at a different AuthApp/account (or the upstream account is recreated), so the local User upsert must key on email, not smbcloud_id. Also note the new "Continue with GitHub" controller. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seto Elkahfi committed
Jun 30, 2026 at 21:25 UTC
5c1f40baadab2ed82fa236e0c9522f896ee85160
1 file changed
+35
.agents/skills/smbcloud-auth/SKILL.md
+35
index dac57b8..7c9994a 100644
--- a/.agents/skills/smbcloud-auth/SKILL.md
+++ b/.agents/skills/smbcloud-auth/SKILL.md
@@ -23,6 +23,10 @@ SDKs, the web console), see the authoritative skill in
Do not call `SmbCloud::Auth` directly from controllers; go through this service.
- `app/controllers/sessions_controller.rb` — HTML sign in / sign out (`/auth`).
- `app/controllers/registrations_controller.rb` — HTML sign up (`/auth/signup`).
+- `app/controllers/oauth/github_controller.rb` — "Continue with GitHub"
+ (`/auth/github` → smbCloud authorize; `/auth/github/callback` consumes the
+ returned `access_token`). smbCloud brokers the GitHub OAuth flow; this app only
+ starts it and reuses the same `me` → upsert → session path as `sessions#create`.
- `app/models/user.rb` — local mirror, upserted via
`User.find_or_create_from_smbcloud(profile.merge(access_token:))`.
- `Gemfile` — `gem "smbcloud-auth", "~> 0.3.35"` (native Rust/Magnus extension).
@@ -50,6 +54,34 @@ Class methods, all of which translate gem errors into local error types:
Controllers map these to flash + HTTP status (`:unprocessable_entity` for auth
failures, `:internal_server_error` for config/unexpected). Preserve that mapping.
+## Local user mirror & identity reconciliation
+
+`app/models/user.rb` mirrors the smbCloud account. The upsert keys on
+`smbcloud_id` (smbCloud's numeric `auth_user.id`) but **reconciles by email** —
+and that reconciliation is load-bearing, not a nicety:
+
+> **smbCloud's `auth_user.id` is per-AuthApp, not a stable global identity.**
+> The same email is a *different* `auth_user` (different id) in every AuthApp,
+> and the id also changes if the upstream account is deleted/recreated. So the
+> moment sigit-si is repointed at a different AuthApp — e.g. moving the app to a
+> different smbCloud **account** — every existing local `User.smbcloud_id` goes
+> stale: it points at an `auth_user` that no longer backs that email.
+
+Because `User.email` is locally unique, a `smbcloud_id`-only upsert then breaks:
+a login returns the email's *current* `auth_user.id`, no local row matches it,
+and inserting a new row collides on the unique email →
+`ActiveRecord::RecordInvalid`, which surfaces as a generic **"An unexpected error
+occurred"** on sign-in (this is exactly how the GitHub callback failed at
+launch — the GitHub login resolved to the email's *current* AuthApp `auth_user`,
+which differed from the id stored when the app pointed at an earlier
+AuthApp/account).
+
+`find_or_create_from_smbcloud` therefore does: look up by `smbcloud_id`; if that
+misses **and** the authenticated email already has a local user, adopt that row
+and move it onto the new `smbcloud_id`. Treat **email as the durable key** and
+`smbcloud_id` as a pointer that can change. Never reintroduce a
+`smbcloud_id`-only upsert. (Specs: `spec/models/user_smbcloud_spec.rb`.)
+
## Credentials & environment
Required env vars (raise `KeyError` if missing — handle as a config error, not an
@@ -108,6 +140,9 @@ surface — the current controllers are HTML + session-cookie only. When adding
## Common mistakes
- Calling `SmbCloud::Auth` directly from a controller instead of via the service
+- Keying the local `User` upsert **only** on `smbcloud_id` — it is per-AuthApp
+ and changes when the app is repointed at a different AuthApp/account (or an
+ upstream account is recreated); reconcile by email
- Treating `AccountIncompleteError` as bad credentials instead of "verify email"
- Leaking `SmbCloud::Auth::Error` (or its `error_code`) to the view layer
- Using the Rails session cookie to authenticate desktop-app API requests