@fuzdev/fuz_app 0.110.1 → 0.112.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.
- package/dist/actions/CLAUDE.md +15 -11
- package/dist/actions/action_rpc.d.ts +3 -3
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +4 -6
- package/dist/actions/peer_ping.d.ts +8 -0
- package/dist/actions/peer_ping.d.ts.map +1 -1
- package/dist/actions/peer_ping.js +8 -0
- package/dist/actions/peer_request.d.ts +1 -1
- package/dist/actions/peer_request.js +1 -1
- package/dist/actions/perform_action.d.ts +16 -9
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +30 -21
- package/dist/actions/register_action_ws.d.ts +1 -1
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +88 -21
- package/dist/auth/CLAUDE.md +59 -10
- package/dist/auth/account_action_specs.d.ts +0 -2
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +10 -1
- package/dist/auth/account_queries.d.ts +22 -0
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +70 -16
- package/dist/auth/account_schema.d.ts +27 -9
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +23 -3
- package/dist/auth/actor_search_actions.d.ts +3 -3
- package/dist/auth/actor_search_actions.d.ts.map +1 -1
- package/dist/auth/actor_search_actions.js +10 -6
- package/dist/auth/admin_action_specs.d.ts +0 -2
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/api_token_queries.d.ts +9 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +36 -8
- package/dist/auth/app_settings_queries.d.ts +8 -0
- package/dist/auth/app_settings_queries.d.ts.map +1 -1
- package/dist/auth/app_settings_queries.js +16 -3
- package/dist/auth/audit_log_queries.d.ts +21 -3
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +38 -7
- package/dist/auth/audit_log_schema.d.ts +0 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/auth_ddl.d.ts +6 -0
- package/dist/auth/auth_ddl.d.ts.map +1 -1
- package/dist/auth/auth_ddl.js +9 -0
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +3 -8
- package/dist/auth/cell_audit_actions.d.ts.map +1 -1
- package/dist/auth/cell_audit_actions.js +1 -1
- package/dist/auth/cell_field_actions.js +1 -1
- package/dist/auth/cell_grant_actions.js +1 -1
- package/dist/auth/cell_item_actions.js +1 -1
- package/dist/auth/invite_queries.d.ts +10 -0
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +28 -7
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +10 -0
- package/dist/auth/role_grant_offer_ddl.d.ts +34 -2
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +61 -2
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +48 -53
- package/dist/auth/role_grant_queries.d.ts +18 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +58 -18
- package/dist/auth/session_queries.d.ts +12 -3
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +26 -6
- package/dist/db/CLAUDE.md +34 -0
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +3 -1
- package/dist/db/cell_field_queries.d.ts +12 -3
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +25 -7
- package/dist/db/cell_grant_queries.d.ts +10 -2
- package/dist/db/cell_grant_queries.d.ts.map +1 -1
- package/dist/db/cell_grant_queries.js +29 -11
- package/dist/db/cell_item_queries.d.ts +13 -2
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +27 -7
- package/dist/db/cell_queries.d.ts +32 -3
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +57 -22
- package/dist/db/fact_queries.d.ts +7 -0
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +27 -4
- package/dist/db/schema_ready.d.ts +6 -1
- package/dist/db/schema_ready.d.ts.map +1 -1
- package/dist/db/schema_ready.js +6 -2
- package/dist/db/sql_columns.d.ts +144 -0
- package/dist/db/sql_columns.d.ts.map +1 -0
- package/dist/db/sql_columns.js +164 -0
- package/dist/db/sql_identifier.d.ts +6 -5
- package/dist/db/sql_identifier.d.ts.map +1 -1
- package/dist/db/sql_identifier.js +6 -5
- package/dist/http/CLAUDE.md +4 -2
- package/dist/http/jsonrpc_helpers.d.ts +13 -0
- package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
- package/dist/http/jsonrpc_helpers.js +13 -0
- package/dist/http/route_spec.d.ts +2 -2
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +1 -2
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +7 -2
- package/dist/testing/CLAUDE.md +55 -28
- package/dist/testing/admin_integration.d.ts +0 -3
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +0 -1
- package/dist/testing/adversarial_404.d.ts.map +1 -1
- package/dist/testing/adversarial_404.js +4 -1
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +5 -4
- package/dist/testing/app_server.d.ts +4 -3
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -5
- package/dist/testing/attack_surface.d.ts +16 -0
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +36 -13
- package/dist/testing/audit_completeness.d.ts +0 -3
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +0 -1
- package/dist/testing/auth_apps.d.ts +9 -2
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +9 -2
- package/dist/testing/cross_backend/account_lifecycle.d.ts +5 -4
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +7 -0
- package/dist/testing/cross_backend/action_manifest.d.ts +19 -3
- package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest.js +10 -4
- package/dist/testing/cross_backend/action_manifest_parity.d.ts +9 -3
- package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest_parity.js +6 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts +3 -2
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +4 -2
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +1 -1
- package/dist/testing/cross_backend/cell_crud.d.ts +2 -2
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +10 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts +3 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.d.ts +2 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts +2 -2
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/expected_schema.json +1 -1
- package/dist/testing/cross_backend/fact_serving.d.ts +2 -2
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts +17 -2
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.js +23 -3
- package/dist/testing/cross_backend/origin.d.ts +4 -2
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +11 -7
- package/dist/testing/cross_backend/query_shape.d.ts +6 -5
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/ready.d.ts +2 -2
- package/dist/testing/cross_backend/ready.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.js +7 -1
- package/dist/testing/cross_backend/setup.d.ts +26 -6
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.d.ts +5 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +1 -5
- package/dist/testing/cross_backend/testing_backdoor.js +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +6 -0
- package/dist/testing/cross_backend/token_scope_surface.d.ts +2 -2
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/wire_shapes.d.ts +22 -0
- package/dist/testing/cross_backend/wire_shapes.d.ts.map +1 -0
- package/dist/testing/cross_backend/wire_shapes.js +41 -0
- package/dist/testing/data_exposure.d.ts +0 -3
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +0 -1
- package/dist/testing/db.d.ts +62 -7
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +86 -28
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -2
- package/dist/testing/round_trip.d.ts +0 -3
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +0 -4
- package/dist/testing/rpc_attack_surface.js +2 -2
- package/dist/testing/rpc_round_trip.d.ts +0 -3
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +0 -1
- package/dist/testing/schema_generators.d.ts +15 -0
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +45 -11
- package/dist/testing/standard.d.ts +5 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +2 -7
- package/dist/timestamp.d.ts +58 -0
- package/dist/timestamp.d.ts.map +1 -0
- package/dist/timestamp.js +57 -0
- package/dist/ui/AdminOverview.svelte +5 -10
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/RoleGrantOfferHistory.svelte +3 -1
- package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +6 -2
- package/package.json +1 -1
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -69,7 +69,7 @@ Convention — `*_schema.ts` is Zod-only; `*_ddl.ts` holds DDL strings.
|
|
|
69
69
|
- `auth/invite_schema.ts` — `Invite`, `CreateInviteInput`.
|
|
70
70
|
- `auth/app_settings_schema.ts` — `AppSettings`, `UpdateAppSettingsInput` (single-row via `CHECK (id = 1)`).
|
|
71
71
|
- `auth/role_grant_offer_schema.ts` — `RoleGrantOffer`, `RoleGrantOfferJson`, `to_role_grant_offer_json`, scope-sentinel constants.
|
|
72
|
-
- `auth/role_grant_offer_ddl.ts` — `role_grant_offer` table + indexes + `ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID` / `_GLOBAL_TOKEN
|
|
72
|
+
- `auth/role_grant_offer_ddl.ts` — `role_grant_offer` table + indexes + `ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID` / `_GLOBAL_TOKEN` + `ROLE_GRANT_OFFER_COLUMNS` and the shared `ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT` CTE tail (homed here per the placement rule in `db/sql_columns.ts`).
|
|
73
73
|
- `auth/role_grant_offer_notifications.ts` — six WS notification specs for the consentful-grant lifecycle.
|
|
74
74
|
|
|
75
75
|
### Queries
|
|
@@ -81,12 +81,35 @@ All take `deps: QueryDeps = {db}` first; `query_validate_api_token` adds `log`.
|
|
|
81
81
|
- `auth/actor_search_queries.ts` — case-insensitive prefix search on `actor.name`, scope-filtered when not admin.
|
|
82
82
|
- `auth/role_grant_queries.ts` — idempotent create, IDOR-guarded revoke (with in-tx supersede), scope-aware lookup, role/account predicates, `query_role_grant_revoke_for_scope` parent-scope cascade.
|
|
83
83
|
- `auth/role_grant_offer_queries.ts` — offer create/decline/retract/list/history/sweep, atomic `query_accept_offer` with sibling supersede; error classes `RoleGrantOfferSelfTargetError` / `_AlreadyTerminalError` / `_ExpiredError` / `_NotFoundError` / `_ActorAccountMismatchError` / `_ActorMismatchError`.
|
|
84
|
-
- `auth/session_queries.ts` — server-side sessions (blake3-hashed), `query_session_revoke_by_hash_unscoped` (logout only), `query_session_enforce_limit` (transaction-required). No touch/renewal query — `expires_at` is an absolute cap set at mint (`AUTH_SESSION_LIFETIME_MS`)
|
|
84
|
+
- `auth/session_queries.ts` — server-side sessions (blake3-hashed), `query_session_revoke_by_hash_unscoped` (logout only), `query_session_enforce_limit` (transaction-required). No touch/renewal query — `expires_at` is an absolute cap set at mint (`AUTH_SESSION_LIFETIME_MS`).
|
|
85
85
|
- `auth/api_token_queries.ts` — token validation with fire-and-forget usage tracking, IDOR-guarded revoke, `query_api_token_enforce_limit` (transaction-required).
|
|
86
86
|
- `auth/invite_queries.ts` — invite create/find/claim/list/delete; `query_invite_claim_unscoped` (scoping enforced upstream by `_find_unclaimed_match_for_update`, which runs inside the signup tx with `FOR UPDATE` so find + claim are atomic).
|
|
87
87
|
- `auth/app_settings_queries.ts` — load/update for the single-row settings table.
|
|
88
88
|
- `auth/audit_log_queries.ts` — `query_audit_log` (in-tx insert), `_list` / `_list_with_usernames` / `_list_role_grant_history` / `_cleanup_before`, drift counters (`get_audit_metadata_validation_failures` / `get_audit_unknown_event_type_failures`).
|
|
89
89
|
|
|
90
|
+
**Named projections** (rationale + helpers: ../../../docs/architecture.md
|
|
91
|
+
§Query Modules). The consts on this side: `ACCOUNT_COLUMNS` + `ACTOR_COLUMNS`
|
|
92
|
+
(`account_queries.ts` owns both tables; its admin listing derives an
|
|
93
|
+
active-grant subset from `ROLE_GRANT_COLUMNS`), `AUTH_SESSION_COLUMNS`,
|
|
94
|
+
`INVITE_COLUMNS`, `AUDIT_LOG_COLUMNS` (shared with `db/cell_audit_queries.ts`),
|
|
95
|
+
`API_TOKEN_COLUMNS` (the client listing derives `minus token_hash`),
|
|
96
|
+
`ROLE_GRANT_COLUMNS`, `ROLE_GRANT_OFFER_COLUMNS` (in `role_grant_offer_ddl.ts`),
|
|
97
|
+
`APP_SETTINGS_COLUMNS` (reads omit the constant singleton `id`; the one const
|
|
98
|
+
without a `satisfies keyof Row` clause, since `AppSettings` deliberately
|
|
99
|
+
doesn't carry `id`). Module direction: `role_grant_offer_queries.ts` imports
|
|
100
|
+
`role_grant_queries.ts` (the accept path writes through
|
|
101
|
+
`query_create_role_grant` / `query_role_grant_by_id`) and not the reverse —
|
|
102
|
+
the revoke cascades in `role_grant_queries.ts` still _write_ offer rows, which
|
|
103
|
+
is exactly why the offer const and the shared CTE tail live in the DDL module.
|
|
104
|
+
Every const is pinned to the live schema by
|
|
105
|
+
`src/test/db/column_projections.db.test.ts`. The Rust `fuz_auth` twin carries
|
|
106
|
+
the same nine consts (names and order), projected through
|
|
107
|
+
`fuz_db::qualify_columns` / `omit_columns`, decoded with compile-time
|
|
108
|
+
name→index (`fuz_db::col!`), and pinned by its own
|
|
109
|
+
`tests/columns.rs` registry; its narrow purpose rows (`AccountRow` is an
|
|
110
|
+
`id, username` pair) keep private per-shape lists checked as subsets of the
|
|
111
|
+
table const.
|
|
112
|
+
|
|
90
113
|
`_unscoped` suffix on `query_session_revoke_by_hash_unscoped` and
|
|
91
114
|
`query_invite_claim_unscoped` is the safety signal: SQL only checks row state,
|
|
92
115
|
caller is responsible for scoping. Production scoping for invites is enforced
|
|
@@ -277,7 +300,7 @@ Account-only — never loads actor or role_grants, never populates
|
|
|
277
300
|
`REQUEST_CONTEXT_KEY`.
|
|
278
301
|
|
|
279
302
|
**Authorization runs before input validation**, matching the dispatcher's
|
|
280
|
-
401 → authz → 403 → 400 phase order (see `http/CLAUDE.md` §Validation
|
|
303
|
+
401 → authz → 403 → 400 phase order — the 429 is dispatcher-only (see `http/CLAUDE.md` §Validation
|
|
281
304
|
pipeline). When the route's input declares `acting?: ActingActor` or its auth
|
|
282
305
|
requires role_grants, the authorization phase calls `resolve_acting_actor` over
|
|
283
306
|
the `acting` selector — the validated query value on GETs, read off the raw
|
|
@@ -302,6 +325,31 @@ path populates `REQUEST_CONTEXT_KEY`; it sets only `ACCOUNT_ID_KEY`,
|
|
|
302
325
|
`TEST_CONTEXT_PRESET_KEY` to bypass DB-backed actor resolution; production
|
|
303
326
|
code that reads `REQUEST_CONTEXT_KEY` is reading test escape-hatch state.
|
|
304
327
|
|
|
328
|
+
### Wire timestamp shape
|
|
329
|
+
|
|
330
|
+
Every timestamp on the wire is **second-precision UTC** —
|
|
331
|
+
`YYYY-MM-DDTHH:MM:SSZ`, exactly 20 characters, the shape both spines emit.
|
|
332
|
+
Timestamps read from a row get it from the SQL projection: each query module
|
|
333
|
+
builds an `iso8601_timestamp_expr` override over its table const and passes
|
|
334
|
+
it to `columns_sql` / `qualify_columns` (see `db/CLAUDE.md`), covering `account`, `actor`,
|
|
335
|
+
`role_grant`, `role_grant_offer`, `auth_session`, `api_token`, `invite`,
|
|
336
|
+
`app_settings`, `audit_log`, and the cell tables. Stamps minted in TS rather
|
|
337
|
+
than read back — the `expires_at` on `account_token_create`'s output — use
|
|
338
|
+
`to_iso8601_seconds` (`timestamp.ts`), so the mint and the later
|
|
339
|
+
`account_token_list` read agree byte for byte.
|
|
340
|
+
|
|
341
|
+
The `*Json` schemas type these as `z.string()`, which a millisecond stamp
|
|
342
|
+
also satisfies — so the shape is pinned behaviorally by
|
|
343
|
+
`assert_iso8601_seconds` (`testing/cross_backend/wire_shapes.ts`) inside the
|
|
344
|
+
shared suites, on both backends.
|
|
345
|
+
|
|
346
|
+
The alias costs one rule: **an `ORDER BY` in a projected SELECT qualifies
|
|
347
|
+
its column** (`ORDER BY auth_session.created_at`) — bare, Postgres resolves
|
|
348
|
+
it to the formatted output column, which ties rows written inside the same
|
|
349
|
+
second and drops the btree index. Qualified, ordering stays exact and
|
|
350
|
+
index-backed, matching the Rust twin (which aliases nothing, so its bare
|
|
351
|
+
`ORDER BY created_at` already means the column).
|
|
352
|
+
|
|
305
353
|
### Open-registry composition
|
|
306
354
|
|
|
307
355
|
Four open string registries — `RoleName`, `ScopeKindName`,
|
|
@@ -414,9 +462,10 @@ in the HTTP RPC dispatcher (`actions/action_rpc.ts`), the WS dispatcher
|
|
|
414
462
|
|
|
415
463
|
### Migrations
|
|
416
464
|
|
|
417
|
-
Schema migrations live in `auth/migrations.ts` —
|
|
418
|
-
`
|
|
419
|
-
`AUTH_MIGRATION_NAMESPACE = 'fuz_auth'
|
|
465
|
+
Schema migrations live in `auth/migrations.ts` — a frozen, append-only chain
|
|
466
|
+
(`full_auth_schema` first, every later change its own appended entry) under
|
|
467
|
+
the single reserved namespace `AUTH_MIGRATION_NAMESPACE = 'fuz_auth'`, each
|
|
468
|
+
entry twinned byte-for-byte by name on the Rust spine. Consumer namespaces must avoid
|
|
420
469
|
`reserved_migration_namespaces`. Runner contract, error vocabulary, and
|
|
421
470
|
operator recipes (rename, mark applied, reset, baseline) are in
|
|
422
471
|
../../../docs/migrations.md.
|
|
@@ -492,15 +541,15 @@ authenticate.
|
|
|
492
541
|
- `account_delete_action_spec` — mutation (soft delete); self-or-admin (`{account, actor}`, handler elevates to admin when `account_id` ≠ self); input `{account_id?}`; output `{ok, deleted}`. Tombstones account + actor(s), revokes sessions/tokens, emits `account_delete` + per-actor `actor_delete`. Refuses a keeper-role target (`ERROR_CANNOT_DELETE_KEEPER`) or the sole active admin (`ERROR_CANNOT_DELETE_LAST_ADMIN`) — both fail-loud with a failure-audit row.
|
|
493
542
|
- `account_purge_action_spec` — mutation (hard purge); keeper-only (`credential_types: ['daemon_token']` + `roles: ['keeper']`) + `confirm: true` + WARN; input `{account_id, confirm?}`; output `{ok, purged}`. Cascading delete; emits `account_purge` + per-actor `actor_purge` (identity snapshot survives in metadata since `audit_log` ids carry no FK). Refuses a keeper-role target (`ERROR_CANNOT_DELETE_KEEPER`) or the sole active admin (`ERROR_CANNOT_DELETE_LAST_ADMIN`).
|
|
494
543
|
- `account_undelete_action_spec` — mutation (reactivation, inverse of soft delete); admin-only (`roles: ['admin']` — no self path, a tombstoned account can't authenticate); input `{account_id}`; output `{ok, undeleted}`. Clears the `deleted_at` tombstone on the account + its actor(s), emits `account_undelete` + per-actor `actor_undelete`. Does not restore revoked sessions/tokens.
|
|
495
|
-
- `admin_session_list_action_spec` — read; input `
|
|
544
|
+
- `admin_session_list_action_spec` — read; input `{acting?}` (`.default({})`); output `{sessions}`.
|
|
496
545
|
- `admin_session_revoke_all_action_spec` — mutation; input `{account_id}`; output `{ok, count}`.
|
|
497
546
|
- `admin_token_revoke_all_action_spec` — mutation; input `{account_id}`; output `{ok, count}`.
|
|
498
|
-
- `audit_log_list_action_spec` — read; input `{event_type?, account_id?, limit?, offset?, since_seq?}`; output `{events}`.
|
|
547
|
+
- `audit_log_list_action_spec` — read; input `{event_type?, outcome?, account_id?, limit?, offset?, since_seq?}`; output `{events}`.
|
|
499
548
|
- `audit_log_role_grant_history_action_spec` — read; input `{limit?, offset?}`; output `{events}`.
|
|
500
549
|
- `invite_create_action_spec` — mutation; input `{email?, username?}`; output `{ok, invite}`.
|
|
501
|
-
- `invite_list_action_spec` — read; input `
|
|
550
|
+
- `invite_list_action_spec` — read; input `{acting?}` (`.default({})`); output `{invites}`.
|
|
502
551
|
- `invite_delete_action_spec` — mutation; input `{invite_id}`; output `{ok}`.
|
|
503
|
-
- `app_settings_get_action_spec` — read; input `
|
|
552
|
+
- `app_settings_get_action_spec` — read; input `{acting?}` (`.default({})`); output `{settings}`.
|
|
504
553
|
- `app_settings_update_action_spec` — mutation; input `{open_signup}`; output `{ok, settings}`.
|
|
505
554
|
|
|
506
555
|
Constants: `AUDIT_LOG_LIST_LIMIT_MAX = 200`, `ADMIN_ACCOUNT_LIST_DEFAULT_LIMIT = 50`,
|
|
@@ -22,7 +22,6 @@ export declare const SessionListOutput: z.ZodObject<{
|
|
|
22
22
|
account_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
23
23
|
created_at: z.ZodString;
|
|
24
24
|
expires_at: z.ZodString;
|
|
25
|
-
last_seen_at: z.ZodString;
|
|
26
25
|
}, z.core.$strict>>;
|
|
27
26
|
}, z.core.$strict>;
|
|
28
27
|
export type SessionListOutput = z.infer<typeof SessionListOutput>;
|
|
@@ -149,7 +148,6 @@ export declare const account_session_list_action_spec: {
|
|
|
149
148
|
account_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
150
149
|
created_at: z.ZodString;
|
|
151
150
|
expires_at: z.ZodString;
|
|
152
|
-
last_seen_at: z.ZodString;
|
|
153
151
|
}, z.core.$strict>>;
|
|
154
152
|
}, z.core.$strict>;
|
|
155
153
|
async: true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAW3E,6EAA6E;AAC7E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,WAAW,CAAC;AACzC,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,yCAAyC;AACzC,eAAO,MAAM,iBAAiB
|
|
1
|
+
{"version":3,"file":"account_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_action_specs.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAqBxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAqBxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAyBnD,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,iBAAiB,EACvB,UAAS,oBAAyB,KAChC,KAAK,CAAC,SAAS,CAwKjB,CAAC"}
|
|
@@ -30,6 +30,7 @@ import { parse_stored_token_scope, token_scope_from_input, token_scope_label } f
|
|
|
30
30
|
import { token_lifetime_to_expires_at } from './token_lifetime.js';
|
|
31
31
|
import { generate_api_token } from './api_token.js';
|
|
32
32
|
import { DEFAULT_MAX_TOKENS } from './account_route_schema.js';
|
|
33
|
+
import { to_iso8601_seconds } from '../timestamp.js';
|
|
33
34
|
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';
|
|
34
35
|
/**
|
|
35
36
|
* Create the self-service account RPC actions.
|
|
@@ -120,7 +121,15 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
120
121
|
credential_type: ctx.credential_type ?? undefined
|
|
121
122
|
}
|
|
122
123
|
});
|
|
123
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
token,
|
|
127
|
+
id,
|
|
128
|
+
name: input.name,
|
|
129
|
+
// the canonical wire shape, matching what `token_list` reads back
|
|
130
|
+
// through `api_token`'s ISO-8601 projection
|
|
131
|
+
expires_at: expires_at === null ? null : to_iso8601_seconds(expires_at)
|
|
132
|
+
};
|
|
124
133
|
};
|
|
125
134
|
const token_list_handler = async (_input, ctx) => {
|
|
126
135
|
const rows = await query_api_token_list_for_account(ctx, ctx.auth.account.id);
|
|
@@ -8,6 +8,28 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
10
10
|
import { type Account, type Actor, type CreateAccountInput, type AdminAccountEntryJson } from './account_schema.js';
|
|
11
|
+
/**
|
|
12
|
+
* The full `account` column set, named explicitly so a row read fails loud
|
|
13
|
+
* on schema drift.
|
|
14
|
+
*
|
|
15
|
+
* `SELECT *` silently omits a dropped column, which the login lookups then
|
|
16
|
+
* misread: `query_account_by_username_or_email` filters its result with
|
|
17
|
+
* `account.deleted_at === null`, so a missing `deleted_at` column reads back
|
|
18
|
+
* as `undefined`, `undefined === null` is `false`, and *every* login resolves
|
|
19
|
+
* to "not found" (401) — a silent, total auth outage instead of an error.
|
|
20
|
+
* Selecting named columns turns that drift into a hard Postgres
|
|
21
|
+
* `column "..." does not exist`. Mirrors the Rust side
|
|
22
|
+
* (`fuz_auth/src/account_queries.rs`), which selects named columns and
|
|
23
|
+
* decodes them positionally. Keep in sync with `Account` and the `account`
|
|
24
|
+
* DDL in `auth/auth_ddl.ts`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const ACCOUNT_COLUMNS: readonly ["id", "username", "email", "email_verified", "password_hash", "created_at", "created_by", "updated_at", "updated_by", "deleted_at", "deleted_by"];
|
|
27
|
+
/**
|
|
28
|
+
* The full `actor` column set — the same fail-loud discipline as
|
|
29
|
+
* `ACCOUNT_COLUMNS` (this module owns both tables). Keep in sync with
|
|
30
|
+
* `Actor` and the `actor` DDL in `auth/auth_ddl.ts`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const ACTOR_COLUMNS: readonly ["id", "account_id", "name", "created_at", "updated_at", "updated_by", "deleted_at", "deleted_by"];
|
|
11
33
|
/**
|
|
12
34
|
* Create a new account.
|
|
13
35
|
*
|
|
@@ -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,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"account_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { assert_row } from '../db/assert_row.js';
|
|
10
|
+
import { columns_sql, iso8601_timestamp_column, iso8601_timestamp_expr, omit_columns } from '../db/sql_columns.js';
|
|
10
11
|
import { to_admin_account } from './account_schema.js';
|
|
11
12
|
import { ADMIN_ACCOUNT_LIST_DEFAULT_LIMIT } from './admin_action_specs.js';
|
|
13
|
+
import { ROLE_GRANT_COLUMNS } from './role_grant_queries.js';
|
|
12
14
|
/**
|
|
13
15
|
* The full `account` column set, named explicitly so a row read fails loud
|
|
14
16
|
* on schema drift.
|
|
@@ -24,7 +26,46 @@ import { ADMIN_ACCOUNT_LIST_DEFAULT_LIMIT } from './admin_action_specs.js';
|
|
|
24
26
|
* decodes them positionally. Keep in sync with `Account` and the `account`
|
|
25
27
|
* DDL in `auth/auth_ddl.ts`.
|
|
26
28
|
*/
|
|
27
|
-
const ACCOUNT_COLUMNS =
|
|
29
|
+
export const ACCOUNT_COLUMNS = [
|
|
30
|
+
'id',
|
|
31
|
+
'username',
|
|
32
|
+
'email',
|
|
33
|
+
'email_verified',
|
|
34
|
+
'password_hash',
|
|
35
|
+
'created_at',
|
|
36
|
+
'created_by',
|
|
37
|
+
'updated_at',
|
|
38
|
+
'updated_by',
|
|
39
|
+
'deleted_at',
|
|
40
|
+
'deleted_by'
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* The full `actor` column set — the same fail-loud discipline as
|
|
44
|
+
* `ACCOUNT_COLUMNS` (this module owns both tables). Keep in sync with
|
|
45
|
+
* `Actor` and the `actor` DDL in `auth/auth_ddl.ts`.
|
|
46
|
+
*/
|
|
47
|
+
export const ACTOR_COLUMNS = [
|
|
48
|
+
'id',
|
|
49
|
+
'account_id',
|
|
50
|
+
'name',
|
|
51
|
+
'created_at',
|
|
52
|
+
'updated_at',
|
|
53
|
+
'updated_by',
|
|
54
|
+
'deleted_at',
|
|
55
|
+
'deleted_by'
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* The timestamp override both tables this module owns share (`ACTOR_COLUMNS`
|
|
59
|
+
* carries the same triple); every read here is on the bare table. Actor
|
|
60
|
+
* timestamps never reach the wire (the summaries carry `{id, name}` only —
|
|
61
|
+
* see `actor_lookup_action_specs.ts` on timing-oracle avoidance), but `Actor`
|
|
62
|
+
* declares them as strings, so the projection makes the row honest there too.
|
|
63
|
+
*/
|
|
64
|
+
const account_actor_expr = iso8601_timestamp_expr(ACCOUNT_COLUMNS, [
|
|
65
|
+
'created_at',
|
|
66
|
+
'updated_at',
|
|
67
|
+
'deleted_at'
|
|
68
|
+
])('');
|
|
28
69
|
/**
|
|
29
70
|
* Create a new account.
|
|
30
71
|
*
|
|
@@ -36,7 +77,7 @@ const ACCOUNT_COLUMNS = 'id, username, email, email_verified, password_hash, cre
|
|
|
36
77
|
export const query_create_account = async (deps, input) => {
|
|
37
78
|
const row = await deps.db.query_one(`INSERT INTO account (username, password_hash, email)
|
|
38
79
|
VALUES ($1, $2, $3)
|
|
39
|
-
RETURNING
|
|
80
|
+
RETURNING ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)}`, [input.username, input.password_hash, input.email ?? null]);
|
|
40
81
|
return assert_row(row, 'INSERT INTO account');
|
|
41
82
|
};
|
|
42
83
|
/**
|
|
@@ -49,19 +90,19 @@ export const query_create_account = async (deps, input) => {
|
|
|
49
90
|
* soft-deleted rows too, uses `query_purge_account` directly.
|
|
50
91
|
*/
|
|
51
92
|
export const query_account_by_id = async (deps, id) => {
|
|
52
|
-
return deps.db.query_one(`SELECT ${ACCOUNT_COLUMNS} FROM account WHERE id = $1 AND deleted_at IS NULL`, [id]);
|
|
93
|
+
return deps.db.query_one(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account WHERE id = $1 AND deleted_at IS NULL`, [id]);
|
|
53
94
|
};
|
|
54
95
|
/**
|
|
55
96
|
* Find an account by username (case-insensitive).
|
|
56
97
|
*/
|
|
57
98
|
export const query_account_by_username = async (deps, username) => {
|
|
58
|
-
return deps.db.query_one(`SELECT ${ACCOUNT_COLUMNS} FROM account WHERE LOWER(username) = LOWER($1)`, [username]);
|
|
99
|
+
return deps.db.query_one(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account WHERE LOWER(username) = LOWER($1)`, [username]);
|
|
59
100
|
};
|
|
60
101
|
/**
|
|
61
102
|
* Find an account by email (case-insensitive).
|
|
62
103
|
*/
|
|
63
104
|
export const query_account_by_email = async (deps, email) => {
|
|
64
|
-
return deps.db.query_one(`SELECT ${ACCOUNT_COLUMNS} FROM account WHERE LOWER(email) = LOWER($1)`, [email]);
|
|
105
|
+
return deps.db.query_one(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account WHERE LOWER(email) = LOWER($1)`, [email]);
|
|
65
106
|
};
|
|
66
107
|
/**
|
|
67
108
|
* Find an account by username or email.
|
|
@@ -214,7 +255,7 @@ export const query_account_has_any = async (deps) => {
|
|
|
214
255
|
* @mutates `actor` table - inserts the new row
|
|
215
256
|
*/
|
|
216
257
|
export const query_create_actor = async (deps, account_id, name) => {
|
|
217
|
-
const row = await deps.db.query_one(`INSERT INTO actor (account_id, name) VALUES ($1, $2) RETURNING
|
|
258
|
+
const row = await deps.db.query_one(`INSERT INTO actor (account_id, name) VALUES ($1, $2) RETURNING ${columns_sql(ACTOR_COLUMNS, account_actor_expr)}`, [account_id, name]);
|
|
218
259
|
return assert_row(row, 'INSERT INTO actor');
|
|
219
260
|
};
|
|
220
261
|
/**
|
|
@@ -229,7 +270,7 @@ export const query_create_actor = async (deps, account_id, name) => {
|
|
|
229
270
|
* `query_actor_by_id` instead.
|
|
230
271
|
*/
|
|
231
272
|
export const query_actors_by_account = async (deps, account_id) => {
|
|
232
|
-
return deps.db.query(`SELECT
|
|
273
|
+
return deps.db.query(`SELECT ${columns_sql(ACTOR_COLUMNS, account_actor_expr)} FROM actor WHERE account_id = $1 ORDER BY actor.created_at ASC, actor.id ASC`, [account_id]);
|
|
233
274
|
};
|
|
234
275
|
/**
|
|
235
276
|
* List **active** (non-tombstoned) actors on an account, ordered by
|
|
@@ -243,13 +284,13 @@ export const query_actors_by_account = async (deps, account_id) => {
|
|
|
243
284
|
* tombstoned rows stay on the unfiltered `query_actors_by_account`.
|
|
244
285
|
*/
|
|
245
286
|
export const query_active_actors_by_account = async (deps, account_id) => {
|
|
246
|
-
return deps.db.query(`SELECT
|
|
287
|
+
return deps.db.query(`SELECT ${columns_sql(ACTOR_COLUMNS, account_actor_expr)} FROM actor WHERE account_id = $1 AND deleted_at IS NULL ORDER BY actor.created_at ASC, actor.id ASC`, [account_id]);
|
|
247
288
|
};
|
|
248
289
|
/**
|
|
249
290
|
* Find an actor by id.
|
|
250
291
|
*/
|
|
251
292
|
export const query_actor_by_id = async (deps, id) => {
|
|
252
|
-
return deps.db.query_one(`SELECT
|
|
293
|
+
return deps.db.query_one(`SELECT ${columns_sql(ACTOR_COLUMNS, account_actor_expr)} FROM actor WHERE id = $1`, [id]);
|
|
253
294
|
};
|
|
254
295
|
/**
|
|
255
296
|
* Create an account and its actor in a single operation.
|
|
@@ -266,6 +307,19 @@ export const query_create_account_with_actor = async (deps, input) => {
|
|
|
266
307
|
const actor = await query_create_actor(deps, account.id, input.username);
|
|
267
308
|
return { account, actor };
|
|
268
309
|
};
|
|
310
|
+
/**
|
|
311
|
+
* `ROLE_GRANT_COLUMNS` minus the revocation + provenance columns — the
|
|
312
|
+
* active-grant summary the admin listing projects (`RoleGrantWithActorId`).
|
|
313
|
+
*/
|
|
314
|
+
const ADMIN_ROLE_GRANT_COLUMNS = omit_columns(ROLE_GRANT_COLUMNS, 'revoked_at', 'revoked_by', 'revoked_reason', 'source_offer_id');
|
|
315
|
+
/**
|
|
316
|
+
* The admin listing's active-grant summary timestamps — twin of the spine's
|
|
317
|
+
* `role_grant_summary_columns`. The read is on the bare `role_grant` table.
|
|
318
|
+
*/
|
|
319
|
+
const admin_role_grant_expr = iso8601_timestamp_expr(ADMIN_ROLE_GRANT_COLUMNS, [
|
|
320
|
+
'created_at',
|
|
321
|
+
'expires_at'
|
|
322
|
+
])('');
|
|
269
323
|
/**
|
|
270
324
|
* List accounts with their actors, active role_grants, and pending inbound
|
|
271
325
|
* role_grant offers for admin display.
|
|
@@ -291,23 +345,23 @@ export const query_admin_account_list = async (deps, options) => {
|
|
|
291
345
|
// `include_deleted` to surface tombstoned rows for reactivation.
|
|
292
346
|
const where = options?.include_deleted ? '' : 'WHERE deleted_at IS NULL';
|
|
293
347
|
const account_query = limit == null
|
|
294
|
-
? deps.db.query(`SELECT
|
|
295
|
-
|
|
296
|
-
])
|
|
297
|
-
: deps.db.query(`SELECT * FROM account ${where} ORDER BY created_at LIMIT $1 OFFSET $2`, [limit, offset]);
|
|
348
|
+
? deps.db.query(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account ${where} ORDER BY account.created_at OFFSET $1`, [offset])
|
|
349
|
+
: deps.db.query(`SELECT ${columns_sql(ACCOUNT_COLUMNS, account_actor_expr)} FROM account ${where} ORDER BY account.created_at LIMIT $1 OFFSET $2`, [limit, offset]);
|
|
298
350
|
const accounts = await account_query;
|
|
299
351
|
if (accounts.length === 0)
|
|
300
352
|
return [];
|
|
301
353
|
const account_ids = accounts.map((a) => a.id);
|
|
302
354
|
const [actors, role_grants, pending_offers] = await Promise.all([
|
|
303
|
-
deps.db.query(`SELECT
|
|
304
|
-
deps.db.query(`SELECT
|
|
355
|
+
deps.db.query(`SELECT ${columns_sql(ACTOR_COLUMNS, account_actor_expr)} FROM actor WHERE account_id = ANY($1::uuid[])`, [account_ids]),
|
|
356
|
+
deps.db.query(`SELECT ${columns_sql(ADMIN_ROLE_GRANT_COLUMNS, admin_role_grant_expr)}
|
|
305
357
|
FROM role_grant
|
|
306
358
|
WHERE actor_id IN (SELECT id FROM actor WHERE account_id = ANY($1::uuid[]))
|
|
307
359
|
AND revoked_at IS NULL
|
|
308
360
|
AND (expires_at IS NULL OR expires_at > NOW())`, [account_ids]),
|
|
309
361
|
deps.db.query(`SELECT po.id, po.to_account_id, po.from_actor_id, po.role, po.scope_kind, po.scope_id,
|
|
310
|
-
po
|
|
362
|
+
${iso8601_timestamp_column('po', 'created_at')} AS created_at,
|
|
363
|
+
${iso8601_timestamp_column('po', 'expires_at')} AS expires_at,
|
|
364
|
+
a.username AS from_username
|
|
311
365
|
FROM role_grant_offer po
|
|
312
366
|
JOIN actor act ON act.id = po.from_actor_id
|
|
313
367
|
JOIN account a ON a.id = act.account_id
|
|
@@ -97,6 +97,26 @@ export interface RoleGrant {
|
|
|
97
97
|
/** Offer that produced this role_grant (set by `query_accept_offer`). `null` for direct grants. */
|
|
98
98
|
source_offer_id: Uuid | null;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Whether a role grant is live, from its wire fields — not revoked, and not
|
|
102
|
+
* past `expires_at`.
|
|
103
|
+
*
|
|
104
|
+
* Server-side this is belt-and-suspenders: every `role_grant` a request
|
|
105
|
+
* context holds came through `query_role_grant_find_active_for_actor`, whose
|
|
106
|
+
* `revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW())` is the
|
|
107
|
+
* authoritative gate on both spines (the Rust twin's in-memory
|
|
108
|
+
* `RoleGrantRow` carries no timestamps at all and re-checks nothing).
|
|
109
|
+
*
|
|
110
|
+
* `expires_at` arrives second-truncated, so the expiry check is
|
|
111
|
+
* `is_iso8601_seconds_live` (`timestamp.ts`): it admits through the end of
|
|
112
|
+
* the second the stamp names, so the predicate can never report a grant
|
|
113
|
+
* expired *before* the SQL gate would — a bare `expires_at > now` would deny
|
|
114
|
+
* an admitted grant for up to a second.
|
|
115
|
+
*
|
|
116
|
+
* @param p - the grant's revocation + expiry fields
|
|
117
|
+
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
118
|
+
* @returns `true` when the grant is neither revoked nor expired
|
|
119
|
+
*/
|
|
100
120
|
export declare const is_role_grant_active: (p: {
|
|
101
121
|
revoked_at?: string | null;
|
|
102
122
|
expires_at: string | null;
|
|
@@ -112,14 +132,13 @@ export declare const is_role_grant_active: (p: {
|
|
|
112
132
|
*/
|
|
113
133
|
export declare const SessionId: z.core.$ZodBranded<z.ZodString, "SessionId", "out">;
|
|
114
134
|
export type SessionId = z.infer<typeof SessionId>;
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
135
|
+
/**
|
|
136
|
+
* Server-side auth session row, keyed by blake3 hash of the session token.
|
|
137
|
+
* Structurally identical to the wire shape `AuthSessionJson` — kept as the
|
|
138
|
+
* DB-row name so query signatures keep saying which side of the boundary
|
|
139
|
+
* they read.
|
|
140
|
+
*/
|
|
141
|
+
export type AuthSession = AuthSessionJson;
|
|
123
142
|
/** API token for CLI/programmatic access. */
|
|
124
143
|
export interface ApiToken {
|
|
125
144
|
id: string;
|
|
@@ -152,7 +171,6 @@ export declare const AuthSessionJson: z.ZodObject<{
|
|
|
152
171
|
account_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
153
172
|
created_at: z.ZodString;
|
|
154
173
|
expires_at: z.ZodString;
|
|
155
|
-
last_seen_at: z.ZodString;
|
|
156
174
|
}, z.core.$strict>;
|
|
157
175
|
export type AuthSessionJson = z.infer<typeof AuthSessionJson>;
|
|
158
176
|
/** Zod schema for client-safe API token listing (excludes `token_hash`). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAG9C,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"account_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,6 +20,7 @@ import { z } from 'zod';
|
|
|
20
20
|
import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
21
21
|
import { Blake3Hash } from '@fuzdev/fuz_util/hash_schemas.ts';
|
|
22
22
|
import { Username, Email } from '../primitive_schemas.js';
|
|
23
|
+
import { is_iso8601_seconds_live } from '../timestamp.js';
|
|
23
24
|
/**
|
|
24
25
|
* Maximum length of the optional free-form `revoked_reason` attached to a
|
|
25
26
|
* revoked role_grant. Bounds the value at the schema layer so both the admin
|
|
@@ -27,7 +28,27 @@ import { Username, Email } from '../primitive_schemas.js';
|
|
|
27
28
|
* `role_grant_revoke` WS notification validate against the same ceiling.
|
|
28
29
|
*/
|
|
29
30
|
export const ROLE_GRANT_REVOKED_REASON_LENGTH_MAX = 500;
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Whether a role grant is live, from its wire fields — not revoked, and not
|
|
33
|
+
* past `expires_at`.
|
|
34
|
+
*
|
|
35
|
+
* Server-side this is belt-and-suspenders: every `role_grant` a request
|
|
36
|
+
* context holds came through `query_role_grant_find_active_for_actor`, whose
|
|
37
|
+
* `revoked_at IS NULL AND (expires_at IS NULL OR expires_at > NOW())` is the
|
|
38
|
+
* authoritative gate on both spines (the Rust twin's in-memory
|
|
39
|
+
* `RoleGrantRow` carries no timestamps at all and re-checks nothing).
|
|
40
|
+
*
|
|
41
|
+
* `expires_at` arrives second-truncated, so the expiry check is
|
|
42
|
+
* `is_iso8601_seconds_live` (`timestamp.ts`): it admits through the end of
|
|
43
|
+
* the second the stamp names, so the predicate can never report a grant
|
|
44
|
+
* expired *before* the SQL gate would — a bare `expires_at > now` would deny
|
|
45
|
+
* an admitted grant for up to a second.
|
|
46
|
+
*
|
|
47
|
+
* @param p - the grant's revocation + expiry fields
|
|
48
|
+
* @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
|
|
49
|
+
* @returns `true` when the grant is neither revoked nor expired
|
|
50
|
+
*/
|
|
51
|
+
export const is_role_grant_active = (p, now = new Date()) => !p.revoked_at && (!p.expires_at || is_iso8601_seconds_live(p.expires_at, now.getTime()));
|
|
31
52
|
/**
|
|
32
53
|
* A session's storage key — the blake3 hash of its raw token, branded so a
|
|
33
54
|
* bare `string` can't stand in for one. `Blake3Hash` (`hash_schemas.ts`) is
|
|
@@ -52,8 +73,7 @@ export const AuthSessionJson = z.strictObject({
|
|
|
52
73
|
id: SessionId,
|
|
53
74
|
account_id: Uuid,
|
|
54
75
|
created_at: z.string(),
|
|
55
|
-
expires_at: z.string()
|
|
56
|
-
last_seen_at: z.string()
|
|
76
|
+
expires_at: z.string()
|
|
57
77
|
});
|
|
58
78
|
/** Zod schema for client-safe API token listing (excludes `token_hash`). */
|
|
59
79
|
export const ClientApiTokenJson = z.strictObject({
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* - **Admin gate on empty `scope_ids`** — unbounded global search is
|
|
12
12
|
* admin-only. Non-admin callers without a `scope_ids` filter are
|
|
13
13
|
* rejected with `invalid_params` carrying `actor_search_scope_required`.
|
|
14
|
-
* The admin check is account-grain (any actor on the caller's
|
|
15
|
-
* holds a global `admin` role_grant) since the `actor: 'none'`
|
|
16
|
-
* doesn't load `auth.role_grants` for an in-memory check.
|
|
14
|
+
* The admin check is account-grain (any **active** actor on the caller's
|
|
15
|
+
* account holds a global `admin` role_grant) since the `actor: 'none'`
|
|
16
|
+
* posture doesn't load `auth.role_grants` for an in-memory check.
|
|
17
17
|
* - **Limit clamp** — input is bounded by `ACTOR_SEARCH_LIMIT_MAX` at
|
|
18
18
|
* the schema; the handler picks the default when omitted.
|
|
19
19
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actor_search_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/actor_search_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAc9F,sDAAsD;AACtD,MAAM,WAAW,qBAAqB;IACrC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,eAAO,MAAM,2BAA2B,GAAI,OAAO,qBAAqB,KAAG,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"actor_search_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/actor_search_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAc9F,sDAAsD;AACtD,MAAM,WAAW,qBAAqB;IACrC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,eAAO,MAAM,2BAA2B,GAAI,OAAO,qBAAqB,KAAG,KAAK,CAAC,SAAS,CAyCzF,CAAC"}
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* - **Admin gate on empty `scope_ids`** — unbounded global search is
|
|
12
12
|
* admin-only. Non-admin callers without a `scope_ids` filter are
|
|
13
13
|
* rejected with `invalid_params` carrying `actor_search_scope_required`.
|
|
14
|
-
* The admin check is account-grain (any actor on the caller's
|
|
15
|
-
* holds a global `admin` role_grant) since the `actor: 'none'`
|
|
16
|
-
* doesn't load `auth.role_grants` for an in-memory check.
|
|
14
|
+
* The admin check is account-grain (any **active** actor on the caller's
|
|
15
|
+
* account holds a global `admin` role_grant) since the `actor: 'none'`
|
|
16
|
+
* posture doesn't load `auth.role_grants` for an in-memory check.
|
|
17
17
|
* - **Limit clamp** — input is bounded by `ACTOR_SEARCH_LIMIT_MAX` at
|
|
18
18
|
* the schema; the handler picks the default when omitted.
|
|
19
19
|
*
|
|
@@ -26,15 +26,19 @@
|
|
|
26
26
|
import { jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
27
27
|
import { rpc_action } from '../actions/action_rpc.js';
|
|
28
28
|
import { query_actor_search } from './actor_search_queries.js';
|
|
29
|
-
import {
|
|
29
|
+
import { query_account_has_active_global_role } from './role_grant_queries.js';
|
|
30
30
|
import { ROLE_ADMIN } from './role_schema.js';
|
|
31
31
|
import { ACTOR_SEARCH_LIMIT_DEFAULT, ERROR_ACTOR_SEARCH_SCOPE_REQUIRED, actor_search_action_spec } from './actor_search_action_specs.js';
|
|
32
32
|
export const create_actor_search_actions = (_deps) => {
|
|
33
33
|
const handler = async (input, ctx) => {
|
|
34
34
|
if (!input.scope_ids || input.scope_ids.length === 0) {
|
|
35
35
|
// Unbounded global search is admin-only. Account-grain admin
|
|
36
|
-
// check — any actor on the caller's account holds the
|
|
37
|
-
|
|
36
|
+
// check — any *active* actor on the caller's account holds the
|
|
37
|
+
// role. The tombstone-aware query is what every admission gate
|
|
38
|
+
// reads: soft-delete leaves `role_grant` rows intact (undelete
|
|
39
|
+
// restores them), so the blind sibling would admit an account
|
|
40
|
+
// whose only admin grant hangs off a tombstoned actor.
|
|
41
|
+
const is_admin = await query_account_has_active_global_role(ctx, ctx.auth.account.id, ROLE_ADMIN);
|
|
38
42
|
if (!is_admin) {
|
|
39
43
|
throw jsonrpc_errors.invalid_params('scope_ids required for non-admin callers', {
|
|
40
44
|
reason: ERROR_ACTOR_SEARCH_SCOPE_REQUIRED
|
|
@@ -83,7 +83,6 @@ export declare const AdminSessionListOutput: z.ZodObject<{
|
|
|
83
83
|
account_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
84
84
|
created_at: z.ZodString;
|
|
85
85
|
expires_at: z.ZodString;
|
|
86
|
-
last_seen_at: z.ZodString;
|
|
87
86
|
username: z.ZodString;
|
|
88
87
|
}, z.core.$strict>>;
|
|
89
88
|
}, z.core.$strict>;
|
|
@@ -429,7 +428,6 @@ export declare const admin_session_list_action_spec: {
|
|
|
429
428
|
account_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
|
|
430
429
|
created_at: z.ZodString;
|
|
431
430
|
expires_at: z.ZodString;
|
|
432
|
-
last_seen_at: z.ZodString;
|
|
433
431
|
username: z.ZodString;
|
|
434
432
|
}, z.core.$strict>>;
|
|
435
433
|
}, z.core.$strict>;
|