Repository navigation
feat: account activity log and signed-in devices - #50
Merged
Merged
Conversation
This was referenced Oct 16, 2026
Merged
leviFrosty
marked this pull request as ready for review
October 16, 2026 09:36
4 tasks done
leviFrosty
force-pushed
the
account-sessions
branch
from
October 18, 2026 07:14
1b825cc to
d9a25b0
Compare
Every security-relevant account action writes an entry to a new
account_activity table (witness-work ADR 0028), in the same D1 batch as
the action where it is one: account created, session opened, signed out,
revoked from another device, replaced by the 10-session cap, ended by a
reused refresh token, handoff key sent, backup slot deleted or downloaded,
and suspension changes (status only). Entries copy the session's sealed
device label so they outlive the session row, and the hourly cron prunes
them after 400 days.
New bearer routes: GET /accounts/v1/activity (cursor pages, newest first),
GET /accounts/v1/sessions (live sessions, the caller's marked), and
POST /accounts/v1/sessions/revoke { sessionId } (POST keeps the id out of
the URL).
buddies.inbox_linked is written in linkInbox's own D1 batch, only when the link is new (an account that loses a race logs nothing), naming the bearer session that linked it. buddies.deleted is recorded at the route once the inbox DO accepted inbox/delete: the token's account and session for a bearer request, the inbox's linked account for a legacy one. Never in the DO's hot path, and writer ops log nothing, so nothing ties a writing account to another account's inbox.
leviFrosty
force-pushed
the
account-activity
branch
from
October 18, 2026 07:16
d798ec4 to
b9091ef
Compare
leviFrosty
added a commit
that referenced
this pull request
Oct 18, 2026
- feat: Supporter status on the server account, kept by RevenueCat webhooks (#51) - feat: account activity log and signed-in devices (#50) - feat: attested account sessions with bearer tokens for Buddies and Notes Import (#49) - feat: move attestation to /attestation/v1 and add readable link paths (#47) - feat: use readable payload and signature names in signed envelopes (#46) - docs: trim one-time setup steps from the agent guide (#48)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The account now has a security history the User can see in the app, along with its list of signed-in devices (witness-work ADR 0028). It records high-level actions that matter for security: which devices signed in, sessions that ended, backups deleted or downloaded, the account moved to a new phone, and suspensions. It doesn't record ordinary views.
Stacked on #49. Review that PR first; this diff is only the activity log. App PR: leviFrosty/witness-work#777.
Routes (Bearer)
GET /accounts/v1/activity?limit=&before=nextCursorGET /accounts/v1/sessionscurrentmarks the caller'sPOST /accounts/v1/sessions/revoke{ sessionId }: sign a device outRevoke is a
POST, so the session id stays out of the URL. A session that belongs to another account, or doesn't exist, is 404not_found. All three routes count against the read edge tier and work for suspended accounts.What's recorded
account.createdsession.openedsession/open)session.signed_outDELETE /sessions/currentsession.revokedsession.replacedsession.refresh_reusedhandoff.sentbackup.deletedbackup.downloadedaccount.suspended/account.reinstatedbuddies.inbox_linked/buddies.deletedEach entry is written in the same D1 batch as its action wherever the action is in D1. Backup records before the R2 change. Handoff and
buddies.deletedrecord after their Durable Object accepted the change, and a failed write is reported throughcaptureFailure.Privacy
session/open. The server can't read the label; the app decrypts the device name.migrations/accounts/0002_account-activity.sql(tableaccount_activity). The tag deploy applies it.Testing
Run on
d798ec4(rebased onto #49's1b825cc):pnpm test: 1,053 passed.pnpm exec tsc --noEmit: clean.verify-ww-apiloop:doctorok;pnpm test:e2e77 passed with 3 skipped, including the newactivity.e2e.test.ts;pnpm fuzz:buddiesok.src/test/memoryD1.ts:failQueriesnow throws synchronously fromrows(), the way a failing statement inside a real D1 batch does. Before, a failure inside a batch was silently skipped.0002applied toww-accounts-dev, and this branch deployed toww-proxy-dev. The activity, accounts, and bearer Buddies e2e suites run against the deployed worker: 24 passed, 5 skipped. The skips are the tests that need the local launcher's KV or caller IPs.Open questions
session.opened.