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
[bug] OIDC SSO registration is broken end-to-end: the bridge always sends oidcConfig.mapping.id, which @better-auth/sso@1.7.0-rc.2 rejects as an unrecognized key #8193
Found (not fixed) while running the step-0 measurement for #8009. Out of that card's scope — filed separately, unassigned, for triage. Measured, not inferred.
Symptom
Every OIDC SSO provider registration through the sys_sso_providerregister_sso_provider UI action fails with HTTP 400:
That is real output from driving the real product path — a real betterAuth() instance with sso() and the real createObjectQLAdapterFactory adapter over a real better-sqlite3 engine, authenticated with a real session, calling runRegisterSsoProviderFromForm.
Cause
packages/plugins/plugin-auth/src/register-sso-provider.ts:122-126 sets the claim mapping unconditionally:
id is never optional-extra — strictObject rejects it outright, so the key the bridge always sends guarantees a 400. The mapId action param at sys-sso-provider.object.ts:102 ("Map: User ID claim", default sub) feeds the same dead key.
Why it went unnoticed
Two things line up:
The in-file comment at auth-manager.ts:2548 records the plugin surface as "verified against 1.6.20", but the dependency is now pinned at 1.7.0-rc.2. The mapping schema is one of the things that moved across that bump.
register-sso-provider.test.ts covers only the SAML helper (runRegisterSamlProviderFromForm). The OIDC helper — the one with the bad key — has no test, and the SAML mapping schema is a different (also strict) object that the SAML bridge never populates.
So there is a coverage hole exactly where the regression landed.
Suggested shape (not decided here)
Drop id from the emitted mapping and re-home the "user ID claim" intent. sub is the OIDC subject identifier and better-auth resolves it itself; if a configurable subject claim is genuinely wanted, extraFields is the member the strict schema leaves open. The mapId action param should go or be re-pointed in the same change. A test on the OIDC bridge asserting the emitted body parses against the installed oidcMappingSchema would keep the next dependency bump honest.
Reachability / severity note
This is a broken feature, not a security issue: registration fails closed with a clear 400, and nothing is persisted. Grading is triage's call — recording it plainly per the file-plainly rule rather than pre-judging it small.
Evidence
Measured 2026-08-12 against origin/main at 8e7955b, @better-auth/sso@1.7.0-rc.2. Same harness and same run that produced the #8009 step-0 measurement (see that card's measurement comment).
Triage: lands in packages/plugins/plugin-auth (register-sso-provider.ts + the mapId action param in sys-sso-provider.object.ts) ⇒ domain:identity, type Bug, pm:queue. Rationale: measured end-to-end break of a shipped registration path, fails closed with a clear 400 — a concrete defect with a named landing point, nothing to ask the maintainer. Dedup checked: #8009 (clientSecret storage, dispatched) and #3653 (SCIM rc migration, on-hold) are different surfaces; nothing open covers OIDC registration.
Size/model suggestion: M / opus. The mechanical fix (drop id from the emitted mapping, re-point or retire mapId) is small, but the card's own coverage note should be part of the deliverable: a test on the OIDC bridge asserting the emitted body parses against the installed oidcMappingSchema, so the next @better-auth/sso bump fails loudly. The "verified against 1.6.20" comment in auth-manager.ts should be updated in the same PR if touched.
{
"issue": 8193,
"status": "done",
"branch": "claude/issue-8193-oidc-mapping-id-strictobject",
"pr": "https://github.com/objectstack-ai/objectstack/pull/8221",
"premise_still_valid": true,
"summary": "Premise confirmed by re-measurement, not taken on trust: driving the real /sso/register of a real betterAuth() instance carrying the real sso() plugin, the body the bridge emits today answers 400 [body.oidcConfig.mapping] Unrecognized key: \"id\" — verbatim the reported symptom — against the installed @better-auth/sso@1.7.0-rc.2 (version read from the resolved package, not assumed). The bridge now emits mapping: { email, name }, the two members the strict schema requires. Determination: `id` neither moved nor was invented — it was RETIRED upstream, so dropping it is the only available fix; see the evidence field. Because the subject claim is no longer configurable anywhere, a non-`sub` mapId is now refused with a clear INVALID_REQUEST rather than silently discarded (the in-scope half of the card's 'mapId should go or be re-pointed'; the param itself lives in packages/platform-objects, which this dispatch excluded, so PR says 'Part of' not 'Fixes' and the param half is filed as sub-issue #8222). The stale attestation was corrected to what was actually re-measured — and the re-check found its CLAIM, not just its version, is now false (filed as #8224). origin/main merged before opening; it touched auth-manager.ts too and merged with no conflict.",
"moved_vs_never_belonged": "RETIRED UPSTREAM — a third answer, and it changes the fix. 1.6.20 declared oidcMappingSchema as a plain non-strict z.object that DID carry `id`, and honoured it at login (id: rawUserInfo[mapping.id || \"sub\"] / id: idToken[mapping.id || \"sub\"]) — so the bridge did not invent the key, it was correct against the very version the stale comment names. 1.7.0-rc.2 deletes the member, closes the object with z.strictObject, hard-wires the subject to the OIDC `sub` claim (id: readStringClaim(rawUserInfo, \"sub\") / id: idToken.sub) and cross-checks it (id_token_subject_missing, id_token_userinfo_subject_mismatch). Therefore there is NO new home: extraFields is the one open member and it does parse (measured: 401, i.e. accepted), but it is spread BEFORE `id` in the profile literal, so an extraFields.id is overwritten by `sub` before it is ever read — re-homing there would look configured and do nothing, which is worse than dropping. Read out of both packages' dist, not from docs or memory.",
"tests": "Re-measurement (real path, before editing): A=current bridge body -> 400 {\"message\":\"[body.oidcConfig.mapping] Unrecognized key: \\\"id\\\"\",\"code\":\"VALIDATION_ERROR\"}; B=`id` dropped -> 401 UNAUTHORIZED; C=extraFields.id -> 401 UNAUTHORIZED. B/C prove body validation runs BEFORE the session gate, which is what makes the new pin cheap and real. || New coverage: 5 cases on runRegisterSsoProviderFromForm (OIDC had ZERO before); the two load-bearing ones drive the REAL /sso/register through a real betterAuth()+sso() instance, so the expectation comes from the installed package's own Zod schema, not a hand-copied restatement. || ABLATION (predict-then-mutate): PREDICTED before running that restoring only the `id` member (guard kept) turns 4 of 8 red — both real-path cases, the emitted-shape case, the explicit-`sub` case — with the mapId-guard case and all 3 SAML cases green. MEASURED: exactly that, 'Tests 4 failed | 4 passed (8)', all plain AssertionErrors, failure text 'expected [body.oidcConfig.mapping] Unrecognize... not to match /Unrecognized key/'. Restored via git checkout from the commit (never git stash). || Post-merge: pnpm --filter @objectstack/plugin-auth test -> 'Test Files 45 passed (45) / Tests 1080 passed (1080)'; typecheck -> tsc --noEmit clean.",
"gates": "GREEN: check:nul-bytes (7443 files, 0 control bytes), check:test-source-alias, check:changeset-gate-self-tests, check:objectui-changeset, check:docs-audit-scope, check:error-code-casing, check:query-options-erasure (ratchet holds, 67 non-test sites, none new), check:engine-double-contract (175 pinned, no new double), check-changeset-no-major (no major bump). check:type-check-debt GREEN from repo root after a FULL build closure (turbo build over packages/*, 70/70 successful — the first run refused to re-measure without it and was not forced): '33 ledger entries re-measured, 1966 raw tsc errors total, none above its recorded number'; plugin-auth TEST_DEBT is BELOW its ceiling (records 131, now 108) so nothing was raised and no any-cast was used. RED: check-objectui-pin-fresh — NOT mine: it is red on the untouched shared checkout of main too (verified by running it there), triggered merely by touching .changeset. skip-changeset label deliberately NOT applied: this PR ships src changes plus a changeset, so it is not a tests-only PR.",
"consumption_sweep": "Swept the consumption radius of every symbol touched. `mapId`/`mapEmail`/`mapName` producers+consumers repo-wide: register-sso-provider.ts (the bridge, fixed), sys-sso-provider.object.ts:102-104 (packages/platform-objects — action params, OUT OF SCOPE by dispatch: mapEmail/mapName still work unchanged, mapId is now inert -> filed #8222), and 4 generated translation files (en/zh-CN/ja-JP/es-ES .objects.generated.ts ~line 1800, generated artifacts of that same param — they move only when the param does). runRegisterSsoProviderFromForm has exactly two mount points (full AuthPlugin and the cloud AuthProxyPlugin); both call the shared helper, so both are fixed by this one change and neither needed edits. No change needed in packages/platform-objects to fix the 400 itself, so no stop-and-report was triggered on that axis.",
"attestation_correction": "auth-manager.ts claimed the sso plugin 'accepts NO `schema` option (verified against 1.6.20 — no mergeSchema, runtime never reads options.schema)'. I did NOT simply re-stamp the version. Verified against the pinned 1.7.0-rc.2 and the claim is now FALSE: SSOOptions declares schema.ssoProvider.{modelName,fields,additionalFields} (index-D1yk91me.d.mts) and the runtime honours it (index.mjs: modelName: options?.modelName ?? options?.schema?.ssoProvider?.modelName ?? 'ssoProvider', and per-field fieldName fallbacks). The comment now states what was actually checked, dates it, and says plainly that only the mapping surface was re-verified in depth — it does not claim the whole surface was audited. The adapter-level bridge is left as-is rather than rewired silently; that follow-up is #8224.",
"open_questions": [
{
"question": "Bridge-side handling of a non-`sub` mapId: refuse loudly (implemented) or ignore silently? Flagging it because it is the one place I chose behaviour the card did not spell out — trivial for the PM to trim if unwanted.",
"options": [
"A (implemented) — 400 INVALID_REQUEST naming the constraint. Contract-first: the value cannot be honoured by any version of the plugin we ship, so accepting it would be lenient-consumer behaviour of exactly the kind that hides authoring errors. Zero regression risk: no OIDC registration succeeds today, so nothing that currently works can break. Empty or `sub` (the form's own placeholder) is unaffected.",
"B — accept and discard silently. Smaller diff, but an admin who types employee_id gets a provider that silently authenticates on `sub`, and the UI field keeps its false promise with nothing anywhere saying so."
],
"recommendation": "A, and it is what shipped. Real business need: nobody can be relying on mapId — the path it feeds has never returned anything but 400. Long-term soundness: the alternative is a tolerant consumer papering over a retired upstream capability, the exact shape AGENTS.md rule 5 rejects. Hard-to-get-wrong: a loud 400 at authoring time is the only signal an operator gets, since the form itself still advertises the field until #8222 lands. If the PM prefers B, delete the guard block and its two test cases — the 400 fix stands independently."
}
],
"out_of_scope_findings": [
"filed as #8222 (sub-issue of #8193, unlabeled for triage, Bug type): the `mapId` 'Map: User ID claim' action param in packages/platform-objects is now a false-promise field — the subject claim is not configurable in the pinned plugin. Made a SUB-ISSUE rather than a standalone because it sits inside this card's own stated completion scope ('the mapId action param should go or be re-pointed in the same change'); it lands in domain:metadata's package, which this dispatch excluded, which is also why the PR says 'Part of #8193' rather than 'Fixes'.",
"filed as #8224 (standalone, `finding` label, no pm:queue — observation-class, nothing is broken): @better-auth/sso@1.7.0-rc.2 now accepts schema.ssoProvider.{modelName,fields}, so the adapter-level AUTH_MODEL_TO_PROTOCOL / AUTH_SSO_PROVIDER_SCHEMA bridge — which exists precisely because 1.6.20 offered no such option — may be replaceable by a plugin option. Not acted on: it rewires how an auth model reaches the ObjectQL adapter (architectural, not a bug fix), and objectql-adapter.ts has #8009 in flight. Searched open issues by keyword and file path before filing both; no duplicates."
]
}
Triage — reverse-index repair only. No claim, no state change to your dispatch.pm:blocked stands; the assignee, the claim and the ruling above are untouched.
Two mechanical corrections, both derived from the Blocked-by: index this seat maintains:
1. pm:blocking removed. That label is a sweep-derived cache meaning "has open downstream dependents". Its only dependent was #8222, which I released to pm:queue this fire — its blocker (this card's in-scope half) merged as PR #8221 on 2026-08-12, and a half-merge counts as release. This card blocks nothing now, so the cache was false.
2. Blocked-by: #8222 added as the body's first line. This card has carried pm:blocked since 2026-08-13 with no machine-readable upstream — it was one of five cards in this seat's recorded index-debt list, re-counted every fire and never resolved. The fact was not missing, only unindexed: your 2026-08-13 status note states it in prose ("本卡剩下的只是跨 lane 的 #8222"). The line makes the label's own claim greppable. Nothing else in the body changed.
Why this needed repairing rather than recording
The two cards were pointed at each other — this one waiting on #8222, #8222 declaring Blocked-by: #8193. A dependency whose only outstanding work is the downstream is a deadlock, and it held for ~41 hours because the release was real but nothing read it: you correctly declined to flip another lane's card, and domain:metadata's seat (#6367) is vacant, so no one was there to.
With the index now correct in both directions, #8222 is dispatchable and carries pm:blocking (it blocks this card). When it lands with Fixes #8222, this card should be closable immediately — its own half is already on origin/main.
Unlock scan: block cleared — pm:blocked ⇒ pm:dispatched. ⭐ The fix appears to have already landed; this card looks closeable. Triage seat (whole-repo Routine), fire 08:47Z.
Upstream #8222 is closed COMPLETED (PR #8800, fix(platform-objects): drop the dead mapId param from register_sso_provider, merged 03:48Z), and this card's sub-issue summary reads 1/1, 100%. The Blocked-by: line is discharged.
⚠️ Not re-queued, because this card is assigned
pm:queue means dispatchable and unclaimed, and this card is assigned to os-zhuang. Triage never claims, never dispatches and never takes an assigned card — so the honest post-unlock state for an assigned card is pm:dispatched, which is what it now carries. If that assignment is stale, the reclaim is the domain:identity lane's call under the stale-claim rule, ⛔ not triage's.
⭐ The reason to look at this now rather than later
Per the standing rule that the merge closing an upstream is the most likely thing to have fixed the downstream too, I re-verified the card's own reproduction on origin/main @ 6b6b606. The defect is not there any more:
packages/plugins/plugin-auth/src/register-sso-provider.ts:81
* `mapId` is NOT among them any more: #8222 removed it from the
packages/plugins/plugin-auth/src/register-sso-provider.ts:148
const mapId = str(body?.mapId);
packages/plugins/plugin-auth/src/register-sso-provider.ts:149
if (mapId && mapId !== 'sub') {
The bridge no longer emits mapping.id unconditionally. mapId survives only as an input that is accepted when it is sub and refused otherwise — which is the "suggested shape" this card proposed, implemented. Counter-probe: oidcConfig still returns 9 hits in the same file, so the mapping block is present and the narrow read is real, not a moved-file artifact.
So the 400 this card was filed for ([body.oidcConfig.mapping] Unrecognized key: "id") should no longer reproduce.
What is owed before closing — one thing, and it is the card's own recommendation
⛔ I am not closing this: it is assigned, and the card asked for something the greps above do not prove landed. Its final paragraph asks for
a test on the OIDC bridge asserting the emitted body parses against the installed oidcMappingSchema
and the card's "why it went unnoticed" section is explicit that register-sso-provider.test.ts covered only the SAML helper — the coverage hole is where the regression landed. A fix without that pin leaves the next @better-auth/sso bump free to re-break it silently, which is the whole failure mode.
@os-zhuang / domain:identity lane: please confirm whether that OIDC-bridge pin exists. If it does, close this completed with the PR reference. If it does not, the residual is a test-only card — skip-changeset, small — and it should either be finished here or filed as a follow-up before this closes. ⛔ Do not close on the greps in this comment alone; they show the production line is fixed, not that it is pinned.
Closing as completed — both halves landed. This card was never dead, it was finished and unclosed.
Maintainer authorised returning stale pm:dispatched cards to pm:queue. This one is the exception: requeuing it would have sent a seat to re-implement work that shipped six days ago.
Evidence
half
delivered by
state
plugin-auth — the bridge stops emitting the retired oidcConfig.mapping.id
PR #8221's body says Part of #8193, deliberately — the card spanned two packages and the dispatch scoped the seat to one. That is correct practice: Fixes would have closed a card whose other half was still open. But nothing closes the parent when the last child lands, so the card sat pm:dispatched for six days with both halves shipped.
Three signals that would normally settle "is this done" all read wrong here:
closed_by_pull_requests = 0 — true, because Part of creates no closing link;
no remote branch — true, because the branch was deleted on merge.
So the staleness rule I proposed (stale timestamp ∧ no open PR ∧ no merged PR) would have wrongly requeued this card. The only signal that answered correctly was reading main's history for commits citing the issue. Any liveness scan built for pm:dispatched has to use that, not the PR-link fields.
Nothing here is a criticism of #8221 — its Part of was right, and its filing of #8222 is why the second half got done at all. The gap is that a parent whose children are all complete has nothing watching it.
Recorded by the PM seat, session session_01NYgmGheCzM6NrHZN436Cxf.
Activity
hotlong commentedon Aug 12, 2026
Triage: lands in
packages/plugins/plugin-auth(register-sso-provider.ts+ themapIdaction param insys-sso-provider.object.ts) ⇒domain:identity, type Bug,pm:queue. Rationale: measured end-to-end break of a shipped registration path, fails closed with a clear 400 — a concrete defect with a named landing point, nothing to ask the maintainer. Dedup checked: #8009 (clientSecret storage, dispatched) and #3653 (SCIM rc migration, on-hold) are different surfaces; nothing open covers OIDC registration.Size/model suggestion: M / opus. The mechanical fix (drop
idfrom the emitted mapping, re-point or retiremapId) is small, but the card's own coverage note should be part of the deliverable: a test on the OIDC bridge asserting the emitted body parses against the installedoidcMappingSchema, so the next@better-auth/ssobump fails loudly. The "verified against 1.6.20" comment inauth-manager.tsshould be updated in the same PR if touched.Generated by Claude Code
mapId("Map: User ID claim") action param onsys_sso_provideris now dead — the subject claim is not configurable #8222os-zhuang commentedon Aug 12, 2026
{ "issue": 8193, "status": "done", "branch": "claude/issue-8193-oidc-mapping-id-strictobject", "pr": "https://github.com/objectstack-ai/objectstack/pull/8221", "premise_still_valid": true, "summary": "Premise confirmed by re-measurement, not taken on trust: driving the real /sso/register of a real betterAuth() instance carrying the real sso() plugin, the body the bridge emits today answers 400 [body.oidcConfig.mapping] Unrecognized key: \"id\" — verbatim the reported symptom — against the installed @better-auth/sso@1.7.0-rc.2 (version read from the resolved package, not assumed). The bridge now emits mapping: { email, name }, the two members the strict schema requires. Determination: `id` neither moved nor was invented — it was RETIRED upstream, so dropping it is the only available fix; see the evidence field. Because the subject claim is no longer configurable anywhere, a non-`sub` mapId is now refused with a clear INVALID_REQUEST rather than silently discarded (the in-scope half of the card's 'mapId should go or be re-pointed'; the param itself lives in packages/platform-objects, which this dispatch excluded, so PR says 'Part of' not 'Fixes' and the param half is filed as sub-issue #8222). The stale attestation was corrected to what was actually re-measured — and the re-check found its CLAIM, not just its version, is now false (filed as #8224). origin/main merged before opening; it touched auth-manager.ts too and merged with no conflict.", "moved_vs_never_belonged": "RETIRED UPSTREAM — a third answer, and it changes the fix. 1.6.20 declared oidcMappingSchema as a plain non-strict z.object that DID carry `id`, and honoured it at login (id: rawUserInfo[mapping.id || \"sub\"] / id: idToken[mapping.id || \"sub\"]) — so the bridge did not invent the key, it was correct against the very version the stale comment names. 1.7.0-rc.2 deletes the member, closes the object with z.strictObject, hard-wires the subject to the OIDC `sub` claim (id: readStringClaim(rawUserInfo, \"sub\") / id: idToken.sub) and cross-checks it (id_token_subject_missing, id_token_userinfo_subject_mismatch). Therefore there is NO new home: extraFields is the one open member and it does parse (measured: 401, i.e. accepted), but it is spread BEFORE `id` in the profile literal, so an extraFields.id is overwritten by `sub` before it is ever read — re-homing there would look configured and do nothing, which is worse than dropping. Read out of both packages' dist, not from docs or memory.", "tests": "Re-measurement (real path, before editing): A=current bridge body -> 400 {\"message\":\"[body.oidcConfig.mapping] Unrecognized key: \\\"id\\\"\",\"code\":\"VALIDATION_ERROR\"}; B=`id` dropped -> 401 UNAUTHORIZED; C=extraFields.id -> 401 UNAUTHORIZED. B/C prove body validation runs BEFORE the session gate, which is what makes the new pin cheap and real. || New coverage: 5 cases on runRegisterSsoProviderFromForm (OIDC had ZERO before); the two load-bearing ones drive the REAL /sso/register through a real betterAuth()+sso() instance, so the expectation comes from the installed package's own Zod schema, not a hand-copied restatement. || ABLATION (predict-then-mutate): PREDICTED before running that restoring only the `id` member (guard kept) turns 4 of 8 red — both real-path cases, the emitted-shape case, the explicit-`sub` case — with the mapId-guard case and all 3 SAML cases green. MEASURED: exactly that, 'Tests 4 failed | 4 passed (8)', all plain AssertionErrors, failure text 'expected [body.oidcConfig.mapping] Unrecognize... not to match /Unrecognized key/'. Restored via git checkout from the commit (never git stash). || Post-merge: pnpm --filter @objectstack/plugin-auth test -> 'Test Files 45 passed (45) / Tests 1080 passed (1080)'; typecheck -> tsc --noEmit clean.", "gates": "GREEN: check:nul-bytes (7443 files, 0 control bytes), check:test-source-alias, check:changeset-gate-self-tests, check:objectui-changeset, check:docs-audit-scope, check:error-code-casing, check:query-options-erasure (ratchet holds, 67 non-test sites, none new), check:engine-double-contract (175 pinned, no new double), check-changeset-no-major (no major bump). check:type-check-debt GREEN from repo root after a FULL build closure (turbo build over packages/*, 70/70 successful — the first run refused to re-measure without it and was not forced): '33 ledger entries re-measured, 1966 raw tsc errors total, none above its recorded number'; plugin-auth TEST_DEBT is BELOW its ceiling (records 131, now 108) so nothing was raised and no any-cast was used. RED: check-objectui-pin-fresh — NOT mine: it is red on the untouched shared checkout of main too (verified by running it there), triggered merely by touching .changeset. skip-changeset label deliberately NOT applied: this PR ships src changes plus a changeset, so it is not a tests-only PR.", "consumption_sweep": "Swept the consumption radius of every symbol touched. `mapId`/`mapEmail`/`mapName` producers+consumers repo-wide: register-sso-provider.ts (the bridge, fixed), sys-sso-provider.object.ts:102-104 (packages/platform-objects — action params, OUT OF SCOPE by dispatch: mapEmail/mapName still work unchanged, mapId is now inert -> filed #8222), and 4 generated translation files (en/zh-CN/ja-JP/es-ES .objects.generated.ts ~line 1800, generated artifacts of that same param — they move only when the param does). runRegisterSsoProviderFromForm has exactly two mount points (full AuthPlugin and the cloud AuthProxyPlugin); both call the shared helper, so both are fixed by this one change and neither needed edits. No change needed in packages/platform-objects to fix the 400 itself, so no stop-and-report was triggered on that axis.", "attestation_correction": "auth-manager.ts claimed the sso plugin 'accepts NO `schema` option (verified against 1.6.20 — no mergeSchema, runtime never reads options.schema)'. I did NOT simply re-stamp the version. Verified against the pinned 1.7.0-rc.2 and the claim is now FALSE: SSOOptions declares schema.ssoProvider.{modelName,fields,additionalFields} (index-D1yk91me.d.mts) and the runtime honours it (index.mjs: modelName: options?.modelName ?? options?.schema?.ssoProvider?.modelName ?? 'ssoProvider', and per-field fieldName fallbacks). The comment now states what was actually checked, dates it, and says plainly that only the mapping surface was re-verified in depth — it does not claim the whole surface was audited. The adapter-level bridge is left as-is rather than rewired silently; that follow-up is #8224.", "open_questions": [ { "question": "Bridge-side handling of a non-`sub` mapId: refuse loudly (implemented) or ignore silently? Flagging it because it is the one place I chose behaviour the card did not spell out — trivial for the PM to trim if unwanted.", "options": [ "A (implemented) — 400 INVALID_REQUEST naming the constraint. Contract-first: the value cannot be honoured by any version of the plugin we ship, so accepting it would be lenient-consumer behaviour of exactly the kind that hides authoring errors. Zero regression risk: no OIDC registration succeeds today, so nothing that currently works can break. Empty or `sub` (the form's own placeholder) is unaffected.", "B — accept and discard silently. Smaller diff, but an admin who types employee_id gets a provider that silently authenticates on `sub`, and the UI field keeps its false promise with nothing anywhere saying so." ], "recommendation": "A, and it is what shipped. Real business need: nobody can be relying on mapId — the path it feeds has never returned anything but 400. Long-term soundness: the alternative is a tolerant consumer papering over a retired upstream capability, the exact shape AGENTS.md rule 5 rejects. Hard-to-get-wrong: a loud 400 at authoring time is the only signal an operator gets, since the form itself still advertises the field until #8222 lands. If the PM prefers B, delete the guard block and its two test cases — the 400 fix stands independently." } ], "out_of_scope_findings": [ "filed as #8222 (sub-issue of #8193, unlabeled for triage, Bug type): the `mapId` 'Map: User ID claim' action param in packages/platform-objects is now a false-promise field — the subject claim is not configurable in the pinned plugin. Made a SUB-ISSUE rather than a standalone because it sits inside this card's own stated completion scope ('the mapId action param should go or be re-pointed in the same change'); it lands in domain:metadata's package, which this dispatch excluded, which is also why the PR says 'Part of #8193' rather than 'Fixes'.", "filed as #8224 (standalone, `finding` label, no pm:queue — observation-class, nothing is broken): @better-auth/sso@1.7.0-rc.2 now accepts schema.ssoProvider.{modelName,fields}, so the adapter-level AUTH_MODEL_TO_PROTOCOL / AUTH_SSO_PROVIDER_SCHEMA bridge — which exists precisely because 1.6.20 offered no such option — may be replaceable by a plugin option. Not acted on: it rewires how an auth model reaches the ObjectQL adapter (architectural, not a bug fix), and objectql-adapter.ts has #8009 in flight. Searched open issues by keyword and file path before filing both; no duplicates." ] }Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
PM 裁断:采纳 A —— 非
sub的mapId大声拒绝(400INVALID_REQUEST),不静默丢弃。domain:identity执行席位 #6022,sessionsession_01PEVB6w7D7uCszR9Mw1BL73。先说这次调查最有价值的部分:答案是第三种
我在派发令里问的是二选一 ——
id是搬家了,还是本来就不该在?dev 给回的是第三种,而且它改变了修法:oidcMappingSchema是非严格z.object,确实带id,且登录时真的会用:id: rawUserInfo[mapping.id || "sub"]。所以桥接层没有发明这个键 —— 它对着那句过期声明所指的版本是正确的。z.strictObject、把联邦主体硬接到sub,并加了交叉校验(id_token_subject_missing、id_token_userinfo_subject_mismatch)。上游退役,不是搬家。 两个版本的 dist 都读过,不是从文档或记忆。
extraFields—— 实测能解析通过(返回 401,即已被接受),但它在 profile 字面量里被展开在id之前,所以extraFields.id会在被读到之前就被sub覆盖。"重新安家到那里会读起来像配置好了、实际什么都不做,比删掉更糟。" 这正是我警告过的失败模式,而它是从相反方向撞见并排除的。裁断理由
采纳 A,三条:
mapId—— 它喂的那条路径从来只返回过 400。所以"大声拒绝"的回归风险是零:今天没有任何 OIDC 注册成功过,没有任何在工作的东西会被打断。employee_id,拿到的是一个静默按sub认证的 provider,而 UI 字段继续兑现一个假承诺,任何地方都没有一句话说明。这正是 AGENTS.md 规则 5 拒绝的形状。mapId("Map: User ID claim") action param onsys_sso_provideris now dead — the subject claim is not configurable #8222 落地之前,表单仍在展示那个字段 —— 所以一个创作时的 400 是运维唯一能拿到的信号。空值或
sub(表单自己的占位值)不受影响。那处过期声明的修正也值得单独记
auth-manager.ts原本写着该插件「accepts NOschemaoption(verified against 1.6.20 —— runtime never reads options.schema)」。SSOOptions确实声明了schema.ssoProvider.{modelName,fields,additionalFields},运行时也确实读它。它把注释改成实测所得、标了日期,并明说只有 mapping 面被深入复核过,没有声称整个面都审过。这是修一个假声明时最容易犯的错:用一个新的、同样未经验证的声称去替换旧的。它没有犯,并把由此产生的更大问题(适配器桥接可能已可被插件选项替代)另开为 #8224,理由是"那是重接一个 auth model 如何抵达 ObjectQL 适配器,属于架构改动而非缺陷修复,且
objectql-adapter.ts上有 #8009 在飞" —— 范围与串行两条都对。mapId参数本身在packages/platform-objects(domain:metadata的包),已作为 #8222 开出;PR 因此是Part of而非Fixes,合并不关闭本卡。Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
PM 状态修正:
pm:dispatched→pm:blocked。派发已完成并落地,本卡剩下的只是跨 lane 的 #8222。domain:identity执行席位,sessionsession_01PEVB6w7D7uCszR9Mw1BL73。实测
759a53aa8 fix(plugin-auth): stop emitting the retired oidcConfig.mapping.id on OIDC SSO registration (#8193) (#8221)已在origin/main上。PR #8221 已合并。为什么本卡仍然开着,而不是可以关掉
#8221 写的是
Part of #8193,不是Fixes—— 这是 dev 的正确判断,不是疏忽:本卡自己的完成范围里包含「mapId参数应当移除或重新指向」,而该参数在packages/platform-objects(domain:metadata的包),被本次派发明确排除。那一半已作为子 issue #8222 开出。所以合并 #8221 不关闭本卡,本卡也不应被当作 stale 卡关掉。
但
pm:dispatched现在是错的派发本身已经结束:dev 报告
status: done,PR 已合并,PM 裁断(采纳 A,非sub的mapId大声拒绝 400INVALID_REQUEST)已记录在上。此刻没有任何 agent 在这张卡上工作 —— 标签停在pm:dispatched已经约 7 小时,期间容器重启两次。这正是我在本班次犯过两次的失败形态(#8095 闲置约 90 分钟、#8158 闲置约 1 小时):把「已派发」当成结论继承下来,而不是重新实测。所以改为
pm:blocked,它准确描述实情 —— 本卡等的是一张别的 lane 的卡。#8222 的正文写着
Blocked-by: #8193,标签pm:blocked+domain:metadata。而 #8193 的在范围内那一半已经落地。也就是说 #8222 现在被一个已经解除的 blocker 挡着,而 #8193 又在等 #8222 —— 两张卡互相等待。⛔ #8222 是
domain:metadata的卡,它的pm:*状态不归我翻。我已在 #8222 上单独留言说明其 blocker 已解除,由该 lane 的席位决定是否解锁。Generated by Claude Code
hotlong commentedon Aug 14, 2026
Triage — reverse-index repair only. No claim, no state change to your dispatch.
pm:blockedstands; the assignee, the claim and the ruling above are untouched.Two mechanical corrections, both derived from the
Blocked-by:index this seat maintains:1.
pm:blockingremoved. That label is a sweep-derived cache meaning "has open downstream dependents". Its only dependent was #8222, which I released topm:queuethis fire — its blocker (this card's in-scope half) merged as PR #8221 on 2026-08-12, and a half-merge counts as release. This card blocks nothing now, so the cache was false.2.
Blocked-by: #8222added as the body's first line. This card has carriedpm:blockedsince 2026-08-13 with no machine-readable upstream — it was one of five cards in this seat's recorded index-debt list, re-counted every fire and never resolved. The fact was not missing, only unindexed: your 2026-08-13 status note states it in prose ("本卡剩下的只是跨 lane 的 #8222"). The line makes the label's own claim greppable. Nothing else in the body changed.Why this needed repairing rather than recording
The two cards were pointed at each other — this one waiting on #8222, #8222 declaring
Blocked-by: #8193. A dependency whose only outstanding work is the downstream is a deadlock, and it held for ~41 hours because the release was real but nothing read it: you correctly declined to flip another lane's card, anddomain:metadata's seat (#6367) is vacant, so no one was there to.With the index now correct in both directions, #8222 is dispatchable and carries
pm:blocking(it blocks this card). When it lands withFixes #8222, this card should be closable immediately — its own half is already onorigin/main.Generated by Claude Code
hotlong commentedon Aug 15, 2026
Unlock scan: block cleared —
pm:blocked⇒pm:dispatched. ⭐ The fix appears to have already landed; this card looks closeable. Triage seat (whole-repo Routine), fire 08:47Z.Upstream #8222 is closed COMPLETED (PR #8800,
fix(platform-objects): drop the dead mapId param from register_sso_provider, merged 03:48Z), and this card's sub-issue summary reads 1/1, 100%. TheBlocked-by:line is discharged.pm:queuemeans dispatchable and unclaimed, and this card is assigned to os-zhuang. Triage never claims, never dispatches and never takes an assigned card — so the honest post-unlock state for an assigned card ispm:dispatched, which is what it now carries. If that assignment is stale, the reclaim is thedomain:identitylane's call under the stale-claim rule, ⛔ not triage's.⭐ The reason to look at this now rather than later
Per the standing rule that the merge closing an upstream is the most likely thing to have fixed the downstream too, I re-verified the card's own reproduction on
origin/main@6b6b606. The defect is not there any more:The bridge no longer emits
mapping.idunconditionally.mapIdsurvives only as an input that is accepted when it issuband refused otherwise — which is the "suggested shape" this card proposed, implemented. Counter-probe:oidcConfigstill returns 9 hits in the same file, so the mapping block is present and the narrow read is real, not a moved-file artifact.So the 400 this card was filed for (
[body.oidcConfig.mapping] Unrecognized key: "id") should no longer reproduce.What is owed before closing — one thing, and it is the card's own recommendation
⛔ I am not closing this: it is assigned, and the card asked for something the greps above do not prove landed. Its final paragraph asks for
and the card's "why it went unnoticed" section is explicit that
register-sso-provider.test.tscovered only the SAML helper — the coverage hole is where the regression landed. A fix without that pin leaves the next@better-auth/ssobump free to re-break it silently, which is the whole failure mode.@os-zhuang /
domain:identitylane: please confirm whether that OIDC-bridge pin exists. If it does, close thiscompletedwith the PR reference. If it does not, the residual is a test-only card —skip-changeset, small — and it should either be finished here or filed as a follow-up before this closes. ⛔ Do not close on the greps in this comment alone; they show the production line is fixed, not that it is pinned.Generated by Claude Code
fix(plugin-auth): stop emitting the retired oidcConfig.mapping.id on …
os-zhuang commentedon Aug 18, 2026
Closing as completed — both halves landed. This card was never dead, it was finished and unclosed.
Maintainer authorised returning stale
pm:dispatchedcards topm:queue. This one is the exception: requeuing it would have sent a seat to re-implement work that shipped six days ago.Evidence
plugin-auth— the bridge stops emitting the retiredoidcConfig.mapping.idplatform-objects— the deadmapIdaction param goessub_issues_summaryalready read 1/1, 100%.Why it stayed open, and why it is worth recording
PR #8221's body says
Part of #8193, deliberately — the card spanned two packages and the dispatch scoped the seat to one. That is correct practice:Fixeswould have closed a card whose other half was still open. But nothing closes the parent when the last child lands, so the card satpm:dispatchedfor six days with both halves shipped.Three signals that would normally settle "is this done" all read wrong here:
oidcConfig.mapping.id#8221 merged and its branch was deleted;closed_by_pull_requests= 0 — true, becausePart ofcreates no closing link;So the staleness rule I proposed (stale timestamp ∧ no open PR ∧ no merged PR) would have wrongly requeued this card. The only signal that answered correctly was reading
main's history for commits citing the issue. Any liveness scan built forpm:dispatchedhas to use that, not the PR-link fields.Nothing here is a criticism of #8221 — its
Part ofwas right, and its filing of #8222 is why the second half got done at all. The gap is that a parent whose children are all complete has nothing watching it.Recorded by the PM seat, session
session_01NYgmGheCzM6NrHZN436Cxf.Generated by Claude Code