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
plugin-auth: a signed-out invitation preview read, so the invitation landing page can name the inviting organization and the inviter (server half of #25886) #26020
Ruled: 6413975861 · letter Q1 A / Q2 B · 2026-10-26T10:39Z
Filing gate ④: the cross-layer child of #25886 (its server layer). Filed by seat domain:ui#1 (seat post #25966) · os-steve · session session_017iXjUm1rujwDk7f3KT1Pkk, from the #25886 card thread's measurement before coding. ⛔ Not a claim. The first step is measure reach first: this card adds a read that answers a viewer with no session, so it can leak data if it is scoped wrong.
What was measured
plugin-auth pins better-auth 1.7.12. Its GET /organization/get-invitation (crud-invites.mjs, getInvitation) answers 401 "Not authenticated" when there is no session.
With a session it also requires session.user.email to equal the invited address, and it may require a verified email.
auth-route-ledger.ts lists no anonymous invitation read.
The ui's getInvitation calls only that endpoint. So a signed-out viewer who opens an invitation link cannot be shown the inviting organization, the inviter, or the invited address.
What #25886's triage allowed (06:59Z, verbatim boundary)
The signed-out page may show only what the invitation itself carries: the inviting org's display name, the inviter's display name and the invited address. It reads them through the invitation id the link already holds, and only for a valid, unexpired invitation. Nothing about the org beyond that. Triage also said that a server read for a signed-out viewer is the part to put before review.
Ask
An anonymous read keyed by the invitation id that:
gives one uniform not-found for an unknown, expired, accepted, rejected or cancelled id, so the answer does not tell those states apart;
is registered in auth-route-ledger.ts.
Whether to mask the invited address, rate-limit the route, or rule the read out entirely is for the server lane, and for the maintainer if triage reads it as a product choice.
Business reason
Who benefits: a person invited to a customer's organization who is not yet signed in. That is how a customer's seats arrive. Scenario: the link opens on a generic 欢迎回来 login page with no word of who invited them or to what. Left undone, the ui can say only "sign in or sign up to accept the invitation" (the #25886 ui slice, Part of #25886). It cannot say "Alice invited you to Acme".
Who acts on it
The domain:server lane (plugin-auth), one dispatch. After it lands, the ui seat 1 reads the new route in LoginPage.tsx / AcceptInvitationPage.tsx under #25886, which stays open for that.
Duplicate check
search_issues on objectstack-ai/objectstack, "anonymous signed-out invitation read endpoint plugin-auth get-invitation preview inviter organization name": 20 hits. The top ones are #25886 (the parent), #25802 (zh-CN invitation mail role name), and the closed #24898, #24865, #24883, #24082, #23945 and #24016. None asks for a signed-out invitation read.
A, anonymous read with masking (triage leaning). A read keyed by the invitation id that returns { organizationName, inviterName, email } with the email masked (for example a***@acme.com). It answers only for a pending, unexpired invitation. Unknown, expired, accepted, rejected and cancelled ids all get one uniform not-found. It is rate-limited and registered in auth-route-ledger.ts. A forwarded link then shows who invited and to which org, but not the full address. Workload: S–M, plus a contract review.
B, as A but unmasked email. The register form can prefill it. A forwarded or leaked link discloses the full invited address. Workload: the same as A.
C, no anonymous read. The page stays generic. Zero work, but invitees still land without context.
Lean: A. The invitation id is already a bearer secret in the mail, and naming the org and inviter to its holder is common practice. Masking keeps a forwarded link from also leaking the address.
#25886 lists a second server-side item of the same kind. Its body (item 4 and the "Optional, needs a server decision" line) asks to skip the second verification mail when the registering address equals the invitation's address and the invitation link was just used. That is also an identity / verification-posture change in plugin-auth, so the seat is putting it in front of the same decision as this read, not filing a third card. Two answers are wanted: (1) the anonymous preview read this card asks for, and (2) whether an invitation-link sign-up may count the address as verified. The ui half (#26044, Part of #25886) needs neither, and #25886 stays open for both.
Director seat, summon #51 continuation (ledger objectstack#12708). Presented to the maintainer in batch #395 as item 1 of 3, with two questions as the domain:ui seat 2 note 6413438154 asks. Seat increment:
ADR-0138 D2 governs this door. The anonymous principal is admitted by a closed list of six door classes; the record says a new surface answers 401 until the list names it, and naming a new class amends the record. The nearest class is 2 (share links: a capability token admits one validated read of one record). An invitation preview keyed by the invitation id is that shape in plugin-auth — so Q1 A or B lands only with a dated D2 row or note in ADR-0138 (docs/adr/**, Tier H), the uniform not-found, the rate limit ADR-0121 D6 requires of guest doors, and the auth-route-ledger.ts row. Today's ledger row GET /api/v1/auth/organization/get-invitation (auth-route-ledger.ts:343) is the authenticated read only; git grep -iE 'invitation preview|anonymous invitation' on AGENTS.md + docs/adr + packages/spec/src → 0 (control get-invitation → 1 hit).
Q2 (folded from Invitation link while signed out: the generic 欢迎回来 login page with no inviter or organization, no prefilled email, and a second verification mail after sign-up #25886 item 4, its "optional, needs a server decision" line): may an invitation-link sign-up count the registering address as verified and skip the second mail? Reading: the invitation id is a bearer secret that can be forwarded (the same fact Q1's masking answers), so a forwarded link that also mints a verified account under the invited address is an identity-posture change, not a convenience; better-auth's own acceptance requires a session whose email matches and may require it verified. The seat leans Q2 B (keep the verification mail; prefill nothing beyond the masked address); Q2 A (count it verified) is on the floor as a weakening of the verification posture.
Half-state read, not written: the card carries needs-user-decision and pm:awaiting-maintainer together (triage first grade 6413156565); the second leaves in the ruling's stroke.
Facets block (the card carried none):
os-decision-facets
① 项目长远合理性:Q1 A 把新门登记进 ADR-0138 的封闭清单(第 2 类形态),匿名面仍只有一张表;Q2 B 保持「邮箱只由邮箱自己证明」一种验证机制。
Facets read: same direction on Q1; the seat agrees with Q1 A (masked email, uniform not-found, rate-limited, ledger row, ADR-0138 D2 amendment) and recommends Q2 B. Workload (card column): S–M server + contract review + the ADR note (Tier H); Q2 B is 0.
Ruling: batch #395 item 1 · letter Q1 A / Q2 B · maintainer 「26020 Q1 A Q2 B 26001 A」 2026-10-26T10:38Z
Recorded by the director seat (summon #51 continuation, session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708, brief 6413837742).
Q1 → A, a conditional ruling in three parts.
① Route: an anonymous read in plugin-auth keyed by the invitation id that returns exactly { organizationName, inviterName, email } with the email masked (shape the dev proposes, e.g. a***@acme.com); answers only for a pending, unexpired invitation; one uniform not-found for an unknown, expired, accepted, rejected or cancelled id; rate-limited as ADR-0121 D6 requires of a guest door; registered in auth-route-ledger.ts; and declared in ADR-0138 D2's closed list as a class-2-shaped door (capability token → one validated read of one record, projected) by a dated row or note in docs/adr/0138-…md (Tier H, the maintainer's merge). Not B (a forwarded link would disclose the full address) and not C (invitees keep landing without context).
② Premises the dev verifies first: (a) auth-route-ledger.ts lists no anonymous invitation read (re-check git grep -n 'get-invitation' packages/plugins/plugin-auth/src/auth-route-ledger.ts → the authenticated row only); (b) ADR-0138 D2's six classes do not already admit this surface (re-check the D2 table; shouldDenyAnonymous at the auth domain's entry answers 401 today); (c) better-auth 1.7.12's getInvitation cannot be configured to serve a signed-out projection, so the read is the plugin's own route.
③ A premise failing ⇒ report a fork (needs_decision); ⛔ no forcing, ⛔ no silent widening to B.
Q2 → B. An invitation-link sign-up does not count the registering address as verified; the second verification mail stays. The invitation id is a forwardable bearer secret (the same fact that makes Q1's masking necessary), so possession of the link is not possession of the mailbox. #25886 item 4 ("optional, needs a server decision") is answered: keep; no card.
Workload of the chosen letters (card column): Q1 A server S–M + contract review + the ADR-0138 note (Tier H); Q2 B 0.
Execution: needs-user-decision → pm:queue in this act (domain:server, p2, security, area:identity); pm:awaiting-maintainer is removed in the same stroke (it sat beside the decision label as a half-state; no Maintainer-action: line). The dispatch carries Ruling-ref: naming this comment; Clause-②: no, Metadata-protocol: none (no metadata key; a new REST route with its closed query-parameter set per AGENTS.md route rule 5). After it lands, domain:ui seat 2 reads the route under #25886.
Claim: PM loop round 3 · card thread
Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01YSfdNdSer8xUQULiYH3Yq5
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-6ezkt3 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#2
Ruling-ref: ruling 6413975861 (Q1 → A, Q2 → B), verbatim: an anonymous read keyed by the invitation id returning exactly { organizationName, inviterName, email } with the email masked, pending and unexpired only, one uniform not-found, rate-limited per ADR-0121 D6, registered in auth-route-ledger.ts, declared by a dated row or note in ADR-0138 D2; its ruling ② premises are verified first and a failing one is a needs_decision fork, ⛔ no widening to the full address; and 「所有的安全卡不要创建issue,直接派发处理修复」 (no new issue, no advisory)
File surface: packages/plugins/plugin-auth/** (the new read, auth-route-ledger.ts, tests), one dated row or note in docs/adr/0138-* D2 (this makes the PR Tier H: it stays draft for the maintainer's merge), one changeset. ⛔ No packages/spec/** (stop on breach; explain in the report)
Container & model: M, security, mode:thread, default model and effort (set at thread open)
Clause-②: no
Metadata-protocol: none
Responsibility: n/a — an enhancement card under a maintainer ruling
Thread-read: 6413975861
Premise: holds (read 2026-10-26T12:22Z at origin/maincc378e93ba: card open, unassigned, pm:queue, ruling 6413975861 the newest comment); a failing ruling ② premise is a legal needs_decision hand-back
Resume-from: none — fresh start from origin/main
Serial constraints cleared: no open PR names #26020; no in-flight claim of seat 1 or seat 2 declares auth-route-ledger.ts; the dev re-measures open PRs on packages/plugins/plugin-auth/** before pr_create
Claim: PM loop round 3 · card thread · amendment (supersedes 6415899256)
Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01YSfdNdSer8xUQULiYH3Yq5
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-6ezkt3 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#2
Ruling-ref: ruling 6413975861 (Q1 → A, Q2 → B), verbatim: an anonymous read keyed by the invitation id returning exactly { organizationName, inviterName, email } with the email masked, pending and unexpired only, one uniform not-found, rate-limited per ADR-0121 D6, registered in auth-route-ledger.ts, declared by a dated row or note in ADR-0138 D2; its ruling ② premises are verified first and a failing one is a needs_decision fork, ⛔ no widening to the full address; and 「所有的安全卡不要创建issue,直接派发处理修复」 (no new issue, no advisory)
File surface: packages/plugins/plugin-auth/** (the new read, auth-route-ledger.ts, tests), packages/qa/dogfood/test/anonymous-door-sweep.classification.ts (the one new route added to class 2's match, so the sweep follows the ADR-0138 amendment as its criterion 2 requires), one dated row or note in docs/adr/0138-* D2 (this makes the PR Tier H: it stays draft for the maintainer's merge), one changeset. ⛔ No packages/spec/** (stop on breach; explain in the report)
Container & model: M, security, mode:thread, default model and effort (set at thread open)
Clause-②: no
Metadata-protocol: none
Responsibility: n/a — an enhancement card under a maintainer ruling
Thread-read: 6413975861
Premise: holds (read 2026-10-26T12:26Z at origin/maincc378e93ba: card open, unassigned, pm:queue, ruling 6413975861 the newest comment); a failing ruling ② premise is a legal needs_decision hand-back
Resume-from: none — fresh start from origin/main
Serial constraints cleared: no open PR names #26020 or touches anonymous-door-sweep.classification.ts (read across every open PR at this act); no in-flight claim of seat 1 or seat 2 declares auth-route-ledger.ts; the dev re-measures open PRs on its surface before pr_create
Claim: PM loop round 3 · card thread · amendment (supersedes 6415970059)
Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01YSfdNdSer8xUQULiYH3Yq5
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-6ezkt3 (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#2
Ruling-ref: ruling 6413975861 (Q1 → A, Q2 → B), verbatim: an anonymous read keyed by the invitation id returning exactly { organizationName, inviterName, email } with the email masked, pending and unexpired only, one uniform not-found, rate-limited per ADR-0121 D6, registered in auth-route-ledger.ts, declared by a dated row or note in ADR-0138 D2; its ruling ② premises are verified first and a failing one is a needs_decision fork, ⛔ no widening to the full address; and 「所有的安全卡不要创建issue,直接派发处理修复」 (no new issue, no advisory)
File surface: packages/plugins/plugin-auth/** (the new read, auth-route-ledger.ts, tests), packages/rest/src/index.ts (one additive export of unknownQueryParamRefusalMessage, so the new route answers AGENTS.md route rule 5 with the one shared implementation; @objectstack/rest patch in the changeset), packages/qa/dogfood/test/anonymous-door-sweep.classification.ts (the one new route added to class 2's match, so the sweep follows the ADR-0138 amendment as its criterion 2 requires), one dated row or note in docs/adr/0138-* D2 (this makes the PR Tier H: it stays draft for the maintainer's merge), one changeset. ⛔ No packages/spec/** (stop on breach; explain in the report)
Container & model: M, security, mode:thread, default model and effort (set at thread open)
Clause-②: no
Metadata-protocol: none
Responsibility: n/a — an enhancement card under a maintainer ruling
Thread-read: 6413975861
Premise: holds (read 2026-10-26T12:27Z at origin/maincc378e93ba: card open, unassigned, pm:queue, ruling 6413975861 the newest comment); a failing ruling ② premise is a legal needs_decision hand-back
Resume-from: none — fresh start from origin/main
Serial constraints cleared: no open PR names #26020 or touches anonymous-door-sweep.classification.ts (read across every open PR at this act); no in-flight claim of seat 1 or seat 2 declares auth-route-ledger.ts; the dev re-measures open PRs on its surface before pr_create
{
"issue": 26020,
"status": "done",
"branch": "claude/project-thread-6ezkt3",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26151",
"session": "session_01YSfdNdSer8xUQULiYH3Yq5",
"premise_still_valid": true,
"summary": "Added GET /api/v1/auth/organization/invitation-preview?id=INVITATION_ID in plugin-auth, a raw-app mount ahead of the catch-all. It answers a signed-out caller with exactly { organizationName, inviterName, email }: email masked (first char + *** + domain), inviterName null when the inviter is gone. It answers only for a pending, unexpired invitation; one uniform 404 RESOURCE_NOT_FOUND for unknown, expired, accepted, rejected or cancelled ids (and a gone organization), each decided after one read. 10 requests per client address per 60 s, counted before parsing, in the kernel cache when registered (lazy counter store). Closed query set { id } via the newly exported @objectstack/rest unknownQueryParamRefusalMessage. The session is never read. Also: ledger row (source objectstack, disposition public, requires organization), dated ADR-0138 D2 note plus status amendment entry (Tier H, PR stays draft), the route added to dogfood sweep class 2 (claim amendment 6415981287), changeset plugin-auth + rest patch. Ruling 2 premises: (a), (b) and (c) all hold at cc378e93ba. Measured correction to (b)'s re-check note: the runtime auth domain does not call shouldDenyAnonymous (packages/runtime/src/domains/auth.ts names it only in a comment). The whole /auth namespace is exempt via isAuthGateAllowlisted, which is why the sweep classification edit was needed; the seat agreed it is not a fork. Route and response shape for domain:ui seat 2 are in the PR body.",
"tests": "New packages/plugins/plugin-auth/src/invitation-preview.test.ts, real mount chain (AuthPlugin.registerAuthRoutes on Hono + real AuthManager on better-auth 1.7.12; invitations issued, accepted, rejected and cancelled through vendor routes): 17 own cases green (26 in the file incl. 9 re-registered from impersonation-bearer-rotation.test via its createMemoryEngine export). Ablations via scripts/ablation-replace.mjs, each anchor 1 -> 0 and restored to the HEAD blob with git diff HEAD empty: delete status !== 'pending' line -> uniform not-found leg red; unmask address -> projection + inviter-gone legs red; disable limit -> both rate-limit legs red. Final head 34e297bde, after merging origin/main and rebuilding the closure: pnpm --filter @objectstack/plugin-auth test 198 files / 3779 passed / 10 skipped; plugin-auth typecheck green incl. test layer; client ledger suites (client-url-conformance, auth-route-ledger-coverage, route-ledger-response-schema) 3 files / 10 passed. Pre-merge: rest test 323 files / 6474 passed, rest typecheck green. eslint --no-inline-config on touched files clean. Sweep classification read statically with its own classify(): preview -> door 2, control get-invitation -> outside control-plane-allowlist, class 2 expectation accepts the bare probe's 400.",
"gates": "Local on head 34e297bde: dispatch-gates --commands derived 85 families; all 85 run, exit codes captured before any pipe, all exit 0; --ran: '85 derived famil(ies) accounted for — 85 run, 0 NOT-MEASURED'. (Pre-merge run: check-plugin-teardown-shape --self-test exit 3 on the shallow clone, green after git fetch origin 621a4876; check:dual-build-cjs-loads exit 3 then, exit 0 on the final head.) check-auth-mount-ledger: 22 mounts all accounted for. CI check-runs on #26151: in_progress at report time.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the anonymous door sweep (packages/qa/dogfood) NOT MEASURED locally, reason: it boots the showcase on a full build; it runs with the main battery",
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "2 — relay fleet-write pr_create (POST /repos/objectstack-ai/objectstack/pulls + assignee, run 40335163273) and this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/26020/comments); git push is not a REST write",
"open_questions": [],
"out_of_scope_findings": [
"none filed. noted, not filed: ruling 6413975861 (b)'s re-check note says shouldDenyAnonymous answers 401 at the auth domain's entry; measured false at cc378e93ba (the /auth namespace is wholly allowlisted). The ruling's route is unchanged; carrier: this report, the seat agreed it is no fork"
]
}
ACCEPT → PR #26151 comment 6418450300 (head 34e297bdea) · domain:server seat 2 · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-26T14:24Z. The head is green and the maintainer's approval is on it; auto-merge is on, so it lands through the queue.
PR #26151 merged at 2026-10-26T14:54Z by the merge queue as 83a26c38d0 on main (Fixes #26020; ACCEPT 6418450300; Tier H, approved by a governed approver). The merge closed this card as completed. · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-26T14:56Z
GET /repos/objectstack-ai/objectstack/issues/26020
closed, completed
Landed: the signed-out invitation preview, with its ADR-0138 amendment, its auth route ledger entry and its anonymous-door sweep classification. The changeset is .changeset/26020-signed-out-invitation-preview.md.
Remains: none on this card.
State: closed by the merge. The card thread is done.
Activity
objectstack-fleet commentedon Oct 26, 2026
Triage: first grade —
enhancement·security·priority:p2·domain:server·area:identity·needs-user-decision·pm:awaiting-maintainer· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-26T09:54Z⛔ Not a claim, ⛔ not a dispatch.
plugin-auth. That is a security boundary, on the human floor. Invitation link while signed out: the generic 欢迎回来 login page with no inviter or organization, no prefilled email, and a second verification mail after sign-up #25886's ui slice ("sign in or sign up to accept") does not wait on it.{ organizationName, inviterName, email }with the email masked (for examplea***@acme.com). It answers only for apending, unexpired invitation. Unknown, expired, accepted, rejected and cancelled ids all get one uniform not-found. It is rate-limited and registered inauth-route-ledger.ts. A forwarded link then shows who invited and to which org, but not the full address. Workload: S–M, plus a contract review.Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Seat note ·
domain:uiseat 2 (now owns #25886) · 2026-10-26T10:09ZSession:
session_01SMps7wq7rBTk53e8KTSEup#25886 lists a second server-side item of the same kind. Its body (item 4 and the "Optional, needs a server decision" line) asks to skip the second verification mail when the registering address equals the invitation's address and the invitation link was just used. That is also an identity / verification-posture change in
plugin-auth, so the seat is putting it in front of the same decision as this read, not filing a third card. Two answers are wanted: (1) the anonymous preview read this card asks for, and (2) whether an invitation-link sign-up may count the address as verified. The ui half (#26044,Part of #25886) needs neither, and #25886 stays open for both.objectstack-fleet commentedon Oct 26, 2026
Brief-claimed-by: session_01KrKXQVvgYk2AJ4VkqMobyb · batch #395 · 2026-10-26T10:30Z
Director seat, summon #51 continuation (ledger objectstack#12708). Presented to the maintainer in batch #395 as item 1 of 3, with two questions as the
domain:uiseat 2 note 6413438154 asks. Seat increment:plugin-auth— so Q1 A or B lands only with a dated D2 row or note in ADR-0138 (docs/adr/**, Tier H), the uniform not-found, the rate limit ADR-0121 D6 requires of guest doors, and theauth-route-ledger.tsrow. Today's ledger rowGET /api/v1/auth/organization/get-invitation(auth-route-ledger.ts:343) is the authenticated read only;git grep -iE 'invitation preview|anonymous invitation'on AGENTS.md + docs/adr + packages/spec/src → 0 (controlget-invitation→ 1 hit).needs-user-decisionandpm:awaiting-maintainertogether (triage first grade 6413156565); the second leaves in the ruling's stroke.os-decision-facets
Prior rulings read:
get-invitation·ADR-0138 D2·ADR-0121 D6→ 3 hits (the ledger row, D2's closed list, the guest-door rate limit); ADR-0138; thread: 6413438154.objectstack-fleet commentedon Oct 26, 2026
Ruling: batch #395 item 1 · letter Q1 A / Q2 B · maintainer 「26020 Q1 A Q2 B 26001 A」 2026-10-26T10:38Z
Recorded by the director seat (summon #51 continuation,
session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708, brief 6413837742).Q1 → A, a conditional ruling in three parts.
① Route: an anonymous read in
plugin-authkeyed by the invitation id that returns exactly{ organizationName, inviterName, email }with the email masked (shape the dev proposes, e.g.a***@acme.com); answers only for apending, unexpired invitation; one uniform not-found for an unknown, expired, accepted, rejected or cancelled id; rate-limited as ADR-0121 D6 requires of a guest door; registered inauth-route-ledger.ts; and declared in ADR-0138 D2's closed list as a class-2-shaped door (capability token → one validated read of one record, projected) by a dated row or note indocs/adr/0138-…md(Tier H, the maintainer's merge). Not B (a forwarded link would disclose the full address) and not C (invitees keep landing without context).② Premises the dev verifies first: (a)
auth-route-ledger.tslists no anonymous invitation read (re-checkgit grep -n 'get-invitation' packages/plugins/plugin-auth/src/auth-route-ledger.ts→ the authenticated row only); (b) ADR-0138 D2's six classes do not already admit this surface (re-check the D2 table;shouldDenyAnonymousat the auth domain's entry answers 401 today); (c) better-auth 1.7.12'sgetInvitationcannot be configured to serve a signed-out projection, so the read is the plugin's own route.③ A premise failing ⇒ report a fork (
needs_decision); ⛔ no forcing, ⛔ no silent widening to B.Q2 → B. An invitation-link sign-up does not count the registering address as verified; the second verification mail stays. The invitation id is a forwardable bearer secret (the same fact that makes Q1's masking necessary), so possession of the link is not possession of the mailbox. #25886 item 4 ("optional, needs a server decision") is answered: keep; no card.
Workload of the chosen letters (card column): Q1 A server S–M + contract review + the ADR-0138 note (Tier H); Q2 B 0.
Execution:
needs-user-decision→pm:queuein this act (domain:server, p2,security,area:identity);pm:awaiting-maintaineris removed in the same stroke (it sat beside the decision label as a half-state; noMaintainer-action:line). The dispatch carriesRuling-ref:naming this comment;Clause-②: no,Metadata-protocol: none(no metadata key; a new REST route with its closed query-parameter set per AGENTS.md route rule 5). After it lands,domain:uiseat 2 reads the route under #25886.objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 3 · card thread
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01YSfdNdSer8xUQULiYH3Yq5Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-6ezkt3(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: ruling
6413975861(Q1 → A, Q2 → B), verbatim: an anonymous read keyed by the invitation id returning exactly{ organizationName, inviterName, email }with the email masked, pending and unexpired only, one uniform not-found, rate-limited per ADR-0121 D6, registered inauth-route-ledger.ts, declared by a dated row or note in ADR-0138 D2; its ruling ② premises are verified first and a failing one is aneeds_decisionfork, ⛔ no widening to the full address; and 「所有的安全卡不要创建issue,直接派发处理修复」 (no new issue, no advisory)File surface:
packages/plugins/plugin-auth/**(the new read,auth-route-ledger.ts, tests), one dated row or note indocs/adr/0138-*D2 (this makes the PR Tier H: it stays draft for the maintainer's merge), one changeset. ⛔ Nopackages/spec/**(stop on breach; explain in the report)Container & model:
M,security,mode:thread, default model and effort (set at thread open)Clause-②: no
Metadata-protocol: none
Responsibility: n/a — an enhancement card under a maintainer ruling
Thread-read: 6413975861
Premise: holds (read 2026-10-26T12:22Z at
origin/maincc378e93ba: card open, unassigned,pm:queue, ruling6413975861the newest comment); a failing ruling ② premise is a legalneeds_decisionhand-backResume-from: none — fresh start from
origin/mainSerial constraints cleared: no open PR names #26020; no in-flight claim of seat 1 or seat 2 declares
auth-route-ledger.ts; the dev re-measures open PRs onpackages/plugins/plugin-auth/**beforepr_createGenerated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 3 · card thread · amendment (supersedes 6415899256)
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01YSfdNdSer8xUQULiYH3Yq5Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-6ezkt3(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: ruling
6413975861(Q1 → A, Q2 → B), verbatim: an anonymous read keyed by the invitation id returning exactly{ organizationName, inviterName, email }with the email masked, pending and unexpired only, one uniform not-found, rate-limited per ADR-0121 D6, registered inauth-route-ledger.ts, declared by a dated row or note in ADR-0138 D2; its ruling ② premises are verified first and a failing one is aneeds_decisionfork, ⛔ no widening to the full address; and 「所有的安全卡不要创建issue,直接派发处理修复」 (no new issue, no advisory)File surface:
packages/plugins/plugin-auth/**(the new read,auth-route-ledger.ts, tests),packages/qa/dogfood/test/anonymous-door-sweep.classification.ts(the one new route added to class 2's match, so the sweep follows the ADR-0138 amendment as its criterion 2 requires), one dated row or note indocs/adr/0138-*D2 (this makes the PR Tier H: it stays draft for the maintainer's merge), one changeset. ⛔ Nopackages/spec/**(stop on breach; explain in the report)Container & model:
M,security,mode:thread, default model and effort (set at thread open)Clause-②: no
Metadata-protocol: none
Responsibility: n/a — an enhancement card under a maintainer ruling
Thread-read: 6413975861
Premise: holds (read 2026-10-26T12:26Z at
origin/maincc378e93ba: card open, unassigned,pm:queue, ruling6413975861the newest comment); a failing ruling ② premise is a legalneeds_decisionhand-backResume-from: none — fresh start from
origin/mainSerial constraints cleared: no open PR names #26020 or touches
anonymous-door-sweep.classification.ts(read across every open PR at this act); no in-flight claim of seat 1 or seat 2 declaresauth-route-ledger.ts; the dev re-measures open PRs on its surface beforepr_createGenerated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Claim: PM loop round 3 · card thread · amendment (supersedes 6415970059)
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01YSfdNdSer8xUQULiYH3Yq5Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-6ezkt3(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: ruling
6413975861(Q1 → A, Q2 → B), verbatim: an anonymous read keyed by the invitation id returning exactly{ organizationName, inviterName, email }with the email masked, pending and unexpired only, one uniform not-found, rate-limited per ADR-0121 D6, registered inauth-route-ledger.ts, declared by a dated row or note in ADR-0138 D2; its ruling ② premises are verified first and a failing one is aneeds_decisionfork, ⛔ no widening to the full address; and 「所有的安全卡不要创建issue,直接派发处理修复」 (no new issue, no advisory)File surface:
packages/plugins/plugin-auth/**(the new read,auth-route-ledger.ts, tests),packages/rest/src/index.ts(one additive export ofunknownQueryParamRefusalMessage, so the new route answers AGENTS.md route rule 5 with the one shared implementation;@objectstack/restpatch in the changeset),packages/qa/dogfood/test/anonymous-door-sweep.classification.ts(the one new route added to class 2's match, so the sweep follows the ADR-0138 amendment as its criterion 2 requires), one dated row or note indocs/adr/0138-*D2 (this makes the PR Tier H: it stays draft for the maintainer's merge), one changeset. ⛔ Nopackages/spec/**(stop on breach; explain in the report)Container & model:
M,security,mode:thread, default model and effort (set at thread open)Clause-②: no
Metadata-protocol: none
Responsibility: n/a — an enhancement card under a maintainer ruling
Thread-read: 6413975861
Premise: holds (read 2026-10-26T12:27Z at
origin/maincc378e93ba: card open, unassigned,pm:queue, ruling6413975861the newest comment); a failing ruling ② premise is a legalneeds_decisionhand-backResume-from: none — fresh start from
origin/mainSerial constraints cleared: no open PR names #26020 or touches
anonymous-door-sweep.classification.ts(read across every open PR at this act); no in-flight claim of seat 1 or seat 2 declaresauth-route-ledger.ts; the dev re-measures open PRs on its surface beforepr_createGenerated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
os-dev-report
{ "issue": 26020, "status": "done", "branch": "claude/project-thread-6ezkt3", "pr": "https://github.com/objectstack-ai/objectstack/pull/26151", "session": "session_01YSfdNdSer8xUQULiYH3Yq5", "premise_still_valid": true, "summary": "Added GET /api/v1/auth/organization/invitation-preview?id=INVITATION_ID in plugin-auth, a raw-app mount ahead of the catch-all. It answers a signed-out caller with exactly { organizationName, inviterName, email }: email masked (first char + *** + domain), inviterName null when the inviter is gone. It answers only for a pending, unexpired invitation; one uniform 404 RESOURCE_NOT_FOUND for unknown, expired, accepted, rejected or cancelled ids (and a gone organization), each decided after one read. 10 requests per client address per 60 s, counted before parsing, in the kernel cache when registered (lazy counter store). Closed query set { id } via the newly exported @objectstack/rest unknownQueryParamRefusalMessage. The session is never read. Also: ledger row (source objectstack, disposition public, requires organization), dated ADR-0138 D2 note plus status amendment entry (Tier H, PR stays draft), the route added to dogfood sweep class 2 (claim amendment 6415981287), changeset plugin-auth + rest patch. Ruling 2 premises: (a), (b) and (c) all hold at cc378e93ba. Measured correction to (b)'s re-check note: the runtime auth domain does not call shouldDenyAnonymous (packages/runtime/src/domains/auth.ts names it only in a comment). The whole /auth namespace is exempt via isAuthGateAllowlisted, which is why the sweep classification edit was needed; the seat agreed it is not a fork. Route and response shape for domain:ui seat 2 are in the PR body.", "tests": "New packages/plugins/plugin-auth/src/invitation-preview.test.ts, real mount chain (AuthPlugin.registerAuthRoutes on Hono + real AuthManager on better-auth 1.7.12; invitations issued, accepted, rejected and cancelled through vendor routes): 17 own cases green (26 in the file incl. 9 re-registered from impersonation-bearer-rotation.test via its createMemoryEngine export). Ablations via scripts/ablation-replace.mjs, each anchor 1 -> 0 and restored to the HEAD blob with git diff HEAD empty: delete status !== 'pending' line -> uniform not-found leg red; unmask address -> projection + inviter-gone legs red; disable limit -> both rate-limit legs red. Final head 34e297bde, after merging origin/main and rebuilding the closure: pnpm --filter @objectstack/plugin-auth test 198 files / 3779 passed / 10 skipped; plugin-auth typecheck green incl. test layer; client ledger suites (client-url-conformance, auth-route-ledger-coverage, route-ledger-response-schema) 3 files / 10 passed. Pre-merge: rest test 323 files / 6474 passed, rest typecheck green. eslint --no-inline-config on touched files clean. Sweep classification read statically with its own classify(): preview -> door 2, control get-invitation -> outside control-plane-allowlist, class 2 expectation accepts the bare probe's 400.", "gates": "Local on head 34e297bde: dispatch-gates --commands derived 85 families; all 85 run, exit codes captured before any pipe, all exit 0; --ran: '85 derived famil(ies) accounted for — 85 run, 0 NOT-MEASURED'. (Pre-merge run: check-plugin-teardown-shape --self-test exit 3 on the shallow clone, green after git fetch origin 621a4876; check:dual-build-cjs-loads exit 3 then, exit 0 on the final head.) check-auth-mount-ledger: 22 mounts all accounted for. CI check-runs on #26151: in_progress at report time.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the anonymous door sweep (packages/qa/dogfood) NOT MEASURED locally, reason: it boots the showcase on a full build; it runs with the main battery", "mcp_calls": "0 — no MCP GitHub calls", "api_writes": "2 — relay fleet-write pr_create (POST /repos/objectstack-ai/objectstack/pulls + assignee, run 40335163273) and this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/26020/comments); git push is not a REST write", "open_questions": [], "out_of_scope_findings": [ "none filed. noted, not filed: ruling 6413975861 (b)'s re-check note says shouldDenyAnonymous answers 401 at the auth domain's entry; measured false at cc378e93ba (the /auth namespace is wholly allowlisted). The ruling's route is unchanged; carrier: this report, the seat agreed it is no fork" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
ACCEPT → PR #26151 comment 6418450300 (head
34e297bdea) ·domain:serverseat 2 ·session_0186vfjHHMsRXC9EUT9hTFMo· 2026-10-26T14:24Z. The head is green and the maintainer's approval is on it; auto-merge is on, so it lands through the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Landed
PR #26151 merged at 2026-10-26T14:54Z by the merge queue as
83a26c38d0onmain(Fixes #26020; ACCEPT6418450300; Tier H, approved by a governed approver). The merge closed this card as completed. ·domain:serverseat 2 ·os-project-manager·session_0186vfjHHMsRXC9EUT9hTFMo· 2026-10-26T14:56Zgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26151-*'git merge-base --is-ancestor 83a26c38d0 origin/mainGET /repos/objectstack-ai/objectstack/issues/26020completed.changeset/26020-signed-out-invitation-preview.md.Generated by Claude Code