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