Skip to content

feat: validate and revoke interactive sessions at state-changing acti… - #791

Open
Amksongs wants to merge 2 commits into
Anchor-kit:mainfrom
Amksongs:feat/718-interactive-session-revocation
Open

Amksongs wants to merge 2 commits into
Anchor-kit:mainfrom
Amksongs:feat/718-interactive-session-revocation

Conversation

@Amksongs

Copy link
Copy Markdown

What I changedsrc/runtime/interfaces.ts — added InteractiveSessionRecord (id, account, transactionId, permitted actions, consumed consumedActions, expiresAt, revokedAt), the InteractiveSessionRejection union, and four DatabaseAdapter methods: insertInteractiveSession, getInteractiveSessionById, revokeInteractiveSession, consumeInteractiveSessionAction.src/runtime/database/sql-database-adapter.ts — persisted interactive_sessions for SQLite and PostgreSQL with lookup indexes; revoke is a guarded UPDATE ... WHERE revoked_at IS NULL; consumption is a bounded compare-and-set on the stored consumed_actions text, re-reading on a lost race, so a given action can only ever be appended once.src/runtime/http/interactive-session-gate.ts (new) — the single shared validator. It reads the X-Interactive-Session header, loads the session, and checks identity binding first (account, then transaction), then revocation, then expiry, then the action set — so a caller who doesn't own a session can't learn whether it was revoked or expired. It also maps each rejection to a status: 401 for missing/unknown/expired/revoked, 403 for wrong-account/wrong-transaction/unpermitted action, 409 for an already-consumed action.src/runtime/http/express-router-impl.ts — new state-changing POST /transactions/:id/complete runs the gate before the transaction is even read, then spends the complete action exactly once. GET /transactions/:id uses the same gate when a session is presented, while plain bearer reads keep working. Transaction-id parsing was extracted to one shared helper, and the new header was added to both CORS allow-lists.Focused check — tests/runtime/interactive-session-revocation.test.ts, 8 assertions driving the real HTTP router over a real SQLite adapter. It wraps the write helper with a counter to prove rejected sessions never invoke it and that no row changes, asserts exactly-one consumption under both a replay and a five-way concurrent race, and asserts the read endpoint applies the identical gate.Acceptance criteriaCriterion StatusExpired, revoked, wrong-account, wrong-transaction sessions fail before writes ✅ Verified by counter + before/after row snapshotsCompletion consumes the permitted action once ✅ Replays and 5 concurrent callers yield exactly one 200Reproducible focused check with command and expected result documented ✅ Test header, docs/acceptance-checks.md, and PR_DESCRIPTION.mdVerificationbun test — 926 pass, 0 fail across 73 files (also run automatically by the pre-commit hook)bun run typecheck (tsc --noEmit + eslint . --max-warnings 0) — passbun run format:check, bun run lint, bun run build, scripts/check-fallow-health.mjs — all pass

closes #718

Amksongs and others added 2 commits October 23, 2026 09:27
…ons (Anchor-kit#718)

A hosted interactive URL only carries a transaction id, so it proves nothing on
its own. Persist interactive session identity and make every interactive
endpoint consult one shared gate: sessions are bound to a single account and
transaction, carry their own expiry and revocation, and spend a permitted action
exactly once via compare-and-set.

Expired, revoked, wrong-account and wrong-transaction sessions are now rejected
before any write, and the new POST /transactions/:id/complete consumes the
permitted action once so a completion cannot be replayed.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Validate and revoke interactive sessions at each state-changing action

1 participant