Uh oh!
There was an error while loading. Please reload this page.
feat(service-messaging): plugin-facing listInboxAsCaller scoped to the authenticated caller - #11536
Conversation
…e authenticated caller The read-side sibling of the #10753 write door: listInbox(userId, opts) is the REST door's contract method, but for an in-process caller its userId is a free string. listInboxAsCaller derives the recipient from the caller's ExecutionContext.userId via the shared resolveInboxRecipient, refusing with InboxCallerError (401 UNAUTHENTICATED) before listInbox's empty-envelope short-circuit. resolveInboxRecipient gains an optional parameter naming the target-user door its refusal prescribes (default: the write door's existing text, byte-unchanged). Part of #11452 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6deb0214229aecb3879e59f44d2fdfec77c507a8 && git checkout 6deb0214229aecb3879e59f44d2fdfec77c507a8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c96cbfc48dd5c46861134832e1d0cbc622205b13 90b3528079d2fa5a1e94b220282616e807effc5e && git checkout -B drift-repro c96cbfc48dd5c46861134832e1d0cbc622205b13 && git merge --no-ff 90b3528079d2fa5a1e94b220282616e807effc5e
node scripts/docs-audit/affected-docs.mjs --json c96cbfc48dd5c46861134832e1d0cbc622205b13 |
os-sam
commented
Aug 24, 2026
Seat review ( |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11452
What
MessagingServicegains the plugin-facing inbox read door scoped to the authenticated caller —listInboxAsCaller(caller, opts)— mirroring the merged write-side counterpart PR #11450 (card #10753):caller.userIdvia the sameresolveInboxRecipient(packages/services/service-messaging/src/inbox-caller.ts) the write door uses — one refusal vocabulary, not two.InboxCallerErrorcarrying the ADR-0112 envelope pair a boundary reads —status: 401, registeredcode: 'UNAUTHENTICATED'.attributedUserId(attribution only, ADR-0118 D2),actor(a service-principal label) andisSystemare refused rather than promoted.listInbox's no-data-engine / no-user short-circuit, which answers a well-formed empty{ notifications: [], unreadCount: 0 }— the read-side analog of the silent success the write door replaced.resolveInboxRecipientgains an optional third parameter naming the target-user door its refusal prescribes (listInbox(userId, opts)for this verb). It defaults to the write door's existing text, so the Console 顶栏铃铛未读角标在审批处理完成后不更新 #10753 call sites keep their refusal bytes unchanged.Additive only:
listInbox(userId, opts)stays byte-for-byte as the publishedINotificationServicecontract the REST door (runtime/src/domains/notifications.ts) binds to. Nopackages/spec/**edits were needed — the new method lives on the service class, exactly as the write door does.What it is, honestly
Same caveat as the write door, deliberately carried forward: this is a discipline boundary, not a security boundary — an in-process plugin already holds the data engine and can read
sys_inbox_messagerows directly; nothing at this layer stops trusted code that means to. What changes is that the correct pattern is the only one the plugin-facing surface expresses, and the incorrect one now fails loudly at the call site.Tests
New
#11452block inmessaging-service.test.ts(7 tests): own-inbox-only scoping (the other user's rendered content absent from the answer in any field), options (read/type/limit) forwarded unchanged (answer-equality againstlistInboxon the same engine), and the refusal matrix — absent/empty/blank context,attributedUserId-only,actorlabel,isSystem, refusal-before-short-circuit, and the refusal prescribing the read contract door rather thanmarkRead(userId, ids).Ablation (fix reverted to a naive
caller?.userId ?? ''delegation, mutation confirmed on disk by anchored grep in both directions): exactly the 5 refusal tests go red, every failure being the predictedexpected InboxCallerError, but the call resolved; restore verified byte-identical viagit hash-object. The suite resolves the subject through relative src imports, so no dist rebuild leg applies.Verification (all at
90b3528079, this PR's head)@objectstack/service-messaging:pnpm test—Test Files 26 passed (26) / Tests 274 passed (274);pnpm typecheckclean.node scripts/pm/dispatch-gates.mjs, derivation stamped at90b3528079):check:changeset-gate-self-tests,check:objectui-changeset,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check-adr-0087-registration,check-changeset-no-major,check-ci-filter-parity,check-empty-changeset,check-plugin-teardown-shape,docs-audit/check-affected-docs,check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:cross-package-test-inputs,check:where-matcher— all exit 0 (exits captured before any pipe).check:type-check-debt(full closure built first):check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 293.3s, 1897 raw tsc error(s) total, none above its recorded number.check:i18n:check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).check:nul-bytes:OK (scanned 6456 text file(s) …).eslint --no-inline-configon the three touched TS files: exit 0 (early signal only; the repo-wide sweep is CI's run).Changeset:
.changeset/messaging-inbox-read-authenticated-caller.md(minor,@objectstack/service-messaging; additive, not breaking, so no ADR-0087 disposition marker is required).Generated by Claude Code