@fuzdev/fuz_app 0.53.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/actions/CLAUDE.md +68 -13
  2. package/dist/actions/action_codegen.d.ts +13 -0
  3. package/dist/actions/action_codegen.d.ts.map +1 -1
  4. package/dist/actions/action_codegen.js +15 -1
  5. package/dist/actions/action_rpc.d.ts +60 -7
  6. package/dist/actions/action_rpc.d.ts.map +1 -1
  7. package/dist/actions/action_rpc.js +158 -44
  8. package/dist/actions/register_action_ws.d.ts +4 -4
  9. package/dist/actions/register_action_ws.js +6 -6
  10. package/dist/actions/register_ws_endpoint.d.ts +20 -7
  11. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  12. package/dist/actions/register_ws_endpoint.js +30 -5
  13. package/dist/actions/transports.d.ts.map +1 -1
  14. package/dist/actions/transports.js +0 -4
  15. package/dist/auth/CLAUDE.md +230 -63
  16. package/dist/auth/account_actions.d.ts +6 -6
  17. package/dist/auth/account_actions.d.ts.map +1 -1
  18. package/dist/auth/account_actions.js +8 -11
  19. package/dist/auth/account_queries.d.ts +6 -3
  20. package/dist/auth/account_queries.d.ts.map +1 -1
  21. package/dist/auth/account_queries.js +14 -5
  22. package/dist/auth/account_routes.d.ts +7 -10
  23. package/dist/auth/account_routes.d.ts.map +1 -1
  24. package/dist/auth/account_routes.js +70 -23
  25. package/dist/auth/account_schema.d.ts +19 -0
  26. package/dist/auth/account_schema.d.ts.map +1 -1
  27. package/dist/auth/account_schema.js +20 -0
  28. package/dist/auth/admin_action_specs.d.ts +45 -11
  29. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  30. package/dist/auth/admin_action_specs.js +23 -8
  31. package/dist/auth/admin_actions.d.ts +8 -7
  32. package/dist/auth/admin_actions.d.ts.map +1 -1
  33. package/dist/auth/admin_actions.js +11 -18
  34. package/dist/auth/audit_log_queries.d.ts +53 -14
  35. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  36. package/dist/auth/audit_log_queries.js +45 -2
  37. package/dist/auth/audit_log_schema.d.ts +55 -1
  38. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  39. package/dist/auth/audit_log_schema.js +19 -3
  40. package/dist/auth/bearer_auth.d.ts +9 -7
  41. package/dist/auth/bearer_auth.d.ts.map +1 -1
  42. package/dist/auth/bearer_auth.js +13 -21
  43. package/dist/auth/cleanup.d.ts.map +1 -1
  44. package/dist/auth/cleanup.js +5 -0
  45. package/dist/auth/daemon_token_middleware.d.ts +23 -11
  46. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  47. package/dist/auth/daemon_token_middleware.js +26 -20
  48. package/dist/auth/deps.d.ts +14 -0
  49. package/dist/auth/deps.d.ts.map +1 -1
  50. package/dist/auth/middleware.d.ts.map +1 -1
  51. package/dist/auth/middleware.js +4 -2
  52. package/dist/auth/migrations.d.ts +15 -7
  53. package/dist/auth/migrations.d.ts.map +1 -1
  54. package/dist/auth/migrations.js +15 -7
  55. package/dist/auth/permit_offer_action_specs.d.ts +45 -6
  56. package/dist/auth/permit_offer_action_specs.d.ts.map +1 -1
  57. package/dist/auth/permit_offer_action_specs.js +38 -7
  58. package/dist/auth/permit_offer_actions.d.ts +2 -2
  59. package/dist/auth/permit_offer_actions.d.ts.map +1 -1
  60. package/dist/auth/permit_offer_actions.js +106 -95
  61. package/dist/auth/permit_offer_notifications.d.ts +10 -0
  62. package/dist/auth/permit_offer_notifications.d.ts.map +1 -1
  63. package/dist/auth/permit_offer_queries.d.ts +68 -9
  64. package/dist/auth/permit_offer_queries.d.ts.map +1 -1
  65. package/dist/auth/permit_offer_queries.js +147 -35
  66. package/dist/auth/permit_offer_schema.d.ts +23 -1
  67. package/dist/auth/permit_offer_schema.d.ts.map +1 -1
  68. package/dist/auth/permit_offer_schema.js +5 -0
  69. package/dist/auth/permit_queries.d.ts +17 -5
  70. package/dist/auth/permit_queries.d.ts.map +1 -1
  71. package/dist/auth/permit_queries.js +19 -8
  72. package/dist/auth/request_context.d.ts +360 -32
  73. package/dist/auth/request_context.d.ts.map +1 -1
  74. package/dist/auth/request_context.js +442 -60
  75. package/dist/auth/route_guards.d.ts +10 -4
  76. package/dist/auth/route_guards.d.ts.map +1 -1
  77. package/dist/auth/route_guards.js +14 -8
  78. package/dist/auth/self_service_role_action_specs.d.ts +2 -0
  79. package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
  80. package/dist/auth/self_service_role_action_specs.js +2 -0
  81. package/dist/auth/self_service_role_actions.d.ts +6 -5
  82. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  83. package/dist/auth/self_service_role_actions.js +32 -19
  84. package/dist/db/migrate.d.ts +11 -7
  85. package/dist/db/migrate.d.ts.map +1 -1
  86. package/dist/db/migrate.js +9 -6
  87. package/dist/dev/setup.d.ts.map +1 -1
  88. package/dist/dev/setup.js +5 -3
  89. package/dist/hono_context.d.ts +77 -0
  90. package/dist/hono_context.d.ts.map +1 -1
  91. package/dist/hono_context.js +50 -0
  92. package/dist/http/CLAUDE.md +80 -17
  93. package/dist/http/error_schemas.d.ts +92 -1
  94. package/dist/http/error_schemas.d.ts.map +1 -1
  95. package/dist/http/error_schemas.js +73 -16
  96. package/dist/http/jsonrpc_errors.d.ts +27 -2
  97. package/dist/http/jsonrpc_errors.d.ts.map +1 -1
  98. package/dist/http/jsonrpc_errors.js +26 -2
  99. package/dist/http/route_spec.d.ts +62 -4
  100. package/dist/http/route_spec.d.ts.map +1 -1
  101. package/dist/http/route_spec.js +117 -21
  102. package/dist/http/schema_helpers.d.ts +13 -1
  103. package/dist/http/schema_helpers.d.ts.map +1 -1
  104. package/dist/http/schema_helpers.js +21 -2
  105. package/dist/http/surface.d.ts +10 -1
  106. package/dist/http/surface.d.ts.map +1 -1
  107. package/dist/http/surface.js +2 -2
  108. package/dist/server/app_server.d.ts.map +1 -1
  109. package/dist/server/app_server.js +11 -1
  110. package/dist/testing/CLAUDE.md +23 -17
  111. package/dist/testing/admin_integration.d.ts.map +1 -1
  112. package/dist/testing/admin_integration.js +15 -13
  113. package/dist/testing/adversarial_headers.js +1 -1
  114. package/dist/testing/app_server.js +2 -2
  115. package/dist/testing/audit_completeness.d.ts.map +1 -1
  116. package/dist/testing/audit_completeness.js +21 -7
  117. package/dist/testing/auth_apps.d.ts.map +1 -1
  118. package/dist/testing/auth_apps.js +6 -3
  119. package/dist/testing/entities.d.ts +2 -1
  120. package/dist/testing/entities.d.ts.map +1 -1
  121. package/dist/testing/entities.js +1 -0
  122. package/dist/testing/integration_helpers.d.ts +4 -2
  123. package/dist/testing/integration_helpers.d.ts.map +1 -1
  124. package/dist/testing/integration_helpers.js +9 -5
  125. package/dist/testing/middleware.d.ts +12 -8
  126. package/dist/testing/middleware.d.ts.map +1 -1
  127. package/dist/testing/middleware.js +67 -25
  128. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  129. package/dist/testing/rpc_helpers.js +3 -1
  130. package/dist/testing/schema_generators.d.ts.map +1 -1
  131. package/dist/testing/schema_generators.js +12 -0
  132. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  133. package/dist/testing/ws_round_trip.js +5 -1
  134. package/dist/ui/CLAUDE.md +16 -10
  135. package/dist/ui/PermitOfferForm.svelte +14 -0
  136. package/dist/ui/PermitOfferForm.svelte.d.ts +6 -0
  137. package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -1
  138. package/dist/ui/admin_accounts_state.svelte.d.ts +8 -1
  139. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  140. package/dist/ui/admin_accounts_state.svelte.js +14 -3
  141. package/dist/ui/permit_offers_state.svelte.d.ts +9 -1
  142. package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -1
  143. package/dist/ui/permit_offers_state.svelte.js +7 -1
  144. package/package.json +1 -1
@@ -84,8 +84,10 @@ Design notes:
84
84
  ### Identity entities (`account_schema.ts`)
85
85
 
86
86
  - `Account` (primary identity, holds `password_hash`), `Actor` (the entity
87
- that acts — owns cells, holds permits, appears in audit trails; 1:1 with
88
- account in v1), `Permit` (time-bounded, revocable grant of a role to an
87
+ that acts — owns cells, holds permits, appears in audit trails; an account
88
+ may host one or more actors, with the dispatcher's authorization phase
89
+ resolving the acting actor per-request via `acting?: ActingActor` on
90
+ inputs), `Permit` (time-bounded, revocable grant of a role to an
89
91
  actor — carries `scope_id`, `source_offer_id`, `revoked_reason`),
90
92
  `AuthSession` (server-side, keyed by blake3), `ApiToken`.
91
93
  - Every `id` / `*_id` field on entity interfaces, `*Json` schemas, and
@@ -209,6 +211,50 @@ Zod enum; `AuditOutcome` is `'success' | 'failure'`.
209
211
  `AuditLogListOptions` (supports `since_seq` for SSE reconnection gap fill);
210
212
  `AUDIT_LOG_DEFAULT_LIMIT = 50` (default page size, lives on the schema
211
213
  side so client codegen can import it without dragging in the query layer).
214
+ `target_actor_id` lives parallel to `target_account_id` on both row
215
+ and input. **Rule** — `target_actor_id` is populated when the event
216
+ subject is bound to a specific actor. Concretely: `permit_revoke`
217
+ and `permit_grant` (admin direct-grant, self-service toggle, and
218
+ in-tx accept all populate both target columns — the grantee is the
219
+ subject regardless of initiator), in-tx `permit_offer_accept` on
220
+ accept, and `permit_offer_decline` always populate both target
221
+ columns (decline joins `from_account_id` into the RETURNING so the
222
+ "both populated → same account" invariant holds uniformly).
223
+ Offer-shape events (`permit_offer_create`, `_expire`, `_retract`,
224
+ `_supersede`) populate `target_actor_id` when the offer was
225
+ actor-targeted at create time (`permit_offer.to_actor_id` set),
226
+ null when the offer was account-grain (any actor on
227
+ `to_account_id` may accept). Account-shape events (login, logout,
228
+ signup, bootstrap, password change, session/token revoke,
229
+ app_settings update, invite events) stay account-grain on both
230
+ `target_actor_id` **and** `actor_id` — the operation is performed
231
+ by the account, and a multi-actor user must be able to log out
232
+ (or change password, or revoke sessions) without first picking an
233
+ acting actor. Permit/admin/offer events keep recording the
234
+ initiator's actor in `actor_id`.
235
+ SSE/WS socket-close keys on `target_account_id ?? account_id`
236
+ (sessions stay account-grain at the routing layer even though
237
+ they bind to a specific actor at request-context resolution time —
238
+ see request_context.ts).
239
+ - **Actor-targetable offers** — `permit_offer.to_actor_id` is the
240
+ optional column that flips an offer from account-grain (null,
241
+ default) to actor-grain (non-null). `query_permit_offer_create`
242
+ validates the actor↔account binding in one SELECT and rejects with
243
+ `PermitOfferActorAccountMismatchError` when the supplied actor isn't
244
+ on `to_account_id`. `query_accept_offer` rejects wrong-actor accepts
245
+ on actor-targeted offers with `PermitOfferActorMismatchError` —
246
+ surfaced to RPC callers as `permit_offer_actor_mismatch`. Closes the
247
+ audit hole where offer-shape events left `target_actor_id` null even
248
+ when the recipient binding was known at offer time.
249
+ - **`emit_permit_target_event` helper** — the canonical entry point
250
+ for permit-shape audit emissions. Takes `(ctx, auth, deps, {event_type,
251
+ target_account_id, target_actor_id, metadata, outcome?})` and lifts
252
+ the `actor_id` / `account_id` / `ip` boilerplate that every
253
+ `permit_*` audit emit site repeats. Use this instead of
254
+ `audit_log_fire_and_forget` for any event populating one of the
255
+ `target_*_id` columns; reach for the lower-level helper only when
256
+ the event is non-permit-shape (e.g., `app_settings_update`,
257
+ bootstrap, signup).
212
258
  - Client-safe: `AuditLogEventJson`, `AuditLogEventWithUsernamesJson`,
213
259
  `PermitHistoryEventJson`, `AdminSessionJson`.
214
260
  - `get_audit_metadata(event)` type-narrows after checking `event_type`.
@@ -334,7 +380,12 @@ CRUD + listing:
334
380
  - `query_update_account_password`, `query_delete_account` (cascades to
335
381
  actors, permits, sessions, tokens).
336
382
  - `query_account_has_any` — used by bootstrap for belt-and-suspenders check.
337
- - `query_actor_by_account`, `query_actor_by_id`.
383
+ - `query_actors_by_account` — list every actor on an account, ordered
384
+ by `created_at`. Used by `resolve_acting_actor` to pick the unique
385
+ actor on single-actor accounts or surface `actor_required` when the
386
+ account has multiple actors.
387
+ - `query_actor_by_id` — direct lookup by id; preferred when the caller
388
+ already has an actor id in scope.
338
389
  - `query_admin_account_list` — composes accounts + actors + active permits +
339
390
  pending inbound offers with **four flat queries** instead of N+1. Pending
340
391
  offers exclude `message` on purpose (cross-admin visibility). Returns
@@ -347,8 +398,14 @@ CRUD + listing:
347
398
  uses `IS NOT DISTINCT FROM` (plain `=` would miss the NULL-scope conflict
348
399
  case).
349
400
  - `query_permit_find_active_role_for_actor(deps, permit_id, actor_id)` —
350
- actor-scoped read, so IDOR protection is consistent with revoke. Returns
351
- `{role}` or `null`.
401
+ actor-scoped read, so IDOR protection is consistent with revoke.
402
+ Returns `{role, account_id}` (the actor's `account_id` joined in) or
403
+ `null`. The `account_id` flows into the audit envelope's
404
+ `target_account_id` and the SSE/WS socket-close fan-out target —
405
+ collapsing what used to be a second `query_actor_by_id` round-trip in
406
+ the revoke handler into one read closes the small TOCTOU window
407
+ where the actor row could be deleted between the IDOR check and the
408
+ actor lookup.
352
409
  - **`query_revoke_permit(deps, permit_id, actor_id, revoked_by, reason?)`** —
353
410
  actor-scoped IDOR guard (returns `null` if the permit belongs to a
354
411
  different actor). Supersedes pending offers for the revoked permit's
@@ -360,7 +417,10 @@ CRUD + listing:
360
417
  - `query_permit_find_active_for_actor`, `query_permit_list_for_actor`.
361
418
  - `query_permit_has_role(deps, actor_id, role, scope_id?)` — `IS NOT DISTINCT FROM`
362
419
  handles the NULL case. Omitted scope matches `scope_id IS NULL` (pre-scope
363
- callers keep semantics).
420
+ callers keep semantics). Use only when checking an arbitrary `actor_id`
421
+ that isn't the request actor (e.g., post-mutation verification, scripts,
422
+ audit-time checks). For the request actor, prefer `has_scoped_role` /
423
+ `has_any_scoped_role` on the in-memory `auth.permits` snapshot.
364
424
  - `query_permit_find_account_id_for_role(deps, role)` — joins
365
425
  permit → actor → account, returns first match. Used by daemon token
366
426
  middleware to resolve the keeper account.
@@ -372,8 +432,9 @@ CRUD + listing:
372
432
  active permit at `scope_id` (role-agnostic) and supersedes every pending
373
433
  offer at `scope_id` (tuple-matched and orphan, undifferentiated) in the
374
434
  caller's transaction. Returns `RevokeForScopeResult = {revoked, superseded_offers}`
375
- — `revoked` carries `account_id` for `permit_revoke` fan-out;
376
- `superseded_offers` carries `from_account_id`. Caller emits
435
+ — `revoked` carries both `actor_id` (drives `target_actor_id` audit
436
+ envelopes) and `account_id` (drives `target_account_id` for socket-close
437
+ fan-out); `superseded_offers` carries `from_account_id`. Caller emits
377
438
  `permit_offer_supersede` audits with `reason: 'scope_destroyed'` and
378
439
  `cause_id: <destroyed scope row id>` per superseded offer (the cause is
379
440
  the scope deletion, not any individual permit revoke). Use from a
@@ -386,9 +447,12 @@ CRUD + listing:
386
447
  Error classes (all extend `Error` with stable `.name` — never use
387
448
  `instanceof` against plain messages):
388
449
 
389
- - `PermitOfferSelfTargetError` — grantor offered themselves. Enforced via
390
- cross-row JOIN in `query_permit_offer_create` (rather than CHECK) to avoid
391
- denormalized columns.
450
+ - `PermitOfferSelfTargetError` — grantor offered themselves. Enforced
451
+ via a single SELECT on the grantor's `actor.account_id` in
452
+ `query_permit_offer_create` (resolving from the grantor side keeps
453
+ the check multi-actor-correct — the grantor → account binding stays
454
+ 1:1 by definition of `actor`, while the recipient account may host
455
+ many actors under multi-actor).
392
456
  - `PermitOfferAlreadyTerminalError` — offer exists for the caller but is
393
457
  accepted / declined / retracted / superseded.
394
458
  - `PermitOfferExpiredError` — pending but past `expires_at` (distinct from
@@ -512,19 +576,24 @@ run'` if the seed somehow missed (defensive — migrations always seed).
512
576
  - `query_audit_log_list(deps, options?)` — supports `event_type`,
513
577
  `event_type_in`, `account_id` (matches `account_id` OR
514
578
  `target_account_id`), `outcome`, `since_seq`, `limit`, `offset`.
515
- - `query_audit_log_list_with_usernames` joins twice to `account`.
579
+ `target_actor_id` filtering is not yet exposed; will land alongside
580
+ the admin-viewer's actor-grain forensics pass.
581
+ - `query_audit_log_list_with_usernames` — joins twice to `account`
582
+ (chains `target_account_id` for the `target_username` field).
583
+ `target_actor_id` is on the row but not currently joined to actor
584
+ for a name; the admin viewer will resolve via `actor_lookup` /
585
+ `actor.name` when the actor-grain forensics pass lands.
516
586
  - `query_audit_log_list_for_account`, `query_audit_log_list_permit_history`
517
587
  (filters to `permit_grant` / `permit_revoke`).
518
588
  - `query_audit_log_cleanup_before`.
519
589
  - **`audit_log_fire_and_forget(route, input, deps)`** —
520
590
  writes to `route.background_db` (pool-level), so audit entries persist
521
- even when the request transaction rolls back. `deps` is an
522
- `AuditLogFireAndForgetDeps` bundle (`{log, on_audit_event, audit_log_config?}`)
523
- structurally compatible with `Pick<AppDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`,
524
- so call sites pass the surrounding deps object directly. Bundling
525
- replaces the prior 5-arg positional signature; consumers that forgot
526
- the trailing `config` would silently fall back to
527
- `BUILTIN_AUDIT_LOG_CONFIG`. Write and `on_audit_event` callback
591
+ even when the request transaction rolls back. `deps` is the shared
592
+ `AuditEmitDeps` bundle (`{log, on_audit_event, audit_log_config?}`)
593
+ from `auth/deps.ts`, so call sites pass the surrounding deps object
594
+ directly. Bundling replaces the prior 5-arg positional signature;
595
+ consumers that forgot the trailing `config` would silently fall back
596
+ to `BUILTIN_AUDIT_LOG_CONFIG`. Write and `on_audit_event` callback
528
597
  failures are logged separately. Pushes onto `route.pending_effects`
529
598
  for test flushing.
530
599
 
@@ -563,11 +632,14 @@ by `sequence`, then enforces:
563
632
  3. **Run the pending tail** (`code[applied.length..]`) inside a single
564
633
  chain transaction; each `INSERT` uses `sequence = max(sequence) + 1`.
565
634
 
566
- **Append-only after first publish.** Once a fuz_app version containing a
567
- migration is published, the migration's name and position are frozen.
568
- Pre-publish, anything goes; the cliff is the publish event. Body edits to
569
- a published migration slip past the runner (no content hashing) — schema-
570
- snapshot tests in consumers catch these.
635
+ **Schema is not stabilized yet append-only is NOT the rule today.**
636
+ While fuz_app is pre-stable, migration bodies, names, and positions can
637
+ change freely between versions and consumers upgrading across a schema
638
+ change are expected to drop and re-bootstrap their dev/test databases.
639
+ Once the schema is declared stable, a hard append-only-after-publish rule
640
+ will apply (with the cliff called out in that release's notes). Until
641
+ then bias toward editing the existing migration entries rather than
642
+ appending patch migrations.
571
643
 
572
644
  `MigrationError` is the only error class thrown from `run_migrations` /
573
645
  `baseline`; branch on `.kind` (never on message text). Kinds:
@@ -634,37 +706,109 @@ consciously violate the contract.
634
706
 
635
707
  ## Middleware
636
708
 
637
- Side of the chain ordering (concept-level see the root `../../../CLAUDE.md`
638
- §Middleware Ordering for the canonical assembly order):
639
-
640
- **Session parsing is separate from auth enforcement.** The session /
641
- request-context middleware populates `{account, actor, permits}` from a
642
- cookie but does not 401; `require_auth` / `require_role` / `require_keeper`
643
- enforce. This lets `/login` and `/bootstrap` participate in cookie refresh
644
- without being blocked.
709
+ See the root `../../../CLAUDE.md` §Middleware Ordering for the canonical
710
+ assembly order. Two-phase identity:
711
+
712
+ - **Authentication** runs in middleware (session / bearer / daemon
713
+ token). Sets `c.var.account_id` + `CREDENTIAL_TYPE_KEY` on a valid
714
+ credential. Account-only never loads actor or permits, never
715
+ populates `REQUEST_CONTEXT_KEY`. **Production-middleware invariant**:
716
+ no production middleware on the auth path (session / bearer / daemon
717
+ token) populates `REQUEST_CONTEXT_KEY`; identity-related context vars
718
+ it does set are `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, and (for
719
+ sessions / bearer) `AUTH_SESSION_TOKEN_HASH_KEY` /
720
+ `AUTH_API_TOKEN_ID_KEY`. Other middleware (proxy, app server,
721
+ session-cookie parser) sets unrelated vars like `client_ip`,
722
+ `pending_effects`, and the session-token slot keyed by
723
+ `session_options.context_key` (default `auth_session_id`) — those
724
+ are out of scope for this invariant. Test harnesses pre-populate
725
+ `REQUEST_CONTEXT_KEY` + `TEST_CONTEXT_PRESET_KEY` to bypass DB-backed
726
+ actor resolution; production code that consults
727
+ `REQUEST_CONTEXT_KEY` is reading test escape-hatch state, never live
728
+ middleware output.
729
+ - **Authorization** runs in the route-spec wrapper / RPC dispatcher
730
+ before input validation (matches the RPC dispatcher's order so 401 /
731
+ 403 surface ahead of `invalid_params`). When the route's input
732
+ declares `acting?: ActingActor` or its auth requires permits
733
+ (`role` / `keeper`), the authorization phase calls
734
+ `resolve_acting_actor` over the raw `acting` value extracted from
735
+ query (GET) or pre-parsed body (mutating methods), builds the
736
+ actor-bound `RequestContext`, and sets `REQUEST_CONTEXT_KEY` before
737
+ the role / keeper guards fire. Account-grain routes skip resolution
738
+ and run with `RequestContext.actor: null`. Resolution failures come
739
+ back as `AuthorizationFailure` (`{status, body}`) — the auth domain
740
+ stops short of constructing a `Response` so each transport binds the
741
+ same failure to its wire shape: REST emits `c.json(body, status)`;
742
+ the WS upgrade does the same; the RPC dispatcher folds it into a
743
+ JSON-RPC envelope (`{jsonrpc, id, error: {code, message, data}}`)
744
+ with `error.message` carrying the reason string and
745
+ `error.data: {reason, ...rest}` flattening any diagnostic fields
746
+ (e.g. `available[]` for `actor_required`). The two 500 reasons the
747
+ phase emits are kept distinct: `no_actors_on_account` names a signup
748
+ invariant violation (`resolve_acting_actor` enumerated zero actors);
749
+ `account_vanished` names a torn-read race (`build_request_context` /
750
+ `build_account_context` returned null after a successful resolve —
751
+ the account or actor row was deleted between credential validation
752
+ and the dispatcher's follow-up read). See the root
753
+ `../../../CLAUDE.md` § Cleanest architecture takes priority for the
754
+ rationale.
755
+
756
+ Session parsing is separate from auth enforcement — login / bootstrap
757
+ participate in cookie refresh without being blocked. `require_auth` /
758
+ `require_role` / `require_keeper` are the gates.
645
759
 
646
760
  ### `request_context.ts`
647
761
 
648
- - `RequestContext = {account, actor, permits}`.
762
+ - `RequestContext = {account, actor: Actor | null, permits}`. `actor`
763
+ is null on account-grain routes (no `acting`, no permit-requiring
764
+ auth); `permits` is empty in that case.
649
765
  - `REQUEST_CONTEXT_KEY` — Hono context variable name.
650
766
  - **`AUTH_SESSION_TOKEN_HASH_KEY`** — holds the blake3 session hash. Set on
651
767
  successful session lookup; `null` for unauthenticated or non-session
652
768
  credentials. Exposed so SSE endpoints can scope per-session resource
653
769
  identity (the audit-log SSE uses this to close only the revoked session's
654
770
  stream on `session_revoke`).
655
- - `get_request_context(c)`, `require_request_context(c)` (throws on misuse
656
- misconfigured middleware surfaces immediately), `has_role(ctx, role, now?)`.
657
- - `build_request_context(deps, account_id)`shared helper used by
658
- session, bearer, and daemon token middleware; does
659
- `account actor permits` and returns `null` if either lookup misses.
771
+ - `get_request_context(c)`, `require_request_context(c)` (throws on
772
+ misuse handler ran without authorization phase wiring).
773
+ - **In-memory permit predicates** `has_role(ctx, role, now?)`,
774
+ `has_scoped_role(ctx, role, scope_id, now?)`,
775
+ `has_any_scoped_role(ctx, roles, scope_id, now?)`. All three take
776
+ `RequestContext | null` and return `false` for null ctx and for
777
+ account-grain ctx (`actor: null`, empty `permits`); they drop into
778
+ `auth: 'public'` and account-grain handlers without a manual narrow.
779
+ `scope_id === null` matches global permits only; UUID matches that
780
+ exact scope. Empty `roles` short-circuits `has_any_scoped_role` to
781
+ `false`. Decide-time predicates only — the predicate / mutation
782
+ race window is the same as the SQL `query_permit_has_role` style and
783
+ only a transactional re-check inside the UPDATE/INSERT closes it.
784
+ - `build_request_context(deps, account_id, actor_id)` — loads
785
+ `account` + the named `actor` + active permits. Verifies
786
+ `actor.account_id === account.id`; returns `null` when the account
787
+ or actor is missing, or when they don't bind to each other. Called
788
+ by the authorization phase after `resolve_acting_actor` succeeds —
789
+ a null return there is a torn read (account/actor deleted mid-request)
790
+ rather than the missing-actor invariant `resolve_acting_actor` would
791
+ have caught upstream, so the phase surfaces `ERROR_ACCOUNT_VANISHED`
792
+ on null. Not called from middleware.
793
+ - `resolve_acting_actor(deps, account_id, acting_actor_id)` — uniform
794
+ resolver. Resolves to `{ok: true, actor_id}` for 1 actor (any
795
+ `acting`) or matching supplied id; `actor_required` with the
796
+ available list when multi-actor and `acting` is missing;
797
+ `actor_not_on_account` when supplied id doesn't belong; `no_actors`
798
+ defensively.
660
799
  - `refresh_permits(ctx, deps)` — reloads permits without mutating the
661
- original (concurrent-safe). Useful for long-lived WebSocket connections.
800
+ original (concurrent-safe). Useful for long-lived WebSocket
801
+ connections that have an acting actor.
662
802
  - `create_request_context_middleware(deps, log, session_context_key?)` —
663
- reads session token from context, hashes, validates, loads context, sets
664
- `CREDENTIAL_TYPE_KEY = 'session'`, fires `session_touch_fire_and_forget`.
665
- - `require_auth` 401 (`ERROR_AUTHENTICATION_REQUIRED`) on no context.
666
- - `require_role(role)` — 401 on no context, 403 (`ERROR_INSUFFICIENT_PERMISSIONS`
667
- - `required_role`) on missing role.
803
+ validates the session and sets `c.var.account_id` +
804
+ `CREDENTIAL_TYPE_KEY = 'session'` + `AUTH_SESSION_TOKEN_HASH_KEY`.
805
+ Touches the session fire-and-forget. Does not load actor / permits.
806
+ - `require_auth` — 401 (`ERROR_AUTHENTICATION_REQUIRED`) when
807
+ `account_id` is null. Does not require an acting actor.
808
+ - `require_role(role)` — 401 on no auth, 403
809
+ (`ERROR_INSUFFICIENT_PERMISSIONS` + `required_role`) when permits
810
+ don't carry the role. Implies the authorization phase ran (a
811
+ role-gated route always resolves an actor).
668
812
 
669
813
  ### `bearer_auth.ts`
670
814
 
@@ -938,7 +1082,7 @@ Closure state:
938
1082
  `all_admin_action_specs: Array<RequestResponseActionSpec>` — codegen-ready
939
1083
  registry of all eleven specs (always includes the two app-settings specs).
940
1084
 
941
- Deps: `AdminActionDeps = Pick<RouteFactoryDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`. The `audit_log_config` slot flows through to `audit_log_fire_and_forget` so consumer-extended event-type metadata gets validated.
1085
+ Deps: `AdminActionDeps = AuditEmitDeps` — the shared `Pick<AppDeps, 'log' | 'on_audit_event' | 'audit_log_config'>` slice every audit-emitting site picks (defined in `auth/deps.ts`). The `audit_log_config` slot flows through to `audit_log_fire_and_forget` so consumer-extended event-type metadata gets validated.
942
1086
 
943
1087
  ### `permit_offer_action_specs.ts` + `permit_offer_actions.ts` — seven RPC actions
944
1088
 
@@ -973,15 +1117,19 @@ Six offer-lifecycle methods plus `permit_revoke`. Authorization is a mix:
973
1117
  **`actor_id`, not `account_id`** — permits are actor-scoped and deriving
974
1118
  actor from account collapses under multi-actor accounts.
975
1119
 
976
- | Spec | Input | Output |
977
- | ---------------------------------- | -------------------------------------------- | ------------------------------------------ |
978
- | `permit_offer_create_action_spec` | `{to_account_id, role, scope_id?, message?}` | `{offer}` |
979
- | `permit_offer_accept_action_spec` | `{offer_id}` | `{permit_id, offer, superseded_offer_ids}` |
980
- | `permit_offer_decline_action_spec` | `{offer_id, reason?}` | `{ok}` |
981
- | `permit_offer_retract_action_spec` | `{offer_id}` | `{ok}` |
982
- | `permit_offer_list_action_spec` | `{account_id?}` | `{offers}` |
983
- | `permit_offer_history_action_spec` | `{account_id?, limit?, offset?}` | `{offers}` |
984
- | `permit_revoke_action_spec` | `{actor_id, permit_id, reason?}` | `{ok, revoked}` |
1120
+ Every input row below also carries the shared `acting?: ActingActor`
1121
+ field that the dispatcher's authorization phase reads off the raw
1122
+ params (omitted from the table for brevity).
1123
+
1124
+ | Spec | Input | Output |
1125
+ | ---------------------------------- | ---------------------------------------------------------- | ------------------------------------------ |
1126
+ | `permit_offer_create_action_spec` | `{to_account_id, to_actor_id?, role, scope_id?, message?}` | `{offer}` |
1127
+ | `permit_offer_accept_action_spec` | `{offer_id}` | `{permit_id, offer, superseded_offer_ids}` |
1128
+ | `permit_offer_decline_action_spec` | `{offer_id, reason?}` | `{ok}` |
1129
+ | `permit_offer_retract_action_spec` | `{offer_id}` | `{ok}` |
1130
+ | `permit_offer_list_action_spec` | `{account_id?}` | `{offers}` |
1131
+ | `permit_offer_history_action_spec` | `{account_id?, limit?, offset?}` | `{offers}` |
1132
+ | `permit_revoke_action_spec` | `{actor_id, permit_id, reason?}` | `{ok, revoked}` |
985
1133
 
986
1134
  Error reason constants (exported as `as const` literals):
987
1135
 
@@ -991,6 +1139,11 @@ Error reason constants (exported as `as const` literals):
991
1139
  - `ERROR_OFFER_NOT_FOUND` (`'offer_not_found'` — 404-over-403 IDOR mask)
992
1140
  - `ERROR_OFFER_ROLE_NOT_GRANTABLE` (`'offer_role_not_grantable'`)
993
1141
  - `ERROR_OFFER_NOT_AUTHORIZED` (`'offer_not_authorized'`)
1142
+ - `ERROR_OFFER_ACTOR_ACCOUNT_MISMATCH` (`'offer_actor_account_mismatch'` —
1143
+ `permit_offer_create` was called with a `to_actor_id` that does not
1144
+ belong to `to_account_id`)
1145
+ - `ERROR_OFFER_ACTOR_MISMATCH` (`'offer_actor_mismatch'` —
1146
+ actor-targeted offer was accepted by an actor other than `to_actor_id`)
994
1147
 
995
1148
  Plus re-uses from `../http/error_schemas.ts`: `ERROR_PERMIT_NOT_FOUND`,
996
1149
  `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_INSUFFICIENT_PERMISSIONS`,
@@ -1007,11 +1160,18 @@ Failure-outcome audit events emitted (success and failure rows both carry
1007
1160
  `ip: ctx.client_ip` — uniform with the admin and self-service surfaces):
1008
1161
 
1009
1162
  - `permit_offer_create` failure — `web_grantable` denial, `authorize`
1010
- denial, self-target rejection (all three denial paths emit the same
1011
- audit row with `target_account_id`).
1163
+ denial, self-target rejection, and actor-account mismatch all emit
1164
+ the same audit row via `emit_create_failure_audit`. `target_account_id`
1165
+ carries `input.to_account_id`; `target_actor_id` echoes
1166
+ `input.to_actor_id` when supplied so failure rows match the
1167
+ success-shape envelope of actor-targeted offers (null on
1168
+ account-grain offers — see audit_log_schema rule).
1012
1169
  - `permit_revoke` failure — `web_grantable` denial after IDOR / role
1013
1170
  lookup succeeded. The admin-role-denied path (pre-IDOR) emits no audit,
1014
- matching the middleware auth-guard precedent.
1171
+ matching the middleware auth-guard precedent. `target_account_id` +
1172
+ `target_actor_id` both populated (the IDOR-passing branch resolves
1173
+ the target actor before the gate; the subject is an actor-bound
1174
+ permit).
1015
1175
 
1016
1176
  WS notifications (post-commit via `emit_after_commit` from
1017
1177
  `../http/pending_effects.js` — swallows exceptions so one failed send
@@ -1025,7 +1185,7 @@ can't starve others; see `../http/CLAUDE.md` §Pending Effects):
1025
1185
  - Revoke → `permit_revoke` to revokee + one `permit_offer_supersede` per
1026
1186
  superseded sibling.
1027
1187
 
1028
- Deps: `PermitOfferActionDeps extends Pick<RouteFactoryDeps, 'log' | 'on_audit_event' | 'audit_log_config'> & {notification_sender?: NotificationSender | null}`.
1188
+ Deps: `PermitOfferActionDeps extends AuditEmitDeps & {notification_sender?: NotificationSender | null}`.
1029
1189
  Notification sender is optional — when absent, WS fan-out is silently
1030
1190
  skipped (DB-only side effects still happen).
1031
1191
 
@@ -1128,7 +1288,7 @@ Audit events emitted (via `audit_log_fire_and_forget` with `ip: ctx.client_ip`):
1128
1288
  IP is the resolved trusted-proxy value from `ActionContext.client_ip`,
1129
1289
  matching the REST handler convention.
1130
1290
 
1131
- Deps: `AccountActionDeps = Pick<RouteFactoryDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`.
1291
+ Deps: `AccountActionDeps = AuditEmitDeps`.
1132
1292
  Options: `{max_tokens?: number | null}` — defaults to `DEFAULT_MAX_TOKENS`
1133
1293
  from `account_routes.ts`; `null` disables the cap.
1134
1294
 
@@ -1172,16 +1332,17 @@ codegen invariant and grow the surface linearly per role.
1172
1332
  `eligible_roles` is checked against `roles.role_options` at factory
1173
1333
  time so typos throw at startup instead of at first call.
1174
1334
 
1175
- Grant branch uses `query_permit_has_role` for a benign-TOCTOU pre-check
1176
- (distinguishes new grant from idempotent re-grant), then
1177
- `query_grant_permit` for the actual insert. Revoke branch filters
1335
+ Grant branch uses `has_scoped_role(auth, role, null)` for a
1336
+ benign-TOCTOU pre-check (distinguishes new grant from idempotent
1337
+ re-grant) reads from the in-memory `auth.permits` snapshot, no DB
1338
+ roundtrip — then `query_grant_permit` for the actual insert. Revoke branch filters
1178
1339
  `query_permit_find_active_for_actor` in JS for the matching
1179
1340
  `(actor, role, scope_id IS NULL)` row before calling
1180
1341
  `query_revoke_permit`. Bundle is **not** included in
1181
1342
  `create_standard_rpc_actions` — `eligible_roles` is app-specific, opt-in,
1182
1343
  spread alongside the standard bundle when needed.
1183
1344
 
1184
- Deps: `SelfServiceRoleActionDeps = Pick<RouteFactoryDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`.
1345
+ Deps: `SelfServiceRoleActionDeps = AuditEmitDeps`.
1185
1346
 
1186
1347
  `all_self_service_role_action_specs: ReadonlyArray<RequestResponseActionSpec>` —
1187
1348
  codegen-ready registry of the single unified spec.
@@ -1231,6 +1392,12 @@ resulting permit.
1231
1392
  - **`RouteFactoryDeps = Omit<AppDeps, 'db'>`** — for route factories. Route
1232
1393
  handlers receive DB access via `RouteContext`, so factories don't capture
1233
1394
  a pool-level `Db`.
1395
+ - **`AuditEmitDeps = Pick<AppDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`**
1396
+ — the slice every audit-emitting site needs. Used by `audit_log_fire_and_forget`
1397
+ / `emit_permit_target_event` (the primitives) and aliased by every
1398
+ action-factory deps type (`AdminActionDeps`, `AccountActionDeps`,
1399
+ `PermitOfferActionDeps`, `SelfServiceRoleActionDeps`) so the five
1400
+ factories stop spelling the same `Pick` independently.
1234
1401
 
1235
1402
  See root `../../../CLAUDE.md` §AppDeps Vocabulary for the
1236
1403
  capability / options / runtime-state split across the whole project.
@@ -22,7 +22,7 @@
22
22
  * @module
23
23
  */
24
24
  import { type RpcAction } from '../actions/action_rpc.js';
25
- import type { RouteFactoryDeps } from './deps.js';
25
+ import type { AuditEmitDeps } from './deps.js';
26
26
  /** Options for `create_account_actions`. */
27
27
  export interface AccountActionOptions {
28
28
  /**
@@ -36,12 +36,12 @@ export interface AccountActionOptions {
36
36
  /**
37
37
  * Dependencies for `create_account_actions`.
38
38
  *
39
- * Shares shape with `AdminActionDeps` / `PermitOfferActionDeps` so consumers
40
- * can pass the same deps to every action factory. `audit_log_config` is
41
- * carried through `AppDeps` and consumed by `audit_log_fire_and_forget`;
42
- * absent → defaults to `BUILTIN_AUDIT_LOG_CONFIG`.
39
+ * Aliases the shared `AuditEmitDeps` (the `log` / `on_audit_event` /
40
+ * optional `audit_log_config` slice every audit-emitting site picks).
41
+ * `audit_log_config` is consumed by `audit_log_fire_and_forget`; absent →
42
+ * defaults to `BUILTIN_AUDIT_LOG_CONFIG`.
43
43
  */
44
- export type AccountActionDeps = Pick<RouteFactoryDeps, 'log' | 'on_audit_event' | 'audit_log_config'>;
44
+ export type AccountActionDeps = AuditEmitDeps;
45
45
  /**
46
46
  * Create the self-service account RPC actions.
47
47
  *
@@ -1 +1 @@
1
- {"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAiC,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAgBxF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,WAAW,CAAC;AAwBhD,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,CACnC,gBAAgB,EAChB,KAAK,GAAG,gBAAgB,GAAG,kBAAkB,CAC7C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,iBAAiB,EACvB,UAAS,oBAAyB,KAChC,KAAK,CAAC,SAAS,CAyHjB,CAAC"}
1
+ {"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAiC,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAgBxF,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,WAAW,CAAC;AAyB7C,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,CAAC;AAE9C;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,iBAAiB,EACvB,UAAS,oBAAyB,KAChC,KAAK,CAAC,SAAS,CAqHjB,CAAC"}
@@ -28,6 +28,7 @@ import { query_api_token_enforce_limit, query_api_token_list_for_account, query_
28
28
  import { generate_api_token } from './api_token.js';
29
29
  import { audit_log_fire_and_forget } from './audit_log_queries.js';
30
30
  import { DEFAULT_MAX_TOKENS } from './account_routes.js';
31
+ import { require_request_auth } from './request_context.js';
31
32
  import { account_verify_action_spec, account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec, account_token_create_action_spec, account_token_list_action_spec, account_token_revoke_action_spec, } from './account_action_specs.js';
32
33
  /**
33
34
  * Create the self-service account RPC actions.
@@ -39,21 +40,20 @@ import { account_verify_action_spec, account_session_list_action_spec, account_s
39
40
  export const create_account_actions = (deps, options = {}) => {
40
41
  const { max_tokens = DEFAULT_MAX_TOKENS } = options;
41
42
  const verify_handler = (_input, ctx) => {
42
- const auth = ctx.auth;
43
+ const auth = require_request_auth(ctx.auth);
43
44
  return to_session_account(auth.account);
44
45
  };
45
46
  const session_list_handler = async (_input, ctx) => {
46
- const auth = ctx.auth;
47
+ const auth = require_request_auth(ctx.auth);
47
48
  const sessions = await query_session_list_for_account(ctx, auth.account.id);
48
49
  return { sessions };
49
50
  };
50
51
  const session_revoke_handler = async (input, ctx) => {
51
- const auth = ctx.auth;
52
+ const auth = require_request_auth(ctx.auth);
52
53
  const revoked = await query_session_revoke_for_account(ctx, input.session_id, auth.account.id);
53
54
  void audit_log_fire_and_forget(ctx, {
54
55
  event_type: 'session_revoke',
55
56
  outcome: revoked ? 'success' : 'failure',
56
- actor_id: auth.actor.id,
57
57
  account_id: auth.account.id,
58
58
  ip: ctx.client_ip,
59
59
  metadata: { session_id: input.session_id },
@@ -61,11 +61,10 @@ export const create_account_actions = (deps, options = {}) => {
61
61
  return { ok: true, revoked };
62
62
  };
63
63
  const session_revoke_all_handler = async (_input, ctx) => {
64
- const auth = ctx.auth;
64
+ const auth = require_request_auth(ctx.auth);
65
65
  const count = await query_session_revoke_all_for_account(ctx, auth.account.id);
66
66
  void audit_log_fire_and_forget(ctx, {
67
67
  event_type: 'session_revoke_all',
68
- actor_id: auth.actor.id,
69
68
  account_id: auth.account.id,
70
69
  ip: ctx.client_ip,
71
70
  metadata: { count },
@@ -73,7 +72,7 @@ export const create_account_actions = (deps, options = {}) => {
73
72
  return { ok: true, count };
74
73
  };
75
74
  const token_create_handler = async (input, ctx) => {
76
- const auth = ctx.auth;
75
+ const auth = require_request_auth(ctx.auth);
77
76
  const { token, id, token_hash } = generate_api_token();
78
77
  await query_create_api_token(ctx, id, auth.account.id, input.name, token_hash);
79
78
  if (max_tokens != null) {
@@ -81,7 +80,6 @@ export const create_account_actions = (deps, options = {}) => {
81
80
  }
82
81
  void audit_log_fire_and_forget(ctx, {
83
82
  event_type: 'token_create',
84
- actor_id: auth.actor.id,
85
83
  account_id: auth.account.id,
86
84
  ip: ctx.client_ip,
87
85
  metadata: { token_id: id, name: input.name },
@@ -89,17 +87,16 @@ export const create_account_actions = (deps, options = {}) => {
89
87
  return { ok: true, token, id, name: input.name };
90
88
  };
91
89
  const token_list_handler = async (_input, ctx) => {
92
- const auth = ctx.auth;
90
+ const auth = require_request_auth(ctx.auth);
93
91
  const tokens = await query_api_token_list_for_account(ctx, auth.account.id);
94
92
  return { tokens };
95
93
  };
96
94
  const token_revoke_handler = async (input, ctx) => {
97
- const auth = ctx.auth;
95
+ const auth = require_request_auth(ctx.auth);
98
96
  const revoked = await query_revoke_api_token_for_account(ctx, input.token_id, auth.account.id);
99
97
  void audit_log_fire_and_forget(ctx, {
100
98
  event_type: 'token_revoke',
101
99
  outcome: revoked ? 'success' : 'failure',
102
- actor_id: auth.actor.id,
103
100
  account_id: auth.account.id,
104
101
  ip: ctx.client_ip,
105
102
  metadata: { token_id: input.token_id },
@@ -68,11 +68,14 @@ export declare const query_account_has_any: (deps: QueryDeps) => Promise<boolean
68
68
  */
69
69
  export declare const query_create_actor: (deps: QueryDeps, account_id: string, name: string) => Promise<Actor>;
70
70
  /**
71
- * Find the actor for an account.
71
+ * List every actor on an account, ordered by `created_at`.
72
72
  *
73
- * For v1, each account has exactly one actor.
73
+ * Used by `resolve_acting_actor` to resolve the acting actor for a
74
+ * request: 1 actor picks transparently, multiple require an explicit
75
+ * `acting` field on the request payload. For lookups by id, use
76
+ * `query_actor_by_id` instead.
74
77
  */
75
- export declare const query_actor_by_account: (deps: QueryDeps, account_id: string) => Promise<Actor | undefined>;
78
+ export declare const query_actors_by_account: (deps: QueryDeps, account_id: string) => Promise<Array<Actor>>;
76
79
  /**
77
80
  * Find an actor by id.
78
81
  */
@@ -1 +1 @@
1
- {"version":3,"file":"account_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAEN,KAAK,OAAO,EACZ,KAAK,KAAK,EACV,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,OAAO,kBAAkB,KACvB,OAAO,CAAC,OAAO,CAQjB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,OAAO,GAAG,SAAS,CAE7B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,OAAO,GAAG,SAAS,CAI7B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,OAAO,GAAG,SAAS,CAI7B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,OAAO,GAAG,SAAS,CAS7B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,IAAI,MAAM,EACV,eAAe,MAAM,EACrB,YAAY,MAAM,GAAG,IAAI,KACvB,OAAO,CAAC,IAAI,CAKd,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAU,MAAM,SAAS,EAAE,IAAI,MAAM,KAAG,OAAO,CAAC,OAAO,CAKvF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,OAAO,CAK5E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,MAAM,MAAM,KACV,OAAO,CAAC,KAAK,CAMf,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,KAAK,GAAG,SAAS,CAE3B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,KAAK,GAAG,SAAS,CAE3B,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,SAAS,EACf,OAAO,kBAAkB,KACvB,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAC,CAI1C,CAAC;AAyBF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,SAAS,KACb,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,CA+EtC,CAAC"}
1
+ {"version":3,"file":"account_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAEN,KAAK,OAAO,EACZ,KAAK,KAAK,EACV,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,OAAO,kBAAkB,KACvB,OAAO,CAAC,OAAO,CAQjB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,OAAO,GAAG,SAAS,CAE7B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,yBAAyB,GACrC,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,OAAO,GAAG,SAAS,CAI7B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,OAAO,GAAG,SAAS,CAI7B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kCAAkC,GAC9C,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,OAAO,GAAG,SAAS,CAS7B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,IAAI,MAAM,EACV,eAAe,MAAM,EACrB,YAAY,MAAM,GAAG,IAAI,KACvB,OAAO,CAAC,IAAI,CAKd,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAU,MAAM,SAAS,EAAE,IAAI,MAAM,KAAG,OAAO,CAAC,OAAO,CAKvF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,OAAO,CAK5E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,MAAM,MAAM,KACV,OAAO,CAAC,KAAK,CAMf,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAKtB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,KAAK,GAAG,SAAS,CAE3B,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,SAAS,EACf,OAAO,kBAAkB,KACvB,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAC,CAI1C,CAAC;AAyBF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,SAAS,KACb,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAqFtC,CAAC"}
@@ -101,12 +101,15 @@ export const query_create_actor = async (deps, account_id, name) => {
101
101
  return assert_row(row, 'INSERT INTO actor');
102
102
  };
103
103
  /**
104
- * Find the actor for an account.
104
+ * List every actor on an account, ordered by `created_at`.
105
105
  *
106
- * For v1, each account has exactly one actor.
106
+ * Used by `resolve_acting_actor` to resolve the acting actor for a
107
+ * request: 1 actor picks transparently, multiple require an explicit
108
+ * `acting` field on the request payload. For lookups by id, use
109
+ * `query_actor_by_id` instead.
107
110
  */
108
- export const query_actor_by_account = async (deps, account_id) => {
109
- return deps.db.query_one(`SELECT * FROM actor WHERE account_id = $1`, [account_id]);
111
+ export const query_actors_by_account = async (deps, account_id) => {
112
+ return deps.db.query(`SELECT * FROM actor WHERE account_id = $1 ORDER BY created_at ASC, id ASC`, [account_id]);
110
113
  };
111
114
  /**
112
115
  * Find an actor by id.
@@ -161,7 +164,13 @@ export const query_admin_account_list = async (deps) => {
161
164
  AND po.expires_at > NOW()
162
165
  ORDER BY po.expires_at ASC`),
163
166
  ]);
164
- // Index actors by account_id (1:1 in v1)
167
+ // Index actors by account_id. Multi-actor TODO: this Map keyed by
168
+ // account_id silently overwrites earlier actors when an account
169
+ // hosts more than one — when multi-actor lands, the admin row shape
170
+ // must change from "account → one actor" to "account → Array<Actor>"
171
+ // (or split into a separate per-actor row). The JSON shape change
172
+ // will ripple into the admin UI; bundle that with the multi-actor
173
+ // session-actor-selector work.
165
174
  const actor_by_account = new Map();
166
175
  for (const actor of actors) {
167
176
  actor_by_account.set(actor.account_id, actor);