You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Identity: the JIT user path reads the IdP locale claim into the user language preference (hosted environments follow the cloud account language) #25261
Filing gate ②: the framework half the maintainer ruled on objectstack-ai/cloud#3125 (batch #387 item 1, column 现在立 card 5: 「用户语言偏好随云账号进入每个环境」, platform first). Filed by repo:cloud#1 (seat post #6026, session session_01FSg7JRUiTPo8GrgT38tAWP). ⛔ Not a claim.
Reader who acts: the objectstack lane. The cloud half (the control plane's OIDC provider emits locale) is objectstack-ai/cloud#3159, which is Blocked-by: this one.
Wanted
When the environment creates or refreshes a user just-in-time from an external IdP sign-in (the cloud as IdP on hosted environments, or any OIDC IdP), it reads the standard OIDC locale claim (BCP 47) into the user's language preference.
On first creation, the claim sets the preference.
On a later sign-in, the claim updates it only while the user has not set a preference in the environment itself; a local choice wins.
An unknown or unsupported locale falls back to the deployment default, closed against the supported locale list.
Acceptance
A JIT sign-in with locale: "zh-CN" creates a user whose console is zh-CN.
A later sign-in with locale: "en" after the user picked ja locally keeps ja.
Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · the whole card · 2026-10-22T21:08Z
Session: session_019PQiYTjCGrkiAcRc8e5mrU
Account: os-sales (the seat's linked user; the card's assignee)
Branch: claude/project-thread-6usz2l (the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
Worktree: objectstack-issue-25261
Domain: domain:services
Seat: domain:services#1 (seat post #6021)
File surface (read on origin/main49185cd; stop on a breach and explain it in the report):
packages/plugins/plugin-auth/src/idp-locale-claim.ts (new): reads the standard OIDC locale claim off the IdP profile better-auth hands the validateUserInfo seam (source.oauth.profile for generic-OAuth / social providers, the cloud IdP included; source.sso.profile for @better-auth/sso OIDC providers), closes it against the i18n service's reported locales, and decides the sys_user.locale write: set on first creation; on a later sign-in, follow the claim only while the stored value is still the one the IdP last wrote (a local choice wins); an unknown or unsupported claim leaves the column unset, so the deployment default applies.
packages/plugins/plugin-auth/src/auth-manager.ts: three call sites only: stage the claim in validateUserInfo, settle it in the composed session.create.after (the one seam every successful IdP sign-in reaches, new user or returning), and clear the IdP record in the composed user.delete.after.
The "what the IdP last wrote" record is a sys_verification row per user, written through better-auth's database adapter (the implicit-linking unlink record's idiom in the same package): no schema change, no new column.
Tests: idp-locale-claim.test.ts (pure decision + an end-to-end OAuth round trip over the in-memory engine, the implicit-account-linking harness shape).
One patch changeset for @objectstack/plugin-auth (Metadata-protocol: none takes patch).
⛔ Not packages/spec, packages/platform-objects (no column or rule change), service-messaging, or the objectui console (it already reads sys_user.locale as the signed-in user's language).
Container & model: M, card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier on this surface: "no path-derived mandate"), effort: high
Clause-②: no
Metadata-protocol: none
Basis for the Clause-② line: no published accept set or public surface changes; a JIT sign-in now fills a column a user could already set for themselves, and nothing a caller could do before is refused.
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared:
os-dev-report
{
"issue": 25261,
"status": "done",
"branch": "claude/project-thread-6usz2l",
"pr": "#25303",
"session": "session_019PQiYTjCGrkiAcRc8e5mrU (card thread)",
"premise_still_valid": true,
"summary": "New module plugin-auth/src/idp-locale-claim.ts: the validateUserInfo seam stages the OIDC locale claim off the IdP profile better-auth hands it (source.oauth.profile for generic-OAuth and social providers incl. the cloud IdP, source.sso.profile for sso-oidc), keyed by the request endpoint context; the composed session.create.after settles it into sys_user.locale. First creation sets it; a later sign-in follows the claim only while the column is unset or still holds what the IdP last wrote (recorded per user in a sys_verification row through the better-auth database adapter, the unlink-record idiom); a local choice wins; an unknown or unsupported claim leaves the column unset (clears an IdP-written value) so the deployment default applies, closed against the i18n service getLocales() via core resolveLocale with the supported spelling stored. auth-manager.ts gains only three call sites plus the instance. Changeset patch, Clause-②: no, Metadata-protocol: none.",
"tests": "idp-locale-claim.test.ts 22/22 (13 pure, 9 end-to-end OAuth round trips through the platform IdP over the in-memory engine, incl. the card's three acceptance cases). Ablation via scripts/ablation-replace.mjs, source-imported subject (no dist leg): settle call removed 5 failed / 17 passed; local-choice guard removed 4 failed / 18 passed; supported-list closing removed 5 failed / 17 passed; each restore proven (blob == HEAD, git diff HEAD empty). On final head 611c99a: pnpm build VERDICT command-exit 0 (76/76 tasks); pnpm --filter @objectstack/plugin-auth test Test Files 186 passed (186), Tests 3515 passed | 10 skipped (3525); plugin-auth typecheck (src, examples, test layer) exit 0. All under os-verify-lock.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 611c99a: 109 derived, all exit 0 (check:type-check-debt under the lock; plugin-teardown self-test after fetching its pinned fixture commit; dual-build-cjs-loads after the full build). --ran: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN. Roster gates under touched dirs (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity): exit 0. First pass fixes: check:durability-log-level flagged a helper whose name collided with the vocabulary callee writeRecord (renamed replaceIdpLocaleRecord); check:engine-double-contract ledger learned the new test file (--write); check-tenant-audit-census counted the new sys_user write (236 to 237): artefacts regenerated with --write and the six hand-written page figures moved by hand. CI on the PR: in_progress at report time.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "5 — reads only: issue_read x2, search_pull_requests x1, pull_request_read x2 (plus the harness PR-activity subscription, not a GitHub write)",
"api_writes": "5 relay strokes, all via scripts/pm (objectstack-fleet[bot]): label-write #25261 (assignee os-sales, +pm:dispatched -pm:queue); post-stamped claim comment 6351359330; dispatch pr_create #25303 (draft, assignee os-sales); dispatch pr_request_reviewers #25303 (os-sales); post-stamped this report. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [],
"notes": "File surface: three derived-ledger paths outside the claim (scripts/engine-double-contract.pinned.json, content/docs/permissions/tenant-audit-census.mdx, docs/audits/2026-08-tenant-audit-write-call-sites.counts.md) are the gates' own ledgers for the declared write, declared in the PR body. Serial: the #25224 move-only split of auth-manager.ts is in flight; this change keeps its logic in a new file. PR body written once at pr_create."
}
State: the card closed completed through the PR; this stroke clears pm:dispatched and the assignee. The cloud half (the control plane emitting locale) is objectstack-ai/cloud#3159, outside this repo.
Activity
objectstack-fleet commentedon Oct 22, 2026
Claim: card thread ·
domain:servicesseat 1 (seat post #6021, project run by the Sales account) · p2 · the whole card · 2026-10-22T21:08ZSession:
session_019PQiYTjCGrkiAcRc8e5mrUAccount:
os-sales(the seat's linked user; the card's assignee)Branch:
claude/project-thread-6usz2l(the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)Worktree:
objectstack-issue-25261Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (read on
origin/main49185cd; stop on a breach and explain it in the report):packages/plugins/plugin-auth/src/idp-locale-claim.ts(new): reads the standard OIDClocaleclaim off the IdP profile better-auth hands thevalidateUserInfoseam (source.oauth.profilefor generic-OAuth / social providers, the cloud IdP included;source.sso.profilefor@better-auth/ssoOIDC providers), closes it against the i18n service's reported locales, and decides thesys_user.localewrite: set on first creation; on a later sign-in, follow the claim only while the stored value is still the one the IdP last wrote (a local choice wins); an unknown or unsupported claim leaves the column unset, so the deployment default applies.packages/plugins/plugin-auth/src/auth-manager.ts: three call sites only: stage the claim invalidateUserInfo, settle it in the composedsession.create.after(the one seam every successful IdP sign-in reaches, new user or returning), and clear the IdP record in the composeduser.delete.after.sys_verificationrow per user, written through better-auth's database adapter (the implicit-linking unlink record's idiom in the same package): no schema change, no new column.idp-locale-claim.test.ts(pure decision + an end-to-end OAuth round trip over the in-memory engine, the implicit-account-linking harness shape).patchchangeset for@objectstack/plugin-auth(Metadata-protocol: nonetakespatch).packages/spec,packages/platform-objects(no column or rule change),service-messaging, or the objectui console (it already readssys_user.localeas the signed-in user's language).Container & model:
M, card thread (in-session executor),model: default tier(dispatch-gates.mjs --tieron this surface: "no path-derived mandate"),effort: highClause-②: no
Metadata-protocol: none
Basis for the Clause-② line: no published accept set or public surface changes; a JIT sign-in now fills a column a user could already set for themselves, and nothing a caller could do before is refused.
Responsibility:
n/a — not a defect cardThread-read: none
Serial constraints cleared:
src/auth-manager.ts(10,338 lines, +1,867 in 30 days) along itsAuthManagerseams, one move-only slice per PR #25224 move-only split ofauth-manager.tsis in flight (cut 1 landed as PR refactor(plugin-auth): move AuthManager's actor and organization guards into auth-manager-actor-guards.ts (#25224 cut 1) #25273); this card adds three call sites there and keeps its logic in a new file, so whichever lands second mergesmain.last-admin-guard.tsonly; not on this surface.Resume-from: none
Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{
"issue": 25261,
"status": "done",
"branch": "claude/project-thread-6usz2l",
"pr": "#25303",
"session": "session_019PQiYTjCGrkiAcRc8e5mrU (card thread)",
"premise_still_valid": true,
"summary": "New module plugin-auth/src/idp-locale-claim.ts: the validateUserInfo seam stages the OIDC locale claim off the IdP profile better-auth hands it (source.oauth.profile for generic-OAuth and social providers incl. the cloud IdP, source.sso.profile for sso-oidc), keyed by the request endpoint context; the composed session.create.after settles it into sys_user.locale. First creation sets it; a later sign-in follows the claim only while the column is unset or still holds what the IdP last wrote (recorded per user in a sys_verification row through the better-auth database adapter, the unlink-record idiom); a local choice wins; an unknown or unsupported claim leaves the column unset (clears an IdP-written value) so the deployment default applies, closed against the i18n service getLocales() via core resolveLocale with the supported spelling stored. auth-manager.ts gains only three call sites plus the instance. Changeset patch, Clause-②: no, Metadata-protocol: none.",
"tests": "idp-locale-claim.test.ts 22/22 (13 pure, 9 end-to-end OAuth round trips through the platform IdP over the in-memory engine, incl. the card's three acceptance cases). Ablation via scripts/ablation-replace.mjs, source-imported subject (no dist leg): settle call removed 5 failed / 17 passed; local-choice guard removed 4 failed / 18 passed; supported-list closing removed 5 failed / 17 passed; each restore proven (blob == HEAD, git diff HEAD empty). On final head 611c99a: pnpm build VERDICT command-exit 0 (76/76 tasks); pnpm --filter @objectstack/plugin-auth test Test Files 186 passed (186), Tests 3515 passed | 10 skipped (3525); plugin-auth typecheck (src, examples, test layer) exit 0. All under os-verify-lock.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 611c99a: 109 derived, all exit 0 (check:type-check-debt under the lock; plugin-teardown self-test after fetching its pinned fixture commit; dual-build-cjs-loads after the full build). --ran: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN. Roster gates under touched dirs (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity): exit 0. First pass fixes: check:durability-log-level flagged a helper whose name collided with the vocabulary callee writeRecord (renamed replaceIdpLocaleRecord); check:engine-double-contract ledger learned the new test file (--write); check-tenant-audit-census counted the new sys_user write (236 to 237): artefacts regenerated with --write and the six hand-written page figures moved by hand. CI on the PR: in_progress at report time.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "5 — reads only: issue_read x2, search_pull_requests x1, pull_request_read x2 (plus the harness PR-activity subscription, not a GitHub write)",
"api_writes": "5 relay strokes, all via scripts/pm (objectstack-fleet[bot]): label-write #25261 (assignee os-sales, +pm:dispatched -pm:queue); post-stamped claim comment 6351359330; dispatch pr_create #25303 (draft, assignee os-sales); dispatch pr_request_reviewers #25303 (os-sales); post-stamped this report. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [],
"notes": "File surface: three derived-ledger paths outside the claim (scripts/engine-double-contract.pinned.json, content/docs/permissions/tenant-audit-census.mdx, docs/audits/2026-08-tenant-audit-write-call-sites.counts.md) are the gates' own ledgers for the declared write, declared in the PR body. Serial: the #25224 move-only split of auth-manager.ts is in flight; this change keeps its logic in a new file. PR body written once at pr_create."
}
objectstack-fleet commentedon Oct 22, 2026
Landed ·
domain:servicesseat 1 (seat post #6021) ·os-sales·session_016g7nYP9wdNx1GDa5XDr7Uy· 2026-10-22T23:13Z6352928671)origin/main829cb77917State: the card closed
completedthrough the PR; this stroke clearspm:dispatchedand the assignee. The cloud half (the control plane emittinglocale) is objectstack-ai/cloud#3159, outside this repo.Generated by Claude Code