Repository navigation
Commit da7bcc8
feat(plugin-auth): a sign-in that skips the OIDC seams applies the IdP locale rule through AuthManager.applyIdpLocaleClaim (#27674)
domain:spec#3 · os-support-ai · session_01WGufzgrxXwu9nECtirwHEK
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBBw7sfgcdYpSr4xrGUrivAR)_
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).
<details><summary>plugin-auth tests and typecheck (under
os-verify-lock)</summary>
- `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.
</details>
<details><summary>Ablation: the OIDC path goes through
IdpLocaleSync.apply</summary>
`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.
</details>
<details><summary>Derived gates: 63 derived, 63 run, all exit
0</summary>
`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 621a487` (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`".
```text
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
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzgrxXwu9nECtirwHEK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6cf3e0b commit da7bcc8
5 files changed
Lines changed: 188 additions & 3 deletions
File tree
- .changeset
- packages/plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
2169 | 2169 | | |
2170 | 2170 | | |
2171 | 2171 | | |
| 2172 | + | |
| 2173 | + | |
| 2174 | + | |
| 2175 | + | |
| 2176 | + | |
| 2177 | + | |
| 2178 | + | |
| 2179 | + | |
| 2180 | + | |
| 2181 | + | |
| 2182 | + | |
| 2183 | + | |
| 2184 | + | |
| 2185 | + | |
| 2186 | + | |
2172 | 2187 | | |
2173 | 2188 | | |
2174 | 2189 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
15 | 19 | | |
16 | 20 | | |
17 | 21 | | |
18 | 22 | | |
| 23 | + | |
| 24 | + | |
19 | 25 | | |
20 | 26 | | |
21 | 27 | | |
| |||
436 | 442 | | |
437 | 443 | | |
438 | 444 | | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
60 | 65 | | |
61 | 66 | | |
62 | 67 | | |
| |||
91 | 96 | | |
92 | 97 | | |
93 | 98 | | |
94 | | - | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
95 | 105 | | |
96 | 106 | | |
97 | 107 | | |
| |||
195 | 205 | | |
196 | 206 | | |
197 | 207 | | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
198 | 224 | | |
199 | 225 | | |
200 | 226 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
36 | 43 | | |
37 | 44 | | |
38 | 45 | | |
| |||
0 commit comments