Repository navigation
fix(plugin-auth,platform-objects): revoke a session by its row id, resolved server-side - #24791
Conversation
…solved server-side The sys_session Revoke Session row action now posts the row's id to a new plugin-auth door, POST /api/v1/auth/sys-session/revoke. The server resolves the session from the id, so the session token stays internal and is never sent to or from the console. The session's owner is re-dispatched to better-auth's own /revoke-session; another user's session is revocable by a platform administrator only (ADR-0068 D2 judge) and is tombstoned with reason admin (ADR-0069 D4). Every other caller reads one not-found answer. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…-action Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
…it census `node scripts/tenant-audit-census.mjs --write`, plus the page's hand-written figures moved to what the census now reports (231 -> 232 write call sites). Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 45 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 18 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 bd3de4b4ec6052f8f6e9d36f62e5575e344dfe8a && git checkout bd3de4b4ec6052f8f6e9d36f62e5575e344dfe8a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 46bb5e3e5405fad31ccba6fe571e98b9234aac78 3270bb1d81b44b8f92f82f3f78e1d45b42f3c33f && git checkout -B drift-repro 46bb5e3e5405fad31ccba6fe571e98b9234aac78 && git merge --no-ff 3270bb1d81b44b8f92f82f3f78e1d45b42f3c33f
node scripts/docs-audit/affected-docs.mjs --json 46bb5e3e5405fad31ccba6fe571e98b9234aac78
|
席位核对ACCEPT · PR #24791 at
Landing: once every check on the head is green, this seat brings Generated by Claude Code |
|
Landed ·
State: there is no card to close. This was the maintainer's direct dispatch, with no public card by their ruling. Revoke Session by row id ships in the next Generated by Claude Code |
Security fix (maintainer-ordered; no public card; detail withheld under the disclosure ruling).
Clause-②: no
The invariant
Setup → Diagnostics → Sessions (and Account → Active Sessions): a row's Revoke Session ends the session it names, and that session's next request is refused. It works for the session's owner and for a platform administrator, and for nobody else. No request to or answer from the revoke path carries a session token, and
sys_session.tokenstaysinternal: true.What changed
packages/platform-objects:sys_sessionrevoke_sessionis keyed on the row'sid, which every list row carries. It setsrecordIdParam: 'sessionId', andrecordIdFieldfalls back to its defaultid. It posts toPOST /api/v1/auth/sys-session/revoke. Thetokenfield is unchanged:internal,hidden,readonly. The console runtime is generic, so objectui needs no change.packages/plugins/plugin-auth: new ObjectStack mountPOST /api/v1/auth/sys-session/revoke. The handler issys-session-revoke.ts, the mount is inauth-plugin.ts, and the route is inauth-route-ledger.tsplus its conformance pin. The server looks up the session from the id through better-auth's own storage adapter, which hides ended sessions and re-attaches the internal column server-side. The token is used only inside the server.POST /revoke-session, with the caller's own credentials. That route's ownership and freshness checks still apply, and so do the existing admission guard and theuser_revokedtombstone, all unchanged.judgePlatformAdminadmits the caller (held sessions are refused per ADR-0069 D3). The session is then tombstoned in place withrevoke_reason: admin(ADR-0069 D4). This is the same row shapemembership-ended-session.tsandforced-reset-revocation.tswrite. The reason value comes from the interactive-revoke ledger, so the two cannot drift. better-auth's/admin/revoke-user-sessionis not used as the delegate: it authorizes on the legacyrolescalar, which ADR-0068 D2 administrators do not carry.404 RESOURCE_NOT_FOUND, byte-identical to the answer for an id that names no live session. An anonymous caller gets401 UNAUTHENTICATED, and a body withoutsessionIdgets400 INVALID_REQUEST./api/v1/auth/admin/, because its first lane admits the owner. It belongs to thesys-oauth-application/registerfamily.session-tombstone.ts: one comment sentence. Theadminreason now has a third writer.content/docs/permissions/tenant-audit-census.mdxanddocs/audits/2026-08-tenant-audit-write-call-sites.counts.mdwere regenerated withnode scripts/tenant-audit-census.mjs --write, and the page's hand-written figures were moved from 231 to 232. The administrator lane's tombstone write is a new engine write call site, andcheck-tenant-audit-censusrequires it to be counted..changeset/sys-session-revoke-by-id.md, a patch for@objectstack/plugin-authand@objectstack/platform-objects.Tests
All on this branch.
plugin-authresolves the subject through relative source imports, so its runs readsrc/.packages/plugins/plugin-auth/src/sys-session-revoke.test.ts. They run through the plugin's real route registration on a real Hono app, in front of a realAuthManager, with cookies minted by better-auth's own sign-in. Every revoke asserts both halves: the cookie stops authenticating, and the row survives with its cause. Every refusal asserts that the target still authenticates and its row is untouched, asstatuspluscode.revoke_reason: admin, and the administrator's own session is untouched);user_revoked);404 RESOURCE_NOT_FOUND, byte-identical to an id that names nothing);401 UNAUTHENTICATED);400 INVALID_REQUEST);adminvalue.packages/platform-objects/src/platform-objects.test.ts:revoke_sessionidentifies its session by the row id and postssessionIdto the new route;sys_session.tokenstaysinternal,hiddenandreadonly;pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2reported Test Files 166 passed (166) and Tests 3185 passed, 10 skipped (3195).pnpm --filter @objectstack/plugin-auth typecheckpassed: src, examples and the test layer (check:test-typecheck: OK).pnpm --filter @objectstack/platform-objects testreported Test Files 79 passed (79) and Tests 1198 passed (1198).typecheckpassed.origin/main(74f7815):pnpm install --frozen-lockfile,@objectstack/specbuild, thencheck:generated("All 14 generated artifacts are up to date"), thenplugin-authandplatform-objectstypecheck. All passed.scripts/ablation-replace.mjsin wrap mode (anchor hit 1 to 0, blob changed). Each restore was proven: blob equals HEAD, andgit diff HEADis empty. The direction was predicted before each run, and every result matched the prediction.token: 2 red, the declaration pin and the sweep.Gates
Derived by
node scripts/pm/dispatch-gates.mjs --commandsat 3270bb1 (the final head), then reconciled with--ranagainst recorded exit codes. Result: 103 derived, 100 run with exit 0, 3 not measured, 0 unrun.check-plugin-teardown-shape --self-test, reason: this shallow checkout cannot reach the fixture commit;check:dual-build-cjs-loads, reason: it needs every package'sdist/;check:i18n, reason: it needs the built CLI closure. No translatable string changed: the action's label, confirm text and success message are byte-identical.check:tenant-audit-censuswent red on the new write site. It passed after the census was regenerated (232 write call sites).test:repoand the full CI battery are left to CI.Acceptance notes
content/docs/permissions/tenant-audit-census.mdxrecords its measurement commit as the merge head the census was regenerated on. Per that page, the measurement date and sha are unenforced.Generated by Claude Code