@fuzdev/fuz_app 0.54.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 (142) 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 +219 -66
  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 +98 -90
  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 +321 -38
  73. package/dist/auth/request_context.d.ts.map +1 -1
  74. package/dist/auth/request_context.js +393 -66
  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 +18 -8
  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/ws_round_trip.d.ts.map +1 -1
  131. package/dist/testing/ws_round_trip.js +5 -1
  132. package/dist/ui/CLAUDE.md +16 -10
  133. package/dist/ui/PermitOfferForm.svelte +14 -0
  134. package/dist/ui/PermitOfferForm.svelte.d.ts +6 -0
  135. package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -1
  136. package/dist/ui/admin_accounts_state.svelte.d.ts +8 -1
  137. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  138. package/dist/ui/admin_accounts_state.svelte.js +14 -3
  139. package/dist/ui/permit_offers_state.svelte.d.ts +9 -1
  140. package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -1
  141. package/dist/ui/permit_offers_state.svelte.js +7 -1
  142. 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
@@ -375,8 +432,9 @@ CRUD + listing:
375
432
  active permit at `scope_id` (role-agnostic) and supersedes every pending
376
433
  offer at `scope_id` (tuple-matched and orphan, undifferentiated) in the
377
434
  caller's transaction. Returns `RevokeForScopeResult = {revoked, superseded_offers}`
378
- — `revoked` carries `account_id` for `permit_revoke` fan-out;
379
- `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
380
438
  `permit_offer_supersede` audits with `reason: 'scope_destroyed'` and
381
439
  `cause_id: <destroyed scope row id>` per superseded offer (the cause is
382
440
  the scope deletion, not any individual permit revoke). Use from a
@@ -389,9 +447,12 @@ CRUD + listing:
389
447
  Error classes (all extend `Error` with stable `.name` — never use
390
448
  `instanceof` against plain messages):
391
449
 
392
- - `PermitOfferSelfTargetError` — grantor offered themselves. Enforced via
393
- cross-row JOIN in `query_permit_offer_create` (rather than CHECK) to avoid
394
- 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).
395
456
  - `PermitOfferAlreadyTerminalError` — offer exists for the caller but is
396
457
  accepted / declined / retracted / superseded.
397
458
  - `PermitOfferExpiredError` — pending but past `expires_at` (distinct from
@@ -515,19 +576,24 @@ run'` if the seed somehow missed (defensive — migrations always seed).
515
576
  - `query_audit_log_list(deps, options?)` — supports `event_type`,
516
577
  `event_type_in`, `account_id` (matches `account_id` OR
517
578
  `target_account_id`), `outcome`, `since_seq`, `limit`, `offset`.
518
- - `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.
519
586
  - `query_audit_log_list_for_account`, `query_audit_log_list_permit_history`
520
587
  (filters to `permit_grant` / `permit_revoke`).
521
588
  - `query_audit_log_cleanup_before`.
522
589
  - **`audit_log_fire_and_forget(route, input, deps)`** —
523
590
  writes to `route.background_db` (pool-level), so audit entries persist
524
- even when the request transaction rolls back. `deps` is an
525
- `AuditLogFireAndForgetDeps` bundle (`{log, on_audit_event, audit_log_config?}`)
526
- structurally compatible with `Pick<AppDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`,
527
- so call sites pass the surrounding deps object directly. Bundling
528
- replaces the prior 5-arg positional signature; consumers that forgot
529
- the trailing `config` would silently fall back to
530
- `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
531
597
  failures are logged separately. Pushes onto `route.pending_effects`
532
598
  for test flushing.
533
599
 
@@ -566,11 +632,14 @@ by `sequence`, then enforces:
566
632
  3. **Run the pending tail** (`code[applied.length..]`) inside a single
567
633
  chain transaction; each `INSERT` uses `sequence = max(sequence) + 1`.
568
634
 
569
- **Append-only after first publish.** Once a fuz_app version containing a
570
- migration is published, the migration's name and position are frozen.
571
- Pre-publish, anything goes; the cliff is the publish event. Body edits to
572
- a published migration slip past the runner (no content hashing) — schema-
573
- 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.
574
643
 
575
644
  `MigrationError` is the only error class thrown from `run_migrations` /
576
645
  `baseline`; branch on `.kind` (never on message text). Kinds:
@@ -637,47 +706,109 @@ consciously violate the contract.
637
706
 
638
707
  ## Middleware
639
708
 
640
- Side of the chain ordering (concept-level see the root `../../../CLAUDE.md`
641
- §Middleware Ordering for the canonical assembly order):
642
-
643
- **Session parsing is separate from auth enforcement.** The session /
644
- request-context middleware populates `{account, actor, permits}` from a
645
- cookie but does not 401; `require_auth` / `require_role` / `require_keeper`
646
- enforce. This lets `/login` and `/bootstrap` participate in cookie refresh
647
- 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.
648
759
 
649
760
  ### `request_context.ts`
650
761
 
651
- - `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.
652
765
  - `REQUEST_CONTEXT_KEY` — Hono context variable name.
653
766
  - **`AUTH_SESSION_TOKEN_HASH_KEY`** — holds the blake3 session hash. Set on
654
767
  successful session lookup; `null` for unauthenticated or non-session
655
768
  credentials. Exposed so SSE endpoints can scope per-session resource
656
769
  identity (the audit-log SSE uses this to close only the revoked session's
657
770
  stream on `session_revoke`).
658
- - `get_request_context(c)`, `require_request_context(c)` (throws on misuse
659
- misconfigured middleware surfaces immediately).
771
+ - `get_request_context(c)`, `require_request_context(c)` (throws on
772
+ misuse handler ran without authorization phase wiring).
660
773
  - **In-memory permit predicates** — `has_role(ctx, role, now?)`,
661
774
  `has_scoped_role(ctx, role, scope_id, now?)`,
662
775
  `has_any_scoped_role(ctx, roles, scope_id, now?)`. All three take
663
- `RequestContext | null` (null returns `false`) so they drop into
664
- `auth: 'public'` handlers without a manual narrow. `scope_id === null`
665
- matches global permits only; UUID matches that exact scope. Empty
666
- `roles` short-circuits `has_any_scoped_role` to `false`. Decide-time
667
- predicates only the predicate / mutation race window is the same as
668
- the SQL `query_permit_has_role` style and only a transactional re-check
669
- inside the UPDATE/INSERT closes it.
670
- - `build_request_context(deps, account_id)` shared helper used by
671
- session, bearer, and daemon token middleware; does
672
- `account actor permits` and returns `null` if either lookup misses.
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.
673
799
  - `refresh_permits(ctx, deps)` — reloads permits without mutating the
674
- 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.
675
802
  - `create_request_context_middleware(deps, log, session_context_key?)` —
676
- reads session token from context, hashes, validates, loads context, sets
677
- `CREDENTIAL_TYPE_KEY = 'session'`, fires `session_touch_fire_and_forget`.
678
- - `require_auth` 401 (`ERROR_AUTHENTICATION_REQUIRED`) on no context.
679
- - `require_role(role)` — 401 on no context, 403 (`ERROR_INSUFFICIENT_PERMISSIONS`
680
- - `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).
681
812
 
682
813
  ### `bearer_auth.ts`
683
814
 
@@ -951,7 +1082,7 @@ Closure state:
951
1082
  `all_admin_action_specs: Array<RequestResponseActionSpec>` — codegen-ready
952
1083
  registry of all eleven specs (always includes the two app-settings specs).
953
1084
 
954
- 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.
955
1086
 
956
1087
  ### `permit_offer_action_specs.ts` + `permit_offer_actions.ts` — seven RPC actions
957
1088
 
@@ -986,15 +1117,19 @@ Six offer-lifecycle methods plus `permit_revoke`. Authorization is a mix:
986
1117
  **`actor_id`, not `account_id`** — permits are actor-scoped and deriving
987
1118
  actor from account collapses under multi-actor accounts.
988
1119
 
989
- | Spec | Input | Output |
990
- | ---------------------------------- | -------------------------------------------- | ------------------------------------------ |
991
- | `permit_offer_create_action_spec` | `{to_account_id, role, scope_id?, message?}` | `{offer}` |
992
- | `permit_offer_accept_action_spec` | `{offer_id}` | `{permit_id, offer, superseded_offer_ids}` |
993
- | `permit_offer_decline_action_spec` | `{offer_id, reason?}` | `{ok}` |
994
- | `permit_offer_retract_action_spec` | `{offer_id}` | `{ok}` |
995
- | `permit_offer_list_action_spec` | `{account_id?}` | `{offers}` |
996
- | `permit_offer_history_action_spec` | `{account_id?, limit?, offset?}` | `{offers}` |
997
- | `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}` |
998
1133
 
999
1134
  Error reason constants (exported as `as const` literals):
1000
1135
 
@@ -1004,6 +1139,11 @@ Error reason constants (exported as `as const` literals):
1004
1139
  - `ERROR_OFFER_NOT_FOUND` (`'offer_not_found'` — 404-over-403 IDOR mask)
1005
1140
  - `ERROR_OFFER_ROLE_NOT_GRANTABLE` (`'offer_role_not_grantable'`)
1006
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`)
1007
1147
 
1008
1148
  Plus re-uses from `../http/error_schemas.ts`: `ERROR_PERMIT_NOT_FOUND`,
1009
1149
  `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_INSUFFICIENT_PERMISSIONS`,
@@ -1020,11 +1160,18 @@ Failure-outcome audit events emitted (success and failure rows both carry
1020
1160
  `ip: ctx.client_ip` — uniform with the admin and self-service surfaces):
1021
1161
 
1022
1162
  - `permit_offer_create` failure — `web_grantable` denial, `authorize`
1023
- denial, self-target rejection (all three denial paths emit the same
1024
- 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).
1025
1169
  - `permit_revoke` failure — `web_grantable` denial after IDOR / role
1026
1170
  lookup succeeded. The admin-role-denied path (pre-IDOR) emits no audit,
1027
- 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).
1028
1175
 
1029
1176
  WS notifications (post-commit via `emit_after_commit` from
1030
1177
  `../http/pending_effects.js` — swallows exceptions so one failed send
@@ -1038,7 +1185,7 @@ can't starve others; see `../http/CLAUDE.md` §Pending Effects):
1038
1185
  - Revoke → `permit_revoke` to revokee + one `permit_offer_supersede` per
1039
1186
  superseded sibling.
1040
1187
 
1041
- 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}`.
1042
1189
  Notification sender is optional — when absent, WS fan-out is silently
1043
1190
  skipped (DB-only side effects still happen).
1044
1191
 
@@ -1141,7 +1288,7 @@ Audit events emitted (via `audit_log_fire_and_forget` with `ip: ctx.client_ip`):
1141
1288
  IP is the resolved trusted-proxy value from `ActionContext.client_ip`,
1142
1289
  matching the REST handler convention.
1143
1290
 
1144
- Deps: `AccountActionDeps = Pick<RouteFactoryDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`.
1291
+ Deps: `AccountActionDeps = AuditEmitDeps`.
1145
1292
  Options: `{max_tokens?: number | null}` — defaults to `DEFAULT_MAX_TOKENS`
1146
1293
  from `account_routes.ts`; `null` disables the cap.
1147
1294
 
@@ -1195,7 +1342,7 @@ roundtrip — then `query_grant_permit` for the actual insert. Revoke branch fil
1195
1342
  `create_standard_rpc_actions` — `eligible_roles` is app-specific, opt-in,
1196
1343
  spread alongside the standard bundle when needed.
1197
1344
 
1198
- Deps: `SelfServiceRoleActionDeps = Pick<RouteFactoryDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`.
1345
+ Deps: `SelfServiceRoleActionDeps = AuditEmitDeps`.
1199
1346
 
1200
1347
  `all_self_service_role_action_specs: ReadonlyArray<RequestResponseActionSpec>` —
1201
1348
  codegen-ready registry of the single unified spec.
@@ -1245,6 +1392,12 @@ resulting permit.
1245
1392
  - **`RouteFactoryDeps = Omit<AppDeps, 'db'>`** — for route factories. Route
1246
1393
  handlers receive DB access via `RouteContext`, so factories don't capture
1247
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.
1248
1401
 
1249
1402
  See root `../../../CLAUDE.md` §AppDeps Vocabulary for the
1250
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);