Skip to content

feat(plugin-auth): a sign-in that skips the OIDC seams applies the IdP locale rule through AuthManager.applyIdpLocaleClaim - #27674

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-po9btk
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-po9btk

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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, IdpLocaleSync or sys_user.locale sync (git grep -n -i "idp.\?locale\|locale claim" -- skills answers 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 里拆出来),规则只有一份。
  • 为什么改:cloud 控制台的「打开」走 sso-open → sso-exchange 交接,不经过 OIDC,所以拿不到「语言跟随 cloud 账号」的规则;包入口此前没导出任何可调用的东西(cloud#3159 实测)。
  • 风险与回滚:只是加宽公开面,规则、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/main efa2915e

  • What the entry exports today. packages/plugins/plugin-auth/src/index.ts lists 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 31 export * from lines for its other modules. idp-locale-claim.ts is not among them: grep -c "idp-locale-claim" src/index.ts answers 0, and the built dist/index.d.ts names none of IdpLocaleSync, readIdpLocaleClaim, resolveIdpLocale, decideIdpLocaleWrite, idpLocaleRecordIdentifier, IdpLocaleClaim. The card's premise holds.
  • settle's apply half (idp-locale-claim.ts on efa2915e): after taking the staged claim off the endpoint context and checking the session's userId, it ran, inside one try: the supported-locale guard (no locales ⇒ one warn, nothing written), getEngine(), authContextOf(ctx) ?? resolveAuth(), the sys_user.locale read, the idp-locale:USERID record read through better-auth's database adapter, decideIdpLocaleWrite(...), the column write, replaceIdpLocaleRecord(...), and the never-throw warn in the catch.
  • How code outside the package reaches the running auth manager. AuthPlugin registers the AuthManager instance itself as the kernel service auth (auth-plugin.ts:776, ctx.registerService('auth', this.authManager)); hosts read it with ctx.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 as kernel.getService('auth') typed as AuthManager. AuthManager is already an entry export. The IdpLocaleSync the 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. settle now takes the staged claim and the session's userId exactly as before, then calls apply(userId, claim, ctx). The only added lines in apply are its own input guard (userId a non-empty string and a claim present, else return), so a JavaScript caller cannot make it throw before the try.
  • AuthManager.applyIdpLocaleClaim(userId, claim) (new, public) returns this.idpLocale.apply(userId, claim). It is the accessor the order prefers: the caller reaches it on the instance the kernel already exposes, and no second IdpLocaleSync is 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's linkingContext() (the same fallback the OIDC path uses when a call carries no context).
  • Entry export: type IdpLocaleClaim from idp-locale-claim.js, the argument the method takes ({ providerId?, value }).
  • Each published name carries a docblock with the rule in one sentence (written while sys_user.locale is 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

Name Exported Why
AuthManager.applyIdpLocaleClaim yes (a method of the already-exported class) The one call cloud#3159 needs: the user id it signs in and the cloud account's language.
IdpLocaleClaim yes (type only) The method's argument type; a TypeScript caller names it.
IdpLocaleSync no The caller reaches its apply through the manager; exporting the class invites a second instance with its own deps.
readIdpLocaleClaim no It reads a better-auth validateUserInfo source (method + oauth/sso profile); the sso-exchange door holds a cloud account, not that shape, and passes { providerId, value } directly.
resolveIdpLocale, decideIdpLocaleWrite, idpLocaleRecordIdentifier no Internal steps of the rule; the method applies them. Publishing them would let a caller rebuild the rule beside it, which is the second owner the ruling forbids.

No 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 in idp-locale-claim.test.ts are untouched and green.

Changeset level: patch, not the order's minor

The order asked for a minor changeset. AGENTS.md (Post-Task Checklist step 3) grades the level by the Metadata-protocol: kind alone: "a kind takes at least minor on a package the diff moves, none takes patch; a non-protocol change, widening or breaking, ships patch". This change is Metadata-protocol: none, so the changeset is '@objectstack/plugin-auth': patch, and check:changeset-no-major reads it so (Verification). Raised with the seat as a conflict between the order and AGENTS.md.

Generated ledgers

None to regenerate. @objectstack/plugin-auth keeps no api-surface snapshot (the api-surface/ ledger is packages/spec's), and git grep -l "authEmailLocaleFromRequest" outside the package's src/ finds only docs/qa/platform-checklist/areas/identity-auth.json, a hand-kept test checklist, not an export ledger.

Tests

  • The package entry. idp-locale-claim.test.ts imports ./index (the module tsup builds as the package entry, dist/index.*) and runs the three rule cases through entry.AuthManager#applyIdpLocaleClaim over the file's in-memory IDataEngine (already a pinned double; no new double) and the real better-auth database adapter the manager resolves: unset ⇒ zh-CN written with the idp-locale: record; still the recorded value ⇒ en-US overwrites both (stored en); a local ja-JP ⇒ kept, engine.update never called and the sys_verification rows byte-identical. A fourth case shows it never throws (a refusing user store, an empty user id, no claim: all resolve, one warn).
  • One method. A spy on IdpLocaleSync.prototype.apply pins 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 that applyIdpLocaleClaim calls the same method.
  • The package entry is imported as ./index, not as @objectstack/plugin-auth. The package cannot import its own name in vitest without a new self-alias in vitest.config.ts, which is outside the declared file surface; ./index is the same module the published entry is built from.

Acceptance notes

  • applyIdpLocaleClaim is a method of AuthManager, not of the IAuthService contract in packages/spec: adding it there would move packages/spec, outside the declared file surface. A host that types the auth service as IAuthService casts to AuthManager to reach it, as this package's own tests do.
  • The method runs with no endpoint context, so the auth store is resolved from the auth instance (linkingContext()); a host that calls it before the auth instance is built triggers that build, as any other AuthManager call that needs the auth context does.

Verification

All readings on head fd9a0e60 (branch base efa2915e; origin/main has since moved to 9d5ebfde with no commit under packages/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 in test-typecheck-debt.json, unchanged).
  • src/idp-locale-claim.test.ts + src/oidc-userinfo-locale-claim.test.ts: 2 files, 44 tests passed.
  • Built entry: dist/index.d.ts declares applyIdpLocaleClaim(userId: string, claim: IdpLocaleClaim) returning a void promise on AuthManager and exports type IdpLocaleClaim; grep -c "readIdpLocaleClaim\|decideIdpLocaleWrite" dist/index.d.ts answers 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):

  • landed: anchor 1 → 0, blob f27b07c0e4da → 42ee4fbd0c0b;
  • result: Tests 6 failed | 22 passed (28): the spy pin "a sign-in through the platform IdP settles its claim through IdpLocaleSync.apply" and five OIDC end-of-chain cases;
  • restored: blob after restore == HEAD (f27b07c0e4da), git diff HEAD empty.
Derived gates: 63 derived, 63 run, all exit 0

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 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-test after git fetch --depth=1 origin 621a4876 (its pinned fixture, absent from the shallow clone; 48 cases pass), and check:dual-build-cjs-loads after a full pnpm 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 --event on this body: exit 0, "LEVEL AXIS … a non-protocol change at patch".

node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:auth-mount-ledger :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

Generated by Claude Code

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 3 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx (via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via AuthManager (symbol, a top-level class))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v18/18-2.mdx (via AuthManager (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/index.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 33ab543913125b53feec6c116f13d616261a908c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8d55baa7fa4691bddd790997d1b4e644edc4f438 — the merge of head fd9a0e600dc5cd9c3cdcf15e71b1b29d205da9ba into base 33ab543913125b53feec6c116f13d616261a908c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 33ab543913125b53feec6c116f13d616261a908c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head fd9a0e6 · 2026-10-29T17:40Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: TIER_DEFAULT
Head-sha: fd9a0e600dc5cd9c3cdcf15e71b1b29d205da9ba
Local-runs: none

① Derived judgments

Inputs 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 main from the files endpoint), the 53 check-runs on fd9a0e60, and the cited code on the head and on origin/main (idp-locale-claim.ts, auth-manager.ts, index.ts, implicit-account-linking.ts, package.json).

Every accept-set and public-surface change the diff implies, each judged:

  1. AuthManager.applyIdpLocaleClaim(userId, claim) — new public method on an already-exported class (auth-manager.ts:2183). Public surface of the published @objectstack/plugin-auth entry grows by one method. Right. It is a thin wrapper that returns this.idpLocale.apply(userId, claim) on the manager's own IdpLocaleSync instance, so no second instance, no second engine, locale list or auth store; this is exactly the shape ruling 6496297039 allows ("export the existing IdpLocaleSync path (or a thin wrapper over it), and do not copy it"). The card's own two shapes (IdpLocaleSync plus deps type, or a free applyIdpLocale({ engine, userId, claim, supported })) would each have let a caller build a second instance with its own deps; the chosen shape is the one that keeps one owner and is reachable from outside the package through the kernel service auth (auth-plugin.ts:776).
  2. export type { IdpLocaleClaim } — new type-only export from the entry (index.ts:42). Right. It is the argument type of the method, { providerId?, value: unknown }, carries no runtime code, and the #25224 header rule of index.ts is honoured: no new module-level name of auth-manager.ts was added to the by-name list; the method rides on AuthManager, which was already listed.
  3. IdpLocaleSync.apply(userId, claim, ctx?) — new public method on an UNEXPORTED class. Not a published-surface change. Compared base settle on origin/main with head apply: the try block is moved verbatim (supported-locale guard with the once-only warn, getEngine(), authContextOf(ctx) ?? resolveAuth(), the sys_user read, the idp-locale: record read, decideIdpLocaleWrite, the column write, replaceIdpLocaleRecord, the never-throw warn). settle keeps its own guards and now ends in await this.apply(userId, claim, ctx). The rule and the record identifier idp-locale:USERID are unchanged. Right.
  4. Accept set of the new call. The only added logic is the guard if (typeof userId !== 'string' || !userId || !claim) return; before the try. A JavaScript caller passing a bad id or no claim gets a resolved promise and no write; a refusing store gets one warn. The call never throws, as every docblock says. The no-context path is sound: authContextOf(undefined) returns undefined (implicit-account-linking.ts:218), so the auth store comes from resolveAuth() = the manager's linkingContext(), the same fallback the OIDC path uses when its endpoint context carries no auth. Right.
  5. Pure helpers (readIdpLocaleClaim, resolveIdpLocale, decideIdpLocaleWrite, idpLocaleRecordIdentifier) and the class IdpLocaleSync stay unexported. Right under the one-owner ruling: publishing the steps would let a caller rebuild the rule beside it. readIdpLocaleClaim reads a better-auth validateUserInfo source shape the sso-exchange door does not hold.
  6. OIDC path's observable behaviour unchanged. The existing end-of-chain OIDC cases in idp-locale-claim.test.ts are untouched; a new spy pins that a real platform-IdP sign-in settles through IdpLocaleSync.apply once, with the user id, { providerId, value: 'zh-CN' } and the endpoint context. Right.
  7. Card "Done when". import { AuthManager, type IdpLocaleClaim } from '@objectstack/plugin-auth' gives a caller outside the package the same write-or-keep decision and the same idp-locale: record the OIDC JIT path uses; the three rule cases (unset: written with record; still the recorded value: overwritten, column and record; local choice: kept, engine.update never called, sys_verification rows byte-identical) run from the entry module. The test imports ./index, the module tsup builds as the entry, rather than the package name; the reason given (a self-alias in vitest.config.ts is outside the declared surface) holds and ./index is the same module. Accepted.
  8. Not on IAuthService in packages/spec. The method is on the concrete AuthManager, so a host typing the auth service as IAuthService casts to AuthManager, as this package's own tests do. packages/spec is outside the declared surface (ruling 6496297039: "the plugin-auth entry export and its test, with no other file under packages/plugins"), and adding it to the contract would have made this a schema-face PR. Right as scoped; see ③.

Gate verdicts, read off the check-runs on fd9a0e60: every non-skipped run concluded success — Test Core (shards 1 to 8 and the aggregate), TypeScript Type Check (workspace, source gates, consumer gates, debt ledger), Lint & Repo Gates (ESLint, repo and tooling gates, PM dispatch-gates, docs and code guards, CI/release/engine gates, scoped query-options erasure, scoped slot lookup and corpus, scoped tooling self-tests), Build Core, Check Changeset (twice), PR-body guards, Governed Surface Queue Guard, Check PR Size, Test UI, Dogfood Regression Gate, Flag docs affected by code changes, Auto Label. The skipped runs are the opt-in harnesses, the UI matrix shards, Live E2E, Build Docs, Temporal Conformance and the schedule gate: none is owed by a diff confined to packages/plugins/plugin-auth/src plus a changeset. The Docs Drift comment is advisory; its three hand-written pages name AuthManager only in passing (auth-service.mdx:56, services-checklist.mdx:298, authentication.mdx:60,427), none enumerates its methods, so no page is falsified and no docs edit is owed; the releases page is read-only by rule.

② Semver level

  • Changeset .changeset/27334-idp-locale-rule-export.md: '@objectstack/plugin-auth': patch, with the two line-initial declarations Clause-②: yes (widening) and Metadata-protocol: none, a migration-free body ("Nothing to do for existing hosts"). The PR body carries the same two lines.
  • patch is right, the order's minor is not. AGENTS.md Post-Task Checklist step 3, verbatim: "The level follows the kind alone: a kind takes at least minor on a package the diff moves, none takes patch; a non-protocol change, widening or breaking, ships patch". The diff adds no key, value or metadata type an author can write, so the kind is none and the level is patch. CLAUDE.md names AGENTS.md the single source of truth, so where the card's Work step 2 and the dispatch order said "minor", AGENTS.md governs. The Check Changeset check-run on the head concluded success, which is the gate's own reading of the same two lines.
  • Clause-②: yes (widening). Right: the diff enlarges the published entry's public surface (one method on an exported class, one type export) and removes or tightens nothing, so the (widening) arm is the only arm; no (narrowing), no BREAKING banner, no ADR-0087 disposition owed. No published schema (packages/spec untouched, no api-surface ledger moves) and no governed path in the five files, so the face is the declaration alone and the record's Served-tier is TIER_DEFAULT.
  • skip-changeset would be wrong here and was not used; the package is published ("@objectstack/plugin-auth" 18.2.0, not private, exports["."] is dist/index.*).

③ Boundary flags

  • open_questions[0] — "The order names a minor changeset; AGENTS.md grades Metadata-protocol none as patch. Which stands?" Answered: A, patch stands, as shipped. The grading rule is quoted in ② and admits no reading under which a Metadata-protocol: none change takes minor; the dispatch order and the card's "minor" were written before the level-by-kind rule was applied to this diff. Nothing to escalate.
  • out_of_scope_findings[0] — applyIdpLocaleClaim is on AuthManager, not on IAuthService (packages/spec). Accepted as out of scope: the declared surface excludes packages/spec, and the consumer named in the PR (objectstack-ai/cloud#3159) reaches the running manager through ctx.getService('auth') and casts, as this package's tests do. If the cloud half wants the call on the contract type, that is a new card on the schema face, not a rider here. Not a must-fix.
  • out_of_scope_findings[1] — three stray empty files written to / in the dev's container. Nothing in the repo; the diff's five files are the whole change. No action.
  • Dev flag — the entry test imports ./index, not @objectstack/plugin-auth. Accepted, see ① item 7.
  • Dev flag — Skills-docs: none. Consistent with the diff: no file under skills/** is touched and the change introduces no metadata key or value a skill would teach.
  • Dev flag — generated ledgers: none to regenerate. Consistent: @objectstack/plugin-auth keeps no api-surface snapshot, and the check-runs that read export ledgers (Lint & Repo Gates, Type Check · consumer gates) are green on the head.
  • Dev flag — premise_still_valid: true. Confirmed on the head's parent side: origin/main index.ts names nothing from idp-locale-claim.ts.
  • Docs Drift advisory. No edit owed (① gate paragraph); the release-owned page stays untouched.

Must-fix: none.

Implemented-by: claude/project-thread-po9btk
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

维护者速读 (席位终稿)

  • 改了什么:@objectstack/plugin-auth 的 AuthManager 新增公开方法 applyIdpLocaleClaim(userId, claim),包入口再导出 IdpLocaleClaim 类型。它调用的就是 OIDC 登录本来在用的那段规则(IdpLocaleSync.apply,从 settle 里拆出来),规则只有一份。
  • 为什么改:cloud 控制台的「打开」走 sso-open → sso-exchange 交接,不经过 OIDC,所以拿不到「语言跟随 cloud 账号」的规则;包入口此前没有导出任何可调用的东西(cloud#3159 实测)。
  • 风险与回滚:只加宽公开面。规则、idp-locale: 记录标识和 OIDC 路径行为都没变:OIDC 端到端用例原样全绿,另有 spy 钉住 OIDC 走同一个方法。该方法从不抛错。回滚 = revert 本 PR。
  • 先看:packages/plugins/plugin-auth/src/idp-locale-claim.ts(settle / apply 的拆分)、auth-manager.ts(applyIdpLocaleClaim)、index.ts(类型导出)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/idp-locale-claim.test.ts,28/28 全绿。
  • 需要你做的:无,席位落地。

席位意见:接受。分诊契约复核 PASS(6504177232,就在当前 head 上),没有 must-fix 和 should-fix。changeset 定为 patch,符合 AGENTS.md「按种类定级」:Metadata-protocol: none 取 patch。席位派单时写的 minor 是席位写错了。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-29T17:51Z

Head: fd9a0e600dc5cd9c3cdcf15e71b1b29d205da9ba · card #27334 · claim 6501842999

  1. 形态: a draft PR to main carrying Fixes #27334: 5 paths (+188 / −3), inside the claim's surface. IdpLocaleSync.settle keeps its staging half and calls a new public apply(userId, claim, ctx?) holding the apply half verbatim. AuthManager.applyIdpLocaleClaim delegates to the manager's own instance (the kernel auth service), and the entry adds type IdpLocaleClaim. Clause-②: yes (widening) / Metadata-protocol: none, one @objectstack/plugin-auth patch changeset. check-governed-merges reports NOT governed, so the ordinary queue landing applies.
  2. 前提实测: the premise holds: on origin/main, index.ts names nothing from idp-locale-claim.ts. The contract review (6504177232, PASS, this head) confirmed one owner for the rule (no copy) and the OIDC path through the same method. Its OIDC-path ablation turns 6 tests red.
  3. 门禁 check-runs (on this head): 56 runs read at 2026-10-29T17:51Z: every run is success or an expected skip, and none is red or pending. Check Changeset re-ran on the label edit and is green. Dev-side: plugin-auth 202 files / 3849 tests, typecheck green, dispatch-gates --ran 63/63 with 0 NOT-MEASURED.
  4. 接受的偏差: the entry test imports ./index, not the package name; the review accepted this because the package resolves to the same module. The changeset is patch, not the order's minor: the dev is right by the level-by-kind rule.
  5. 范围外去向: applyIdpLocaleClaim lives on AuthManager, not on the spec's IAuthService. The cloud consumer (objectstack-ai/cloud#3159) reaches it through ctx.getService('auth'). If the cloud half wants it on the contract type, that is a new card on the schema face. Skills-docs: none.
  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body (r0) and the changeset. The seat readies the PR and arms auto-merge in this act, follows it to MERGED, and lands plugin-auth: export the IdP locale rule so a non-OIDC sign-in can apply it (cloud owner door) #27334.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 17:52
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 29, 2026 17:52
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit da7bcc8 Oct 29, 2026
57 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-po9btk branch October 29, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-auth: export the IdP locale rule so a non-OIDC sign-in can apply it (cloud owner door)

1 participant