Repository navigation
feat(plugin-auth): a sign-in that skips the OIDC seams applies the IdP locale rule through AuthManager.applyIdpLocaleClaim - #27674
Conversation
…pLocaleClaim IdpLocaleSync.settle now only takes the staged claim and hands it to a new public IdpLocaleSync.apply, which holds the rule unchanged. AuthManager exposes that method as applyIdpLocaleClaim so a host whose sign-in skips the OIDC seams applies the same rule and record; the entry exports the IdpLocaleClaim type it takes. Claude-Session: https://claude.ai/code/session_01WGufzgrxXwu9nECtirwHEK Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzgrxXwu9nECtirwHEK Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8d55baa7fa4691bddd790997d1b4e644edc4f438 && git checkout 8d55baa7fa4691bddd790997d1b4e644edc4f438
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 33ab543913125b53feec6c116f13d616261a908c fd9a0e600dc5cd9c3cdcf15e71b1b29d205da9ba && git checkout -B drift-repro 33ab543913125b53feec6c116f13d616261a908c && git merge --no-ff fd9a0e600dc5cd9c3cdcf15e71b1b29d205da9ba
node scripts/docs-audit/affected-docs.mjs --json 33ab543913125b53feec6c116f13d616261a908c
|
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head fd9a0e6 · 2026-10-29T17:40Z Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: ① Derived judgmentsInputs read: card #27334 (body and all six comments, including triage 6484963623, the retriage 6495077738, its answer 6496297039, the release 6500886024, the claim 6501842999 and the os-dev-report 6503460374), PR #27674 (body, five-file list, net diff against Every accept-set and public-surface change the diff implies, each judged:
Gate verdicts, read off the check-runs on ② Semver level
③ Boundary flags
Must-fix: none. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP 维护者速读 (席位终稿)
席位意见:接受。分诊契约复核 PASS(6504177232,就在当前 head 上),没有 must-fix 和 should-fix。changeset 定为 patch,符合 AGENTS.md「按种类定级」: 席位核对 · ACCEPT ·
|
domain:spec#3 · os-support-ai · session_01WGufzgrxXwu9nECtirwHEK
Requested by Yibo Wang · project thread
Fixes #27334
Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: none — no file under
skills/**teaches the IdP locale claim,IdpLocaleSyncorsys_user.localesync (git grep -n -i "idp.\?locale\|locale claim" -- skillsanswers 0 lines on HEAD); the change is a new method on an exported class and one new type export, with no metadata key or value.Consumer: objectstack-ai/cloud#3159 (the cloud sso-exchange door wires the call; the cloud half stays there).
维护者速读
@objectstack/plugin-auth的AuthManager新增公开方法applyIdpLocaleClaim(userId, claim),入口再导出IdpLocaleClaim类型。它调用的就是 OIDC 登录本来就在用的那段规则(IdpLocaleSync.apply,从settle里拆出来),规则只有一份。idp-locale:记录标识和 OIDC 路径行为都没变(OIDC 端到端用例原样全绿,另有 spy 钉住 OIDC 走同一方法)。方法从不抛错。回滚即还原本 PR。packages/plugins/plugin-auth/src/idp-locale-claim.ts(settle/apply的拆分),packages/plugins/plugin-auth/src/auth-manager.ts(applyIdpLocaleClaim),packages/plugins/plugin-auth/src/index.ts(类型导出)。pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/idp-locale-claim.test.ts,应 28/28 全绿。What changes
Measured first, on
origin/mainefa2915epackages/plugins/plugin-auth/src/index.tslists auth-manager's published names by name (DEFAULT_AUTH_BASE_PATH,readMcpServerEnabledEnv,resolveOidcProviderEnabled,resolveDcrEnabled,resolveScimEnabled,assertScimAdminCoherence,isOAuthEligibleBaseUrl,InvitationPlacementServiceLike,readInvitationPlacementIntent,AuthManagerOptions,ipMatchesRange,normalizeAuthEmailLocale,authEmailLocaleFromRequest,AuthManager) and carries 31export * fromlines for its other modules.idp-locale-claim.tsis not among them:grep -c "idp-locale-claim" src/index.tsanswers 0, and the builtdist/index.d.tsnames none ofIdpLocaleSync,readIdpLocaleClaim,resolveIdpLocale,decideIdpLocaleWrite,idpLocaleRecordIdentifier,IdpLocaleClaim. The card's premise holds.settle's apply half (idp-locale-claim.tsonefa2915e): after taking the staged claim off the endpoint context and checking the session'suserId, it ran, inside onetry: the supported-locale guard (no locales ⇒ onewarn, nothing written),getEngine(),authContextOf(ctx) ?? resolveAuth(), thesys_user.localeread, theidp-locale:USERIDrecord read through better-auth's database adapter,decideIdpLocaleWrite(...), the column write,replaceIdpLocaleRecord(...), and the never-throwwarnin thecatch.AuthPluginregisters theAuthManagerinstance itself as the kernel serviceauth(auth-plugin.ts:776,ctx.registerService('auth', this.authManager)); hosts read it withctx.getService('auth')(packages/runtime/src/dispatcher-plugin.ts:2453,packages/cloud-connection/src/cloud-connection-plugin.ts:470), and this package's own tests take it askernel.getService('auth')typed asAuthManager.AuthManageris already an entry export. TheIdpLocaleSyncthe OIDC path uses is a private field of that instance, wired to the manager's data engine, the i18n service's locales and better-auth's auth context.The change
IdpLocaleSync.apply(userId, claim, ctx?)(new, public) holds the apply half above, moved verbatim.settlenow takes the staged claim and the session'suserIdexactly as before, then callsapply(userId, claim, ctx). The only added lines inapplyare its own input guard (userIda non-empty string and a claim present, else return), so a JavaScript caller cannot make it throw before thetry.AuthManager.applyIdpLocaleClaim(userId, claim)(new, public) returnsthis.idpLocale.apply(userId, claim). It is the accessor the order prefers: the caller reaches it on the instance the kernel already exposes, and no secondIdpLocaleSyncis constructed, so the once-only "no i18n service" warning, the engine, the locale list and the auth store are the manager's own. Without an endpoint context the auth store resolves through the manager'slinkingContext()(the same fallback the OIDC path uses when a call carries no context).type IdpLocaleClaimfromidp-locale-claim.js, the argument the method takes ({ providerId?, value }).sys_user.localeis unset or still the value an IdP sign-in last wrote; a local choice is kept) and says it never throws.Exported names, and why each
AuthManager.applyIdpLocaleClaimIdpLocaleClaimIdpLocaleSyncapplythrough the manager; exporting the class invites a second instance with its own deps.readIdpLocaleClaimvalidateUserInfosource (method+oauth/ssoprofile); the sso-exchange door holds a cloud account, not that shape, and passes{ providerId, value }directly.resolveIdpLocale,decideIdpLocaleWrite,idpLocaleRecordIdentifierNo change to the rule, to the record identifier (
idp-locale:USERID), or to the OIDC path's observable behaviour: the end-of-chain OIDC cases inidp-locale-claim.test.tsare untouched and green.Changeset level:
patch, not the order'sminorThe order asked for a
minorchangeset. AGENTS.md (Post-Task Checklist step 3) grades the level by theMetadata-protocol:kind alone: "a kind takes at leastminoron a package the diff moves,nonetakespatch; a non-protocol change, widening or breaking, shipspatch". This change isMetadata-protocol: none, so the changeset is'@objectstack/plugin-auth': patch, andcheck:changeset-no-majorreads it so (Verification). Raised with the seat as a conflict between the order and AGENTS.md.Generated ledgers
None to regenerate.
@objectstack/plugin-authkeeps no api-surface snapshot (theapi-surface/ledger ispackages/spec's), andgit grep -l "authEmailLocaleFromRequest"outside the package'ssrc/finds onlydocs/qa/platform-checklist/areas/identity-auth.json, a hand-kept test checklist, not an export ledger.Tests
idp-locale-claim.test.tsimports./index(the module tsup builds as the package entry,dist/index.*) and runs the three rule cases throughentry.AuthManager#applyIdpLocaleClaimover the file's in-memoryIDataEngine(already a pinned double; no new double) and the real better-auth database adapter the manager resolves: unset ⇒zh-CNwritten with theidp-locale:record; still the recorded value ⇒en-USoverwrites both (storeden); a localja-JP⇒ kept,engine.updatenever called and thesys_verificationrows byte-identical. A fourth case shows it never throws (a refusing user store, an empty user id, no claim: all resolve, onewarn).IdpLocaleSync.prototype.applypins that a real OAuth sign-in through the platform IdP settles through it (called once with the user id,{ providerId, value: 'zh-CN' }and the endpoint context), and thatapplyIdpLocaleClaimcalls the same method../index, not as@objectstack/plugin-auth. The package cannot import its own name in vitest without a new self-alias invitest.config.ts, which is outside the declared file surface;./indexis the same module the published entry is built from.Acceptance notes
applyIdpLocaleClaimis a method ofAuthManager, not of theIAuthServicecontract inpackages/spec: adding it there would movepackages/spec, outside the declared file surface. A host that types theauthservice asIAuthServicecasts toAuthManagerto reach it, as this package's own tests do.linkingContext()); a host that calls it before the auth instance is built triggers that build, as any otherAuthManagercall that needs the auth context does.Verification
All readings on head
fd9a0e60(branch baseefa2915e;origin/mainhas since moved to9d5ebfdewith no commit underpackages/plugins/plugin-auth, so no merge was taken before opening).plugin-auth tests and typecheck (under os-verify-lock)
pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2: Test Files 202 passed (202), Tests 3849 passed, 10 skipped (3859).pnpm --filter @objectstack/plugin-auth build && pnpm --filter @objectstack/plugin-auth typecheck:VERDICT command-exit 0;check:test-typecheck: OK(10 files / 88 errors / 23 pinned signatures held intest-typecheck-debt.json, unchanged).src/idp-locale-claim.test.ts+src/oidc-userinfo-locale-claim.test.ts: 2 files, 44 tests passed.dist/index.d.tsdeclaresapplyIdpLocaleClaim(userId: string, claim: IdpLocaleClaim)returning a void promise onAuthManagerand exportstype IdpLocaleClaim;grep -c "readIdpLocaleClaim\|decideIdpLocaleWrite" dist/index.d.tsanswers 0.Ablation: the OIDC path goes through IdpLocaleSync.apply
node scripts/ablation-replace.mjs --file packages/plugins/plugin-auth/src/idp-locale-claim.ts --anchor ' await this.apply(userId, claim, ctx);' --replacement ' void claim;' -- … vitest run src/idp-locale-claim.test.ts(the test imports source, no dist leg):f27b07c0e4da→42ee4fbd0c0b;f27b07c0e4da),git diff HEADempty.Derived gates: 63 derived, 63 run, all exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 commands from 5 changed paths (191 changed lines). Two first exited 3 on prerequisites and were re-run green:check-plugin-teardown-shape --self-testaftergit fetch --depth=1 origin 621a4876(its pinned fixture, absent from the shallow clone; 48 cases pass), andcheck:dual-build-cjs-loadsafter a fullpnpm build(114/114 tasks; 112 require entry points across 69 packages load).dispatch-gates --ran: "63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED". Separately,check-changeset-no-major --base origin/main --eventon this body: exit 0, "LEVEL AXIS … a non-protocol change atpatch".Generated by Claude Code