registry: sessions, linked identities and the account page (slice 1 of #16) #223

Closed
opened 2026-10-08 21:26:56 +00:00 by orion · 5 comments
Collaborator

Goal

The foundation every sign-in method of #16 builds on: server-side sessions, identities linked to an account, and the small account page the registry serves itself. Today accounts exist only when the operator creates them with the administrator token (POST /api/1/accounts); after this slice an account can hold several sign-in identities and a browser session, still created by the operator until slice 2 (#224) adds sign-up through a provider.

Owner decision (recorded on #16)

  • The user signs in on a small server-rendered account page served by the registry itself (a few HTML forms plus one small script for WebAuthn), under the registry's own host; no separate site or framework.
  • An identity is the provider plus the provider's user id, never the e-mail address; one account may link several identities and passkeys; the owner name is chosen at first sign-up, first come, under the existing similarity rules; removing the last sign-in method is refused.
  • No e-mail sign-up in this first cut.

Pinned records

Acceptance criteria

  • SQLite migration in tools/registry/src/database.trb: tables for identities (account, provider, subject, UNIQUE(provider, subject)), sessions (id stored as its SHA-256, account, created, expires, last seen) and CSRF secrets; existing databases migrate in place, existing accounts and tokens keep working.
  • Sessions are server-side: the cookie carries 256 bits of the system's randomness and nothing else, HttpOnly; Secure; SameSite=Lax; Path=/, with an idle and an absolute expiry; signing out deletes the row.
  • Every state-changing form or call of the account page carries a CSRF token bound to the session; a missing or wrong token is refused with 403.
  • Account page (GET /account): the account's owner name, its linked identities, its passkeys and second factors (empty lists until slices 4 and 5), its tokens; served as HTML with a strict Content-Security-Policy, no third-party assets.
  • Unlinking an identity is refused when it is the account's last sign-in method.
  • A test seam for slices 2 and 4: a session can be created for an account in tests/ without a provider (test-only route or helper, never reachable in a production configuration).
  • tests/server.test.trb (or a new test file) covers each call once as it works and once as it is refused (no session, expired session, wrong CSRF token, last identity); torb test tools/registry/tests green natively and in the VM.
  • tools/registry/README.md (calls table, configuration, "Not built yet") and RELEASE.md's Accounts row updated.

Seed impact

None: TorbScript in tools/registry on existing std; no syntax change, no native, no driver flag, no std name the compiler looks up by string.

Size and dependencies

One PR, one or two Conventional Commits (feat(registry): ...). No dependency. Parent: #16. Security-relevant: architect review of the diff before the PR.

## Goal The foundation every sign-in method of #16 builds on: server-side sessions, identities linked to an account, and the small account page the registry serves itself. Today accounts exist only when the operator creates them with the administrator token (`POST /api/1/accounts`); after this slice an account can hold several sign-in identities and a browser session, still created by the operator until slice 2 (#224) adds sign-up through a provider. ## Owner decision (recorded on #16) - The user signs in on a small server-rendered account page served by the registry itself (a few HTML forms plus one small script for WebAuthn), under the registry's own host; no separate site or framework. - An identity is the provider plus the provider's user id, never the e-mail address; one account may link several identities and passkeys; the owner name is chosen at first sign-up, first come, under the existing similarity rules; removing the last sign-in method is refused. - No e-mail sign-up in this first cut. ## Pinned records - [RELEASE.md 7.1 Owners](https://git.torb.dev/torbscript/language/src/commit/45be87a8bdba3d0a00212c7a82fa8e451bef398c/docs/design/RELEASE.md#L866) - [RELEASE.md 7.6 Security](https://git.torb.dev/torbscript/language/src/commit/45be87a8bdba3d0a00212c7a82fa8e451bef398c/docs/design/RELEASE.md#L1029) - [RELEASE.md, the write service as built (Accounts row)](https://git.torb.dev/torbscript/language/src/commit/45be87a8bdba3d0a00212c7a82fa8e451bef398c/docs/design/RELEASE.md#L1282) - `tools/registry/README.md`, "Not built yet" ## Acceptance criteria - [ ] SQLite migration in `tools/registry/src/database.trb`: tables for identities (`account`, `provider`, `subject`, `UNIQUE(provider, subject)`), sessions (id stored as its SHA-256, account, created, expires, last seen) and CSRF secrets; existing databases migrate in place, existing accounts and tokens keep working. - [ ] Sessions are server-side: the cookie carries 256 bits of the system's randomness and nothing else, `HttpOnly; Secure; SameSite=Lax; Path=/`, with an idle and an absolute expiry; signing out deletes the row. - [ ] Every state-changing form or call of the account page carries a CSRF token bound to the session; a missing or wrong token is refused with 403. - [ ] Account page (`GET /account`): the account's owner name, its linked identities, its passkeys and second factors (empty lists until slices 4 and 5), its tokens; served as HTML with a strict `Content-Security-Policy`, no third-party assets. - [ ] Unlinking an identity is refused when it is the account's last sign-in method. - [ ] A test seam for slices 2 and 4: a session can be created for an account in `tests/` without a provider (test-only route or helper, never reachable in a production configuration). - [ ] `tests/server.test.trb` (or a new test file) covers each call once as it works and once as it is refused (no session, expired session, wrong CSRF token, last identity); `torb test tools/registry/tests` green natively and in the VM. - [ ] `tools/registry/README.md` (calls table, configuration, "Not built yet") and RELEASE.md's Accounts row updated. ## Seed impact None: TorbScript in `tools/registry` on existing `std`; no syntax change, no native, no driver flag, no std name the compiler looks up by string. ## Size and dependencies One PR, one or two Conventional Commits (`feat(registry): ...`). No dependency. Parent: #16. Security-relevant: architect review of the diff before the PR.
Author
Collaborator

[loop:torbscript] implement: ready, but it edits tools/registry while PR #237 (#17) is in CI and review on the same paths. next: waiting (PR #237 on the same paths) until 2026-10-09T05:10:00Z

[loop:torbscript] implement: ready, but it edits `tools/registry` while PR #237 (#17) is in CI and review on the same paths. next: waiting (PR #237 on the same paths) until 2026-10-09T05:10:00Z
Author
Collaborator

[loop:torbscript] implement: ready, but PR #245 (#18, registry search) is in CI and review and edits tools/registry/src/database.trb and the other registry sources this slice migrates; PR #237 has landed. next: waiting (PR #245 on the same paths)

[loop:torbscript] implement: ready, but PR #245 (#18, registry search) is in CI and review and edits `tools/registry/src/database.trb` and the other registry sources this slice migrates; PR #237 has landed. next: waiting (PR #245 on the same paths)
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T10:20Z

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T10:20Z
Author
Collaborator

[loop:torbscript] implement: PR #249 (#249) implements all acceptance criteria in one Conventional Commit:

  • migration of identities, sessions and CSRF secrets in place;
  • server-side sessions with idle and absolute expiry, and a __Host- cookie carrying only 256 random bits;
  • CSRF bound to the session, with 403 on a missing or wrong token;
  • GET /account with a strict CSP;
  • unlinking the last sign-in method refused with 409;
  • a test seam with no route;
  • tools/registry/tests/account.test.trb, 21 tests covering every refusal path;
  • README and RELEASE.md updated.

An architect security review ran before the PR. It found nothing blocking, and its hardening points are applied: unlinking an identity ends the account's other sessions, Sec-Fetch-Site cross-site/same-site posts are refused, the check and delete of an unlink are in one transaction, and the Traefik route is narrower.

CI is green on c95f86be: gates, tier A/B, tools/registry, pdisc, commit check. Tier A needed one rerun for the known language-server flake #244.

Left for slice 2, listed in the PR:

  • start sessions from a verified identity rather than an account name;
  • a check that no route calls startSession/linkIdentity.

Token usage: ~0.5M.

next: review (waiting on the merge gate argus)

[loop:torbscript] implement: PR #249 (https://git.torb.dev/torbscript/language/pulls/249) implements all acceptance criteria in one Conventional Commit: - migration of identities, sessions and CSRF secrets in place; - server-side sessions with idle and absolute expiry, and a `__Host-` cookie carrying only 256 random bits; - CSRF bound to the session, with 403 on a missing or wrong token; - `GET /account` with a strict CSP; - unlinking the last sign-in method refused with 409; - a test seam with no route; - `tools/registry/tests/account.test.trb`, 21 tests covering every refusal path; - README and RELEASE.md updated. An architect security review ran before the PR. It found nothing blocking, and its hardening points are applied: unlinking an identity ends the account's other sessions, `Sec-Fetch-Site` cross-site/same-site posts are refused, the check and delete of an unlink are in one transaction, and the Traefik route is narrower. CI is green on c95f86be: gates, tier A/B, tools/registry, pdisc, commit check. Tier A needed one rerun for the known language-server flake #244. Left for slice 2, listed in the PR: - start sessions from a verified identity rather than an account name; - a check that no route calls `startSession`/`linkIdentity`. Token usage: ~0.5M. next: review (waiting on the merge gate argus)
argus closed this issue 2026-10-09 06:25:12 +00:00
Author
Collaborator

[loop:torbscript] close: PR #249 approved and merged by argus at 2026-10-09T06:25Z as 13d7e718 (one commit, feat(registry): sessions, linked identities and the account page). PR CI green on c95f86be after the pdisc restamp fix. Main CI on the landed commit is running at the time of this comment. Worker usage: not reported (the worker's run ended with an oversized output after the PR was approved). Slice 1 of #16 done; claim released.

[loop:torbscript] close: PR https://git.torb.dev/torbscript/language/pulls/249 approved and merged by argus at 2026-10-09T06:25Z as 13d7e718 (one commit, `feat(registry): sessions, linked identities and the account page`). PR CI green on c95f86be after the pdisc restamp fix. Main CI on the landed commit is running at the time of this comment. Worker usage: not reported (the worker's run ended with an oversized output after the PR was approved). Slice 1 of #16 done; claim released.
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
torbscript/language#223
No description provided.