@fuzdev/fuz_app 0.65.0 → 0.66.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 +65 -86
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.js +1 -1
- package/dist/actions/action_event_data.d.ts +1 -1
- package/dist/auth/CLAUDE.md +83 -104
- package/dist/auth/audit_log_schema.js +2 -2
- package/dist/auth/daemon_token_middleware.d.ts +15 -5
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +24 -15
- package/dist/auth/invite_queries.d.ts +17 -7
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +19 -8
- package/dist/auth/signup_routes.d.ts +47 -1
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +103 -52
- package/dist/env/resolve.d.ts +44 -7
- package/dist/env/resolve.d.ts.map +1 -1
- package/dist/env/resolve.js +94 -27
- package/dist/http/CLAUDE.md +47 -52
- package/dist/http/jsonrpc.d.ts +23 -7
- package/dist/http/jsonrpc.d.ts.map +1 -1
- package/dist/http/jsonrpc.js +19 -3
- package/dist/http/surface.d.ts +9 -2
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +1 -1
- package/dist/testing/CLAUDE.md +659 -511
- package/dist/testing/admin_integration.d.ts +5 -5
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +95 -39
- package/dist/testing/app_server.d.ts +16 -1
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +18 -3
- package/dist/testing/audit_completeness.d.ts +7 -5
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +5 -9
- package/dist/testing/bootstrap_success.js +2 -2
- package/dist/testing/cross_backend/backend_config.d.ts +113 -0
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/backend_config.js +1 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
- package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/bench_report.js +83 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
- package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
- package/dist/testing/cross_backend/bench/scenario.js +28 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
- package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
- package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
- package/dist/testing/cross_backend/capabilities.d.ts +3 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_backend_configs.js +111 -0
- package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
- package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_secrets.js +39 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/default_spine_surface.js +121 -0
- package/dist/testing/cross_backend/setup.d.ts +270 -34
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +495 -15
- package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
- package/dist/testing/cross_backend/spawn_backend.js +229 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/sse_round_trip.js +137 -0
- package/dist/testing/cross_backend/standard.d.ts +96 -0
- package/dist/testing/cross_backend/standard.d.ts.map +1 -0
- package/dist/testing/cross_backend/standard.js +49 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_bun.js +59 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_core.js +68 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_deno.js +37 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
- package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
- package/dist/testing/cross_backend/testing_server_node.js +50 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_round_trip.js +113 -0
- package/dist/testing/data_exposure.d.ts +4 -6
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +1 -5
- package/dist/testing/db_entities.d.ts +18 -7
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +18 -7
- package/dist/testing/integration.d.ts +27 -6
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +93 -58
- package/dist/testing/round_trip.d.ts +4 -5
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +1 -5
- package/dist/testing/rpc_helpers.d.ts +10 -4
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +1 -1
- package/dist/testing/rpc_round_trip.d.ts +5 -5
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +1 -5
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -68
- package/dist/testing/standard.d.ts +4 -5
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +10 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +9 -2
- package/dist/testing/testing_rate_limiter.d.ts +59 -0
- package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
- package/dist/testing/testing_rate_limiter.js +74 -0
- package/dist/testing/transports/bootstrap.d.ts +52 -0
- package/dist/testing/transports/bootstrap.d.ts.map +1 -0
- package/dist/testing/transports/bootstrap.js +70 -0
- package/dist/testing/transports/fetch_transport.d.ts +81 -0
- package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
- package/dist/testing/transports/fetch_transport.js +74 -0
- package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
- package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
- package/dist/testing/transports/sse_frame_reader.js +84 -0
- package/dist/testing/transports/sse_transport.d.ts +54 -0
- package/dist/testing/transports/sse_transport.d.ts.map +1 -0
- package/dist/testing/transports/sse_transport.js +51 -0
- package/dist/testing/transports/ws_client.d.ts +108 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_client.js +56 -0
- package/dist/testing/transports/ws_transport.d.ts +43 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -0
- package/dist/testing/transports/ws_transport.js +169 -0
- package/dist/testing/ws_round_trip.d.ts +21 -103
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +42 -40
- package/dist/ui/CLAUDE.md +5 -3
- package/dist/ui/MenuLink.svelte +16 -16
- package/dist/ui/MenuLink.svelte.d.ts +13 -4
- package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
- package/package.json +7 -1
- package/dist/testing/transports/surface_source.d.ts +0 -51
- package/dist/testing/transports/surface_source.d.ts.map +0 -1
- package/dist/testing/transports/surface_source.js +0 -19
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -16,13 +16,11 @@ documents the cross-cutting invariants that don't fit on any single symbol.
|
|
|
16
16
|
|
|
17
17
|
## AppDeps split
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
| **Parameters** | `*Options` | Static startup values, per-factory |
|
|
25
|
-
| **Runtime state** | inline ref | Mutable values: `bootstrap_status`, `app_settings` ref, `DaemonTokenState` — NOT in deps or options |
|
|
19
|
+
- **Capabilities** — `AppDeps` — stateless, injectable per env: `stat`, `read_text_file`, `delete_file`, `keyring`, `password`, `db`, `log`, `audit`.
|
|
20
|
+
- **Route caps** — `RouteFactoryDeps` — `Omit<AppDeps, 'db'>`; handlers get `db` via `RouteContext`.
|
|
21
|
+
- **Action caps** — inline — action factories take `Pick<RouteFactoryDeps, 'log' | 'audit'>` (role-grant-offer adds `notification_sender?`).
|
|
22
|
+
- **Parameters** — `*Options` — static startup values, per-factory.
|
|
23
|
+
- **Runtime state** — inline ref — mutable values: `bootstrap_status`, `app_settings` ref, `DaemonTokenState`. NOT in deps or options.
|
|
26
24
|
|
|
27
25
|
`audit: AuditEmitter` is the bound emitter built once at backend assembly by
|
|
28
26
|
the consumer's `audit_factory` callback over `create_audit_emitter`; closes
|
|
@@ -33,15 +31,13 @@ over the pool so rows persist when request transactions roll back. See root
|
|
|
33
31
|
|
|
34
32
|
### Crypto primitives (pure, I/O-free)
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| `auth/daemon_token.ts` | `DaemonToken`, `DAEMON_TOKEN_HEADER` (`X-Daemon-Token`), `generate_daemon_token`, `validate_daemon_token`, `DaemonTokenState` |
|
|
44
|
-
| `auth/bootstrap_account.ts` | `bootstrap_account` (one-shot, `bootstrap_lock`-protected) |
|
|
34
|
+
- `auth/keyring.ts` — `Keyring`, `create_keyring`, `validate_keyring`, `create_validated_keyring`.
|
|
35
|
+
- `auth/session_cookie.ts` — `SessionOptions<T>`, `parse_session`, `process_session_cookie`, `create_session_config`, `fuz_session_config`, `SESSION_AGE_MAX`, `SESSION_REFRESH_THRESHOLD_S`.
|
|
36
|
+
- `auth/password.ts` — `Password`, `PasswordProvided`, `PasswordHashDeps`, `PASSWORD_LENGTH_MIN` (12, OWASP), `PASSWORD_LENGTH_MAX` (300).
|
|
37
|
+
- `auth/password_argon2.ts` — `hash_password`, `verify_password`, `verify_dummy`, `argon2_password_deps`.
|
|
38
|
+
- `auth/api_token.ts` — `API_TOKEN_PREFIX` (`secret_fuz_token_`), `hash_api_token`, `generate_api_token`.
|
|
39
|
+
- `auth/daemon_token.ts` — `DaemonToken`, `DAEMON_TOKEN_HEADER` (`X-Daemon-Token`), `generate_daemon_token`, `validate_daemon_token`, `DaemonTokenState`.
|
|
40
|
+
- `auth/bootstrap_account.ts` — `bootstrap_account` (one-shot, `bootstrap_lock`-protected).
|
|
45
41
|
|
|
46
42
|
Cross-cutting notes that don't live on any single symbol:
|
|
47
43
|
|
|
@@ -60,43 +56,40 @@ Cross-cutting notes that don't live on any single symbol:
|
|
|
60
56
|
|
|
61
57
|
Convention — `*_schema.ts` is Zod-only; `*_ddl.ts` holds DDL strings.
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
| `auth/role_grant_offer_ddl.ts` | `role_grant_offer` table + indexes + `ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID` / `_GLOBAL_TOKEN` |
|
|
77
|
-
| `auth/role_grant_offer_notifications.ts` | Six WS notification specs for the consentful-grant lifecycle |
|
|
59
|
+
- `auth/account_schema.ts` — `Account`, `Actor`, `RoleGrant`, `AuthSession`, `ApiToken` + client-safe JSON shapes.
|
|
60
|
+
- `auth/role_schema.ts` — `RoleName`, `RoleSpec`, `ROLE_KEEPER`, `ROLE_ADMIN`, `create_role_schema`, `builtin_role_specs_by_name`, `role_has_grant_path`, `list_roles_with_grant_path`.
|
|
61
|
+
- `auth/scope_kind_schema.ts` — `ScopeKindName`, `create_scope_kind_schema` (open registry, no builtins).
|
|
62
|
+
- `auth/credential_type_schema.ts` — `CredentialTypeName`, `CREDENTIAL_TYPE_SESSION` / `_API_TOKEN` / `_DAEMON_TOKEN`, `create_credential_type_schema`.
|
|
63
|
+
- `auth/grant_path_schema.ts` — `GrantPathName`, `GRANT_PATH_ADMIN` / `_SELF_SERVICE` / `_SYSTEM` / `_BOOTSTRAP`, `create_grant_path_schema`.
|
|
64
|
+
- `auth/auth_ddl.ts` — `CREATE TABLE` / index / seed strings for the core identity tables.
|
|
65
|
+
- `auth/audit_log_schema.ts` — `AUDIT_EVENT_TYPES` (21 builtins), `AuditEventType` / `AuditEventTypeName`, `audit_metadata_schemas`, `AuditLogEvent`, `AuditLogInput`, `AuditLogConfig`, `create_audit_log_config`.
|
|
66
|
+
- `auth/audit_log_ddl.ts` — `audit_log` table DDL with `seq BIGSERIAL` for cursor-based gap fill (BIGSERIAL converges with the Rust spine; `create_db` registers a `pg.types` int8 parser so `seq` still reads as a JS number).
|
|
67
|
+
- `auth/invite_schema.ts` — `Invite`, `CreateInviteInput`.
|
|
68
|
+
- `auth/app_settings_schema.ts` — `AppSettings`, `UpdateAppSettingsInput` (single-row via `CHECK (id = 1)`).
|
|
69
|
+
- `auth/role_grant_offer_schema.ts` — `RoleGrantOffer`, `RoleGrantOfferJson`, `to_role_grant_offer_json`, scope-sentinel constants.
|
|
70
|
+
- `auth/role_grant_offer_ddl.ts` — `role_grant_offer` table + indexes + `ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID` / `_GLOBAL_TOKEN`.
|
|
71
|
+
- `auth/role_grant_offer_notifications.ts` — six WS notification specs for the consentful-grant lifecycle.
|
|
78
72
|
|
|
79
73
|
### Queries
|
|
80
74
|
|
|
81
75
|
All take `deps: QueryDeps = {db}` first; `query_validate_api_token` adds `log`.
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
| `auth/app_settings_queries.ts` | Load/update for the single-row settings table |
|
|
94
|
-
| `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`) |
|
|
77
|
+
- `auth/account_queries.ts` — account CRUD, actor resolution, password update with verify-write race guard, paged `query_admin_account_list`.
|
|
78
|
+
- `auth/actor_lookup_queries.ts` — batched `actor` ⨝ `account` for the labels arc.
|
|
79
|
+
- `auth/actor_search_queries.ts` — case-insensitive prefix search on `actor.name`, scope-filtered when not admin.
|
|
80
|
+
- `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.
|
|
81
|
+
- `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`.
|
|
82
|
+
- `auth/session_queries.ts` — server-side sessions (blake3-hashed), `query_session_revoke_by_hash_unscoped` (logout only), `query_session_enforce_limit` (transaction-required).
|
|
83
|
+
- `auth/api_token_queries.ts` — token validation with fire-and-forget usage tracking, IDOR-guarded revoke, `query_api_token_enforce_limit` (transaction-required).
|
|
84
|
+
- `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).
|
|
85
|
+
- `auth/app_settings_queries.ts` — load/update for the single-row settings table.
|
|
86
|
+
- `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`).
|
|
95
87
|
|
|
96
88
|
`_unscoped` suffix on `query_session_revoke_by_hash_unscoped` and
|
|
97
89
|
`query_invite_claim_unscoped` is the safety signal: SQL only checks row state,
|
|
98
90
|
caller is responsible for scoping. Production scoping for invites is enforced
|
|
99
|
-
upstream in `auth/signup_routes.ts` via `
|
|
91
|
+
upstream in `auth/signup_routes.ts` via `query_invite_find_unclaimed_match_for_update`
|
|
92
|
+
(SELECT … FOR UPDATE inside the signup tx — find + claim atomic on the row lock).
|
|
100
93
|
|
|
101
94
|
### Audit emitter
|
|
102
95
|
|
|
@@ -122,13 +115,11 @@ they track the same config. Sample via `get_*`; `reset_*` are test-only.
|
|
|
122
115
|
|
|
123
116
|
### Routes
|
|
124
117
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
| `auth/audit_log_routes.ts` | Optional `GET /audit/stream` (SSE) — list/history are on the RPC surface |
|
|
131
|
-
| `auth/auth_guard_resolver.ts` | `fuz_auth_guard_resolver` injected into `apply_route_specs` so the framework stays auth-agnostic |
|
|
118
|
+
- `auth/account_routes.ts` — `POST /login` / `/logout` / `/password`, `GET /verify` (nginx `auth_request` shim), `GET /api/account/status`. Constants: `DEFAULT_MAX_SESSIONS = 5`, `DEFAULT_MAX_TOKENS = 10`, `DEFAULT_LOGIN_FAIL_FLOOR_MS = 250`, `DEFAULT_LOGIN_FAIL_JITTER_MS = 25`.
|
|
119
|
+
- `auth/bootstrap_routes.ts` — `POST /bootstrap` + `check_bootstrap_status`; `BootstrapStatus` runtime ref.
|
|
120
|
+
- `auth/signup_routes.ts` — `POST /signup` (open or invite-gated).
|
|
121
|
+
- `auth/audit_log_routes.ts` — optional `GET /audit/stream` (SSE); list/history are on the RPC surface.
|
|
122
|
+
- `auth/auth_guard_resolver.ts` — `fuz_auth_guard_resolver` injected into `apply_route_specs` so the framework stays auth-agnostic.
|
|
132
123
|
|
|
133
124
|
**`POST /login` timing floor.** Login 401s are floored to
|
|
134
125
|
`DEFAULT_LOGIN_FAIL_FLOOR_MS` (250ms) + uniform jitter (±25ms) via
|
|
@@ -147,13 +138,11 @@ Everything else listed under §RPC action surfaces.
|
|
|
147
138
|
|
|
148
139
|
### Middleware
|
|
149
140
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
| `auth/bearer_auth.ts` | Soft-fail bearer middleware; rejects when `Origin` or `Referer` present (browser context) |
|
|
156
|
-
| `auth/daemon_token_middleware.ts` | `start_daemon_token_rotation` + `create_daemon_token_middleware` (atomic file write, fail-closed validation, keeper account resolution) |
|
|
141
|
+
- `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token`.
|
|
142
|
+
- `auth/request_context.ts` — `RequestContext`, `resolve_acting_actor`, `build_request_context`, predicates (`has_role`, `has_scoped_role`, `has_any_scoped_role`), guards (`require_auth`, `require_role`, `require_credential_types`), `refresh_role_grants`.
|
|
143
|
+
- `auth/session_middleware.ts` — `process_session_cookie` integration, `create_session_and_set_cookie` (shared by login / signup / bootstrap).
|
|
144
|
+
- `auth/bearer_auth.ts` — soft-fail bearer middleware; rejects when `Origin` or `Referer` present (browser context).
|
|
145
|
+
- `auth/daemon_token_middleware.ts` — `start_daemon_token_rotation` + `create_daemon_token_middleware` (atomic file write, fail-closed validation, keeper account resolution).
|
|
157
146
|
|
|
158
147
|
See root ../../../CLAUDE.md §Middleware Ordering for canonical assembly
|
|
159
148
|
order. The auth-specific invariants are described below in §Cross-cutting
|
|
@@ -304,14 +293,12 @@ codegen-importable) and `*_actions.ts` (`create_*_actions(deps, options)`
|
|
|
304
293
|
factory with handlers). Client codegen imports the specs and skips the
|
|
305
294
|
handler module's transitive query-layer deps.
|
|
306
295
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
| `create_actor_lookup_actions` | `all_actor_lookup_action_specs` | no — opt-in batched id → label resolver |
|
|
314
|
-
| `create_actor_search_actions` | `all_actor_search_action_specs` | no — opt-in prefix-search picker |
|
|
296
|
+
- `create_admin_actions` — registry `all_admin_action_specs` — bundled in `create_standard_rpc_actions`.
|
|
297
|
+
- `create_role_grant_offer_actions` — registry `all_role_grant_offer_action_specs` — bundled.
|
|
298
|
+
- `create_account_actions` — registry `all_account_action_specs` — bundled.
|
|
299
|
+
- `create_self_service_role_actions` — registry `all_self_service_role_action_specs` — not bundled (`eligible_roles` is app-specific).
|
|
300
|
+
- `create_actor_lookup_actions` — registry `all_actor_lookup_action_specs` — not bundled (opt-in batched id → label resolver).
|
|
301
|
+
- `create_actor_search_actions` — registry `all_actor_search_action_specs` — not bundled (opt-in prefix-search picker).
|
|
315
302
|
|
|
316
303
|
`auth/all_action_spec_registries.ts` exposes `all_fuz_auth_action_spec_registries`
|
|
317
304
|
for registry-wide invariant tests. Not a mounting surface; protocol specs
|
|
@@ -353,19 +340,17 @@ are excluded.
|
|
|
353
340
|
|
|
354
341
|
`create_admin_actions(deps, options?)` in `auth/admin_actions.ts`.
|
|
355
342
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
| `app_settings_get_action_spec` | false | `z.void()` | `{settings}` |
|
|
368
|
-
| `app_settings_update_action_spec` | true | `{open_signup}` | `{ok, settings}` |
|
|
343
|
+
- `admin_account_list_action_spec` — read; input `{limit?, offset?}`; output `{accounts, grantable_roles}`.
|
|
344
|
+
- `admin_session_list_action_spec` — read; input `z.void()`; output `{sessions}`.
|
|
345
|
+
- `admin_session_revoke_all_action_spec` — mutation; input `{account_id}`; output `{ok, count}`.
|
|
346
|
+
- `admin_token_revoke_all_action_spec` — mutation; input `{account_id}`; output `{ok, count}`.
|
|
347
|
+
- `audit_log_list_action_spec` — read; input `{event_type?, account_id?, limit?, offset?, since_seq?}`; output `{events}`.
|
|
348
|
+
- `audit_log_role_grant_history_action_spec` — read; input `{limit?, offset?}`; output `{events}`.
|
|
349
|
+
- `invite_create_action_spec` — mutation; input `{email?, username?}`; output `{ok, invite}`.
|
|
350
|
+
- `invite_list_action_spec` — read; input `z.void()`; output `{invites}`.
|
|
351
|
+
- `invite_delete_action_spec` — mutation; input `{invite_id}`; output `{ok}`.
|
|
352
|
+
- `app_settings_get_action_spec` — read; input `z.void()`; output `{settings}`.
|
|
353
|
+
- `app_settings_update_action_spec` — mutation; input `{open_signup}`; output `{ok, settings}`.
|
|
369
354
|
|
|
370
355
|
Constants: `AUDIT_LOG_LIST_LIMIT_MAX = 200`, `ADMIN_ACCOUNT_LIST_DEFAULT_LIMIT = 50`,
|
|
371
356
|
`ADMIN_ACCOUNT_LIST_LIMIT_MAX = 200`.
|
|
@@ -413,15 +398,13 @@ event additionally records `credential_type` in metadata (defense in depth).
|
|
|
413
398
|
> suite assume auto-accept and have to redesign their tests when they
|
|
414
399
|
> discover otherwise.
|
|
415
400
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
| `role_grant_offer_history_action_spec` | `{account_id?, limit?, offset?}` | `{offers}` |
|
|
424
|
-
| `role_grant_revoke_action_spec` | `{actor_id, role_grant_id, reason?}` | `{ok, revoked}` |
|
|
401
|
+
- `role_grant_offer_create_action_spec` — input `{to_account_id, to_actor_id?, role, scope_id?, message?}`; output `{offer}`.
|
|
402
|
+
- `role_grant_offer_accept_action_spec` — input `{offer_id}`; output `{role_grant_id, offer, superseded_offer_ids}`.
|
|
403
|
+
- `role_grant_offer_decline_action_spec` — input `{offer_id, reason?}`; output `{ok}`.
|
|
404
|
+
- `role_grant_offer_retract_action_spec` — input `{offer_id}`; output `{ok}`.
|
|
405
|
+
- `role_grant_offer_list_action_spec` — input `{account_id?}`; output `{offers}`.
|
|
406
|
+
- `role_grant_offer_history_action_spec` — input `{account_id?, limit?, offset?}`; output `{offers}`.
|
|
407
|
+
- `role_grant_revoke_action_spec` — input `{actor_id, role_grant_id, reason?}`; output `{ok, revoked}`.
|
|
425
408
|
|
|
426
409
|
Every input carries `acting?: ActingActor` (registry-time invariant 2).
|
|
427
410
|
`role_grant_revoke` keys on **`actor_id`**, not `account_id` — role_grants
|
|
@@ -462,13 +445,11 @@ matching the middleware auth-guard precedent.
|
|
|
462
445
|
|
|
463
446
|
Post-commit via `emit_after_commit` (see `http/CLAUDE.md` §Pending Effects):
|
|
464
447
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
| Decline | `role_grant_offer_declined` → grantor |
|
|
471
|
-
| Revoke | `role_grant_revoke` → revokee + `_supersede` per superseded sibling |
|
|
448
|
+
- Create — `role_grant_offer_received` → recipient.
|
|
449
|
+
- Retract — `role_grant_offer_retracted` → recipient.
|
|
450
|
+
- Accept — `role_grant_offer_accepted` → grantor + `_supersede` per sibling.
|
|
451
|
+
- Decline — `role_grant_offer_declined` → grantor.
|
|
452
|
+
- Revoke — `role_grant_revoke` → revokee + `_supersede` per superseded sibling.
|
|
472
453
|
|
|
473
454
|
Spec module is `auth/role_grant_offer_notifications.ts` — six
|
|
474
455
|
`RemoteNotificationActionSpec`s with Zod params schemas and notification
|
|
@@ -492,15 +473,13 @@ Options: `roles?: RoleSchemaResult` (drives admin-grant-path lookup),
|
|
|
492
473
|
|
|
493
474
|
`create_account_actions(deps, options?)` in `auth/account_actions.ts`.
|
|
494
475
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
| `account_token_list_action_spec` | false | `z.void()` | `{tokens}` |
|
|
503
|
-
| `account_token_revoke_action_spec` | true | `{token_id}` | `{ok, revoked}` |
|
|
476
|
+
- `account_verify_action_spec` — read; input `z.void()`; output `SessionAccountJson`.
|
|
477
|
+
- `account_session_list_action_spec` — read; input `z.void()`; output `{sessions}`.
|
|
478
|
+
- `account_session_revoke_action_spec` — mutation; input `{session_id}`; output `{ok, revoked}`.
|
|
479
|
+
- `account_session_revoke_all_action_spec` — mutation; input `z.void()`; output `{ok, count}`.
|
|
480
|
+
- `account_token_create_action_spec` — mutation; input `{name?}`; output `{ok, token, id, name}`.
|
|
481
|
+
- `account_token_list_action_spec` — read; input `z.void()`; output `{tokens}`.
|
|
482
|
+
- `account_token_revoke_action_spec` — mutation; input `{token_id}`; output `{ok, revoked}`.
|
|
504
483
|
|
|
505
484
|
`account_verify` is intentionally on both surfaces: the REST `GET /verify`
|
|
506
485
|
shim is a status-only nginx probe; the RPC action returns
|
|
@@ -90,13 +90,13 @@ export const audit_metadata_schemas = Object.freeze({
|
|
|
90
90
|
signup: z.looseObject({
|
|
91
91
|
username: z.string().meta({ description: 'Username submitted at signup.' }),
|
|
92
92
|
invite_id: Uuid.optional().meta({
|
|
93
|
-
description: 'Invite consumed by this signup. Set on success and on `
|
|
93
|
+
description: 'Invite consumed by this signup. Set on success and on `signup_conflict` failure rows when an invite was matched at attempt time.',
|
|
94
94
|
}),
|
|
95
95
|
open_signup: z.boolean().optional().meta({
|
|
96
96
|
description: 'True when the signup occurred via the `open_signup` setting (no invite required). Set on success rows under `open_signup` and on failure rows when the attempt was made under `open_signup`.',
|
|
97
97
|
}),
|
|
98
98
|
reason: z.string().optional().meta({
|
|
99
|
-
description: 'Failure category: `no_match` (no unclaimed invite matched), `
|
|
99
|
+
description: 'Failure category: `no_match` (no unclaimed invite matched), `signup_conflict` (username/email already exists, raised by the DB unique constraint), `internal_error` (catch-all for non-classified tx failures — Argon2 fault, session create error, DB outage mid-tx). Set only on `outcome=failure`.',
|
|
100
100
|
}),
|
|
101
101
|
email: Email.optional().meta({
|
|
102
102
|
description: 'Email submitted at signup — recorded on failure rows for forensic correlation. Omitted on success rows because the email is already tied to the resulting account.',
|
|
@@ -34,6 +34,11 @@ export declare const get_daemon_token_path: (runtime: Pick<EnvDeps, "env_get">,
|
|
|
34
34
|
*
|
|
35
35
|
* Uses `write_file_atomic` (temp file + rename) and optionally sets mode 0600.
|
|
36
36
|
*
|
|
37
|
+
* On-disk format is JSON `{"token": "..."}` — the wrapper leaves room for
|
|
38
|
+
* future fields (rotated_at, version) without changing every reader. Both
|
|
39
|
+
* the TS cross-backend harness reader (`spawn_backend.read_daemon_token`)
|
|
40
|
+
* and the Rust daemon-token writer match this shape.
|
|
41
|
+
*
|
|
37
42
|
* @param runtime - runtime with file write capabilities
|
|
38
43
|
* @param token_path - path to write the token
|
|
39
44
|
* @param token - the raw token string
|
|
@@ -58,8 +63,14 @@ export declare const write_daemon_token: (runtime: DaemonTokenWriteDeps, token_p
|
|
|
58
63
|
export declare const resolve_keeper_account_id: (deps: QueryDeps) => Promise<string | null>;
|
|
59
64
|
/** Options for daemon token rotation. */
|
|
60
65
|
export interface DaemonTokenRotationOptions {
|
|
61
|
-
/**
|
|
62
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Absolute path the token file is written to. Caller computes from
|
|
68
|
+
* its own conventions — e.g. `get_daemon_token_path(runtime, app_name)`
|
|
69
|
+
* for the standard `~/.{name}/run/daemon_token` layout, or a path
|
|
70
|
+
* derived from `PUBLIC_<APP>_DIR` for cross-process test setups that
|
|
71
|
+
* isolate the app dir to a tmpdir.
|
|
72
|
+
*/
|
|
73
|
+
token_path: string;
|
|
63
74
|
/** Rotation interval in ms. Default: `30000` (30s). */
|
|
64
75
|
rotation_interval_ms?: number;
|
|
65
76
|
}
|
|
@@ -76,13 +87,12 @@ export interface DaemonTokenRotation {
|
|
|
76
87
|
* Generates an initial token, writes it to disk, resolves the keeper account,
|
|
77
88
|
* and sets up periodic rotation. Returns the mutable state object and a stop function.
|
|
78
89
|
*
|
|
79
|
-
* @param runtime - runtime with file
|
|
90
|
+
* @param runtime - runtime with file and remove capabilities
|
|
80
91
|
* @param deps - query dependencies for resolving keeper account
|
|
81
92
|
* @param options - rotation configuration
|
|
82
93
|
* @param log - the logger instance
|
|
83
94
|
* @returns rotation state and stop function
|
|
84
95
|
* @mutates filesystem - writes the token file on each rotation; `stop` removes it
|
|
85
|
-
* @throws Error if `$HOME` is not set so the daemon token path cannot be resolved
|
|
86
96
|
*/
|
|
87
97
|
export declare const start_daemon_token_rotation: (runtime: DaemonTokenWriteDeps & FsRemoveDeps, deps: QueryDeps, options: DaemonTokenRotationOptions, log: Logger) => Promise<DaemonTokenRotation>;
|
|
88
98
|
/**
|
|
@@ -105,5 +115,5 @@ export declare const start_daemon_token_rotation: (runtime: DaemonTokenWriteDeps
|
|
|
105
115
|
* @param state - the daemon token runtime state
|
|
106
116
|
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, and `AUTH_API_TOKEN_ID_KEY` on a valid token
|
|
107
117
|
*/
|
|
108
|
-
export declare const create_daemon_token_middleware: (state: DaemonTokenState,
|
|
118
|
+
export declare const create_daemon_token_middleware: (state: DaemonTokenState, deps: QueryDeps) => MiddlewareHandler;
|
|
109
119
|
//# sourceMappingURL=daemon_token_middleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon_token_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/daemon_token_middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAC,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AASrF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAKN,KAAK,gBAAgB,EACrB,MAAM,mBAAmB,CAAC;AAE3B,8DAA8D;AAC9D,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,iDAAiD;AACjD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,GAC1D,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,iBAAiB,GAAG,QAAQ,CAAC,GAAG;IAC3D,6FAA6F;IAC7F,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EACjC,MAAM,MAAM,KACV,MAAM,GAAG,IAGX,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"daemon_token_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/daemon_token_middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAC5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAC,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AASrF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAKN,KAAK,gBAAgB,EACrB,MAAM,mBAAmB,CAAC;AAE3B,8DAA8D;AAC9D,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,iDAAiD;AACjD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,GAC1D,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,iBAAiB,GAAG,QAAQ,CAAC,GAAG;IAC3D,6FAA6F;IAC7F,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EACjC,MAAM,MAAM,KACV,MAAM,GAAG,IAGX,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kBAAkB,GAC9B,SAAS,oBAAoB,EAC7B,YAAY,MAAM,EAClB,OAAO,MAAM,KACX,OAAO,CAAC,IAAI,CAKd,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,yBAAyB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAEtF,CAAC;AAEF,yCAAyC;AACzC,MAAM,WAAW,0BAA0B;IAC1C;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IACnC,2EAA2E;IAC3E,KAAK,EAAE,gBAAgB,CAAC;IACxB,kGAAkG;IAClG,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,oBAAoB,GAAG,YAAY,EAC5C,MAAM,SAAS,EACf,SAAS,0BAA0B,EACnC,KAAK,MAAM,KACT,OAAO,CAAC,mBAAmB,CAqD7B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,8BAA8B,GAC1C,OAAO,gBAAgB,EACvB,MAAM,SAAS,KACb,iBAsCF,CAAC"}
|
|
@@ -35,13 +35,18 @@ export const get_daemon_token_path = (runtime, name) => {
|
|
|
35
35
|
*
|
|
36
36
|
* Uses `write_file_atomic` (temp file + rename) and optionally sets mode 0600.
|
|
37
37
|
*
|
|
38
|
+
* On-disk format is JSON `{"token": "..."}` — the wrapper leaves room for
|
|
39
|
+
* future fields (rotated_at, version) without changing every reader. Both
|
|
40
|
+
* the TS cross-backend harness reader (`spawn_backend.read_daemon_token`)
|
|
41
|
+
* and the Rust daemon-token writer match this shape.
|
|
42
|
+
*
|
|
38
43
|
* @param runtime - runtime with file write capabilities
|
|
39
44
|
* @param token_path - path to write the token
|
|
40
45
|
* @param token - the raw token string
|
|
41
46
|
* @mutates filesystem - writes `token_path` atomically and `chmod 0600` when supported
|
|
42
47
|
*/
|
|
43
48
|
export const write_daemon_token = async (runtime, token_path, token) => {
|
|
44
|
-
await write_file_atomic(runtime, token_path, token + '\n');
|
|
49
|
+
await write_file_atomic(runtime, token_path, JSON.stringify({ token }) + '\n');
|
|
45
50
|
if (runtime.chmod) {
|
|
46
51
|
await runtime.chmod(token_path, 0o600);
|
|
47
52
|
}
|
|
@@ -70,26 +75,23 @@ export const resolve_keeper_account_id = async (deps) => {
|
|
|
70
75
|
* Generates an initial token, writes it to disk, resolves the keeper account,
|
|
71
76
|
* and sets up periodic rotation. Returns the mutable state object and a stop function.
|
|
72
77
|
*
|
|
73
|
-
* @param runtime - runtime with file
|
|
78
|
+
* @param runtime - runtime with file and remove capabilities
|
|
74
79
|
* @param deps - query dependencies for resolving keeper account
|
|
75
80
|
* @param options - rotation configuration
|
|
76
81
|
* @param log - the logger instance
|
|
77
82
|
* @returns rotation state and stop function
|
|
78
83
|
* @mutates filesystem - writes the token file on each rotation; `stop` removes it
|
|
79
|
-
* @throws Error if `$HOME` is not set so the daemon token path cannot be resolved
|
|
80
84
|
*/
|
|
81
85
|
export const start_daemon_token_rotation = async (runtime, deps, options, log) => {
|
|
82
|
-
const {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// ensure run directory exists
|
|
88
|
-
const app_dir = get_app_dir(runtime, app_name);
|
|
89
|
-
if (app_dir) {
|
|
90
|
-
await runtime.mkdir(`${app_dir}/run`, { recursive: true });
|
|
86
|
+
const { token_path, rotation_interval_ms = DEFAULT_ROTATION_INTERVAL_MS } = options;
|
|
87
|
+
// ensure parent directory exists
|
|
88
|
+
const last_slash = token_path.lastIndexOf('/');
|
|
89
|
+
if (last_slash > 0) {
|
|
90
|
+
await runtime.mkdir(token_path.slice(0, last_slash), { recursive: true });
|
|
91
91
|
}
|
|
92
|
-
// resolve keeper account (may be null pre-bootstrap
|
|
92
|
+
// resolve keeper account (may be null pre-bootstrap; the middleware
|
|
93
|
+
// lazily refreshes on the first null hit to cover the
|
|
94
|
+
// rotation-starts-before-bootstrap case)
|
|
93
95
|
const keeper_account_id = await resolve_keeper_account_id(deps);
|
|
94
96
|
// generate initial token and write to disk
|
|
95
97
|
const initial_token = generate_daemon_token();
|
|
@@ -150,7 +152,7 @@ export const start_daemon_token_rotation = async (runtime, deps, options, log) =
|
|
|
150
152
|
* @param state - the daemon token runtime state
|
|
151
153
|
* @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, and `AUTH_API_TOKEN_ID_KEY` on a valid token
|
|
152
154
|
*/
|
|
153
|
-
export const create_daemon_token_middleware = (state,
|
|
155
|
+
export const create_daemon_token_middleware = (state, deps) => {
|
|
154
156
|
return async (c, next) => {
|
|
155
157
|
const token_header = c.req.header(DAEMON_TOKEN_HEADER);
|
|
156
158
|
if (!token_header) {
|
|
@@ -166,7 +168,14 @@ export const create_daemon_token_middleware = (state, _deps) => {
|
|
|
166
168
|
if (!validate_daemon_token(parse_result.data, state)) {
|
|
167
169
|
return c.json({ error: ERROR_INVALID_DAEMON_TOKEN }, 401);
|
|
168
170
|
}
|
|
169
|
-
// daemon token valid — resolve keeper account
|
|
171
|
+
// daemon token valid — resolve keeper account. `start_daemon_token_rotation`
|
|
172
|
+
// resolves the keeper once at startup, but rotation often starts before the
|
|
173
|
+
// keeper account exists (e.g. cross-process test harnesses spawn the binary
|
|
174
|
+
// then POST /bootstrap). Lazily refresh from the DB on the first null hit
|
|
175
|
+
// so the post-bootstrap state lands without a separate hook.
|
|
176
|
+
if (!state.keeper_account_id) {
|
|
177
|
+
state.keeper_account_id = await resolve_keeper_account_id(deps);
|
|
178
|
+
}
|
|
170
179
|
if (!state.keeper_account_id) {
|
|
171
180
|
return c.json({ error: ERROR_KEEPER_ACCOUNT_NOT_CONFIGURED }, 503);
|
|
172
181
|
}
|
|
@@ -26,27 +26,37 @@ export declare const query_invite_find_unclaimed_by_email: (deps: QueryDeps, ema
|
|
|
26
26
|
*/
|
|
27
27
|
export declare const query_invite_find_unclaimed_by_username: (deps: QueryDeps, username: string) => Promise<Invite | undefined>;
|
|
28
28
|
/**
|
|
29
|
-
* Find an unclaimed invite matching email and/or username
|
|
29
|
+
* Find an unclaimed invite matching email and/or username, taking a
|
|
30
|
+
* row-level write lock on the matched row.
|
|
31
|
+
*
|
|
32
|
+
* Three scoping modes:
|
|
30
33
|
*
|
|
31
34
|
* - **Email-only invite** (email set, username NULL) → matches only if signup provides matching email.
|
|
32
35
|
* - **Username-only invite** (username set, email NULL) → matches only if signup provides matching username.
|
|
33
36
|
* - **Both-field invite** (both set) → requires BOTH email and username to match.
|
|
34
37
|
*
|
|
35
|
-
*
|
|
38
|
+
* Must run inside the same transaction as `query_invite_claim_unscoped`:
|
|
39
|
+
* `FOR UPDATE` makes find + claim atomic, so a concurrent signup that
|
|
40
|
+
* matched the same invite blocks on the lock until this transaction
|
|
41
|
+
* commits/rolls back. After commit, the loser's `find_for_update`
|
|
42
|
+
* returns no row (the winner flipped `claimed_at`) and falls through to
|
|
43
|
+
* `ERROR_NO_MATCHING_INVITE` — no race window between find and claim.
|
|
44
|
+
*
|
|
45
|
+
* @param deps - query dependencies — `deps.db` MUST be a transaction
|
|
36
46
|
* @param email - email to match (or null if signup provides none)
|
|
37
47
|
* @param username - username to match
|
|
38
|
-
* @returns the matching invite, or `undefined`
|
|
48
|
+
* @returns the matching invite (locked), or `undefined`
|
|
39
49
|
*/
|
|
40
|
-
export declare const
|
|
50
|
+
export declare const query_invite_find_unclaimed_match_for_update: (deps: QueryDeps, email: string | null, username: string) => Promise<Invite | undefined>;
|
|
41
51
|
/**
|
|
42
52
|
* Claim an invite by setting the claimed_by and claimed_at fields.
|
|
43
53
|
*
|
|
44
54
|
* The `_unscoped` suffix is the safety signal — the SQL only checks the
|
|
45
55
|
* row state (`claimed_at IS NULL`), not whether the claiming account's
|
|
46
56
|
* email or username matches the invite. Callers must scope the lookup
|
|
47
|
-
* upstream via
|
|
48
|
-
*
|
|
49
|
-
* caller claim any unclaimed invite by id.
|
|
57
|
+
* upstream via one of the `_find_unclaimed_match*` siblings (production
|
|
58
|
+
* uses `_for_update` to make find + claim atomic). Skipping the find
|
|
59
|
+
* step lets a caller claim any unclaimed invite by id.
|
|
50
60
|
*
|
|
51
61
|
* Mirrors the `query_session_revoke_by_hash_unscoped` precedent — there
|
|
52
62
|
* is no scoped sibling because the scoping is provided by a separate
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invite_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/invite_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,KAAK,EAAC,MAAM,EAAE,iBAAiB,EAAE,uBAAuB,EAAC,MAAM,oBAAoB,CAAC;AAE3F;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,OAAO,iBAAiB,KACtB,OAAO,CAAC,MAAM,CAQhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,MAAM,GAAG,SAAS,CAK5B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uCAAuC,GACnD,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,MAAM,GAAG,SAAS,CAK5B,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"invite_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/invite_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,KAAK,EAAC,MAAM,EAAE,iBAAiB,EAAE,uBAAuB,EAAC,MAAM,oBAAoB,CAAC;AAE3F;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,OAAO,iBAAiB,KACtB,OAAO,CAAC,MAAM,CAQhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,MAAM,GAAG,SAAS,CAK5B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uCAAuC,GACnD,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,MAAM,GAAG,SAAS,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,4CAA4C,GACxD,MAAM,SAAS,EACf,OAAO,MAAM,GAAG,IAAI,EACpB,UAAU,MAAM,KACd,OAAO,CAAC,MAAM,GAAG,SAAS,CAgB5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,WAAW,MAAM,EACjB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAQjB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAElF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,KACb,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAUxC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,OAAO,CAMjB,CAAC"}
|
|
@@ -34,18 +34,28 @@ export const query_invite_find_unclaimed_by_username = async (deps, username) =>
|
|
|
34
34
|
return deps.db.query_one(`SELECT * FROM invite WHERE LOWER(username) = LOWER($1) AND claimed_at IS NULL`, [username]);
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
|
-
* Find an unclaimed invite matching email and/or username
|
|
37
|
+
* Find an unclaimed invite matching email and/or username, taking a
|
|
38
|
+
* row-level write lock on the matched row.
|
|
39
|
+
*
|
|
40
|
+
* Three scoping modes:
|
|
38
41
|
*
|
|
39
42
|
* - **Email-only invite** (email set, username NULL) → matches only if signup provides matching email.
|
|
40
43
|
* - **Username-only invite** (username set, email NULL) → matches only if signup provides matching username.
|
|
41
44
|
* - **Both-field invite** (both set) → requires BOTH email and username to match.
|
|
42
45
|
*
|
|
43
|
-
*
|
|
46
|
+
* Must run inside the same transaction as `query_invite_claim_unscoped`:
|
|
47
|
+
* `FOR UPDATE` makes find + claim atomic, so a concurrent signup that
|
|
48
|
+
* matched the same invite blocks on the lock until this transaction
|
|
49
|
+
* commits/rolls back. After commit, the loser's `find_for_update`
|
|
50
|
+
* returns no row (the winner flipped `claimed_at`) and falls through to
|
|
51
|
+
* `ERROR_NO_MATCHING_INVITE` — no race window between find and claim.
|
|
52
|
+
*
|
|
53
|
+
* @param deps - query dependencies — `deps.db` MUST be a transaction
|
|
44
54
|
* @param email - email to match (or null if signup provides none)
|
|
45
55
|
* @param username - username to match
|
|
46
|
-
* @returns the matching invite, or `undefined`
|
|
56
|
+
* @returns the matching invite (locked), or `undefined`
|
|
47
57
|
*/
|
|
48
|
-
export const
|
|
58
|
+
export const query_invite_find_unclaimed_match_for_update = async (deps, email, username) => {
|
|
49
59
|
return deps.db.query_one(`SELECT * FROM invite WHERE claimed_at IS NULL AND (
|
|
50
60
|
(email IS NOT NULL AND username IS NULL
|
|
51
61
|
AND $1::text IS NOT NULL AND LOWER(email) = LOWER($1::text))
|
|
@@ -56,7 +66,8 @@ export const query_invite_find_unclaimed_match = async (deps, email, username) =
|
|
|
56
66
|
(email IS NOT NULL AND username IS NOT NULL
|
|
57
67
|
AND $1::text IS NOT NULL AND LOWER(email) = LOWER($1::text)
|
|
58
68
|
AND LOWER(username) = LOWER($2))
|
|
59
|
-
) ORDER BY created_at ASC, id ASC LIMIT 1
|
|
69
|
+
) ORDER BY created_at ASC, id ASC LIMIT 1
|
|
70
|
+
FOR UPDATE`, [email, username]);
|
|
60
71
|
};
|
|
61
72
|
/**
|
|
62
73
|
* Claim an invite by setting the claimed_by and claimed_at fields.
|
|
@@ -64,9 +75,9 @@ export const query_invite_find_unclaimed_match = async (deps, email, username) =
|
|
|
64
75
|
* The `_unscoped` suffix is the safety signal — the SQL only checks the
|
|
65
76
|
* row state (`claimed_at IS NULL`), not whether the claiming account's
|
|
66
77
|
* email or username matches the invite. Callers must scope the lookup
|
|
67
|
-
* upstream via
|
|
68
|
-
*
|
|
69
|
-
* caller claim any unclaimed invite by id.
|
|
78
|
+
* upstream via one of the `_find_unclaimed_match*` siblings (production
|
|
79
|
+
* uses `_for_update` to make find + claim atomic). Skipping the find
|
|
80
|
+
* step lets a caller claim any unclaimed invite by id.
|
|
70
81
|
*
|
|
71
82
|
* Mirrors the `query_session_revoke_by_hash_unscoped` precedent — there
|
|
72
83
|
* is no scoped sibling because the scoping is provided by a separate
|