@fuzdev/fuz_app 0.108.0 → 0.110.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/README.md +4 -0
- package/dist/actions/CLAUDE.md +1 -1
- package/dist/auth/CLAUDE.md +7 -6
- package/dist/auth/account_action_specs.d.ts +18 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +14 -1
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +6 -2
- package/dist/auth/audit_emitter.d.ts +9 -5
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.d.ts +7 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +16 -1
- package/dist/auth/bootstrap_account.d.ts +7 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +9 -4
- package/dist/auth/bootstrap_routes.d.ts +12 -4
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +26 -7
- package/dist/auth/daemon_token_middleware.d.ts +7 -69
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +8 -104
- package/dist/auth/deps.d.ts +9 -5
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/middleware.js +1 -1
- package/dist/auth/migrations.d.ts +7 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +25 -8
- package/dist/auth/request_context.d.ts +1 -3
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +2 -5
- package/dist/auth/session_cookie.d.ts +18 -36
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +16 -26
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +1 -4
- package/dist/auth/session_queries.d.ts +14 -30
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +15 -45
- package/dist/auth/token_lifetime.d.ts +51 -0
- package/dist/auth/token_lifetime.d.ts.map +1 -0
- package/dist/auth/token_lifetime.js +54 -0
- package/dist/db/CLAUDE.md +9 -5
- package/dist/db/cell_audit_queries.d.ts +4 -2
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +4 -2
- package/dist/db/pg_error.d.ts +12 -0
- package/dist/db/pg_error.d.ts.map +1 -1
- package/dist/db/pg_error.js +15 -1
- package/dist/dev/setup.d.ts +5 -9
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +18 -21
- package/dist/http/CLAUDE.md +78 -18
- package/dist/http/db_routes.d.ts +96 -3
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +263 -46
- package/dist/http/error_schemas.d.ts +12 -1
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -1
- package/dist/http/pending_effects.d.ts +5 -5
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +27 -1
- package/dist/runtime/deps.d.ts +38 -4
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/fs.d.ts +24 -5
- package/dist/runtime/fs.d.ts.map +1 -1
- package/dist/runtime/fs.js +38 -7
- package/dist/runtime/mock.d.ts +7 -0
- package/dist/runtime/mock.d.ts.map +1 -1
- package/dist/runtime/mock.js +41 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +7 -1
- package/dist/runtime/secure_file.d.ts +71 -0
- package/dist/runtime/secure_file.d.ts.map +1 -0
- package/dist/runtime/secure_file.js +125 -0
- package/dist/server/app_backend.d.ts +6 -5
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +2 -3
- package/dist/testing/CLAUDE.md +36 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +10 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +13 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +10 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +34 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +53 -1
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +2 -1
- package/dist/testing/cross_backend/query_shape.d.ts +10 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
- package/dist/testing/cross_backend/query_shape.js +128 -0
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.d.ts +15 -0
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -0
- package/dist/testing/cross_backend/token_lifetime.js +99 -0
- package/dist/testing/cross_backend/token_scope_surface.js +1 -1
- package/dist/testing/daemon_token_rotation.d.ts +83 -0
- package/dist/testing/daemon_token_rotation.d.ts.map +1 -0
- package/dist/testing/daemon_token_rotation.js +99 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +4 -4
- package/dist/ui/AccountSessions.svelte +0 -5
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +7 -4
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +0 -5
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +9 -4
- package/dist/ui/table_state.svelte.d.ts +18 -5
- package/dist/ui/table_state.svelte.d.ts.map +1 -1
- package/dist/ui/table_state.svelte.js +22 -7
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/dist/ui/ui_format.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,10 @@ Deno.serve({port: PORT, hostname: HOST}, app.fetch);
|
|
|
45
45
|
|
|
46
46
|
See [CLAUDE.md](CLAUDE.md) for more usage patterns and the AI-generated docs.
|
|
47
47
|
|
|
48
|
+
## Contributing
|
|
49
|
+
|
|
50
|
+
[fuz.dev/contributing](https://www.fuz.dev/contributing)
|
|
51
|
+
|
|
48
52
|
## License
|
|
49
53
|
|
|
50
54
|
[MIT](LICENSE)
|
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -217,7 +217,7 @@ ones. Matches the Rust spine, which validates handler-side.
|
|
|
217
217
|
|
|
218
218
|
Shim responsibilities (per-request):
|
|
219
219
|
|
|
220
|
-
1. Parse envelope (POST body / GET query string); parse errors → `parse_error` 400.
|
|
220
|
+
1. Parse envelope (POST body / GET query string); parse errors → `parse_error` 400. GET reads `method` / `id` / `params` via Hono's `c.req.query()` — first occurrence wins on a duplicated key, unknown keys are ignored, and an empty value counts as missing. This is the cross-impl contract (the Rust GET dispatch mirrors it via `fuz_http`'s `query_first`, including the id numeric-vs-string normalization), pinned by the `query_shape` cross suite.
|
|
221
221
|
2. Lookup method in the `compile_action_registry`-built map; unknown → `method_not_found`.
|
|
222
222
|
3. GET read restriction — GET rejected for `side_effects: true` actions.
|
|
223
223
|
4. Build `PerformActionInput` from `c.var` + `get_client_ip` + `c.req.raw.signal`. Test-preset escape hatch reads `TEST_CONTEXT_PRESET_KEY` + `REQUEST_CONTEXT_KEY`.
|
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -16,7 +16,7 @@ documents the cross-cutting invariants that don't fit on any single symbol.
|
|
|
16
16
|
|
|
17
17
|
## AppDeps split
|
|
18
18
|
|
|
19
|
-
- **Capabilities** — `AppDeps` — stateless, injectable per env: `
|
|
19
|
+
- **Capabilities** — `AppDeps` — stateless, injectable per env: `read_secure_file` (hardened bootstrap-token read), `delete_file`, `keyring`, `password`, `db`, `log`, `audit`.
|
|
20
20
|
- **Route caps** — `RouteFactoryDeps` — `Omit<AppDeps, 'db'>`; handlers get `db` via `RouteContext`.
|
|
21
21
|
- **Action caps** — `ActionFactoryDeps` (`auth/deps.ts`) — the `{log, audit}` shape action factories take; `RouteFactoryDeps`/`AppDeps` satisfy it structurally (role-grant-offer adds `notification_sender?`). The two pure-read actor factories (`actor_lookup`/`actor_search`) take only `{log: Logger}`.
|
|
22
22
|
- **Parameters** — `*Options` — static startup values, per-factory.
|
|
@@ -32,7 +32,7 @@ over the pool so rows persist when request transactions roll back. See root
|
|
|
32
32
|
### Crypto primitives (pure, I/O-free)
|
|
33
33
|
|
|
34
34
|
- `auth/keyring.ts` — `Keyring`, `create_keyring`, `validate_keyring`, `create_validated_keyring`.
|
|
35
|
-
- `auth/session_cookie.ts` — `SessionOptions<T>`, `parse_session`, `process_session_cookie
|
|
35
|
+
- `auth/session_cookie.ts` — `SessionOptions<T>`, `parse_session`, `process_session_cookie` (`'none' | 'clear'` — no refresh action; the 30-day lifetime is an absolute cap signed once at mint), `create_session_config`, `fuz_session_config`, `SESSION_AGE_MAX`.
|
|
36
36
|
- `auth/password.ts` — `Password`, `PasswordProvided`, `PasswordHashDeps`, `PASSWORD_LENGTH_MIN` (12, OWASP), `PASSWORD_LENGTH_MAX` (300).
|
|
37
37
|
- `auth/password_argon2.ts` — `hash_password`, `verify_password`, `verify_dummy`, `argon2_password_deps`.
|
|
38
38
|
- `auth/api_token.ts` — `API_TOKEN_PREFIX` (`secret_fuz_token_`), `hash_api_token`, `generate_api_token`.
|
|
@@ -61,6 +61,7 @@ Convention — `*_schema.ts` is Zod-only; `*_ddl.ts` holds DDL strings.
|
|
|
61
61
|
- `auth/scope_kind_schema.ts` — `ScopeKindName`, `create_scope_kind_schema` (open registry, no builtins).
|
|
62
62
|
- `auth/credential_type_schema.ts` — `CredentialTypeName`, `CREDENTIAL_TYPE_SESSION` / `_API_TOKEN` / `_DAEMON_TOKEN`, `create_credential_type_schema`.
|
|
63
63
|
- `auth/grant_path_schema.ts` — `GrantPathName`, `GRANT_PATH_ADMIN` / `_SELF_SERVICE` / `_SYSTEM` / `_BOOTSTRAP`, `create_grant_path_schema`.
|
|
64
|
+
- `auth/token_lifetime.ts` — the temporal axis of a minted token: `TokenLifetimeInput` (required union on `account_token_create` — `{kind:'eternal'}` or `{kind:'ttl', days}`), `token_lifetime_to_expires_at`, `TOKEN_TTL_DAYS_MAX`. No default — `expires_at IS NULL` always means "deliberately eternal".
|
|
64
65
|
- `auth/token_scope.ts` — per-credential authority narrowing stored on `api_token.scope`: `TokenScope` / `TokenScopeInput` + constructors, `parse_stored_token_scope` (fail-closed), `token_scope_admits_method` / `_non_rpc`, and the `<section>:<id>` capability vocabulary `RouteAuth.required_scope` is written in (`parse_token_scope_capability`, `token_scope_admits_capability`, `token_scope_denied_body`, `TOKEN_SURFACES`). Not an open registry — see ../../../docs/security.md §Token scoping for why the identifier half is deliberately unregistered.
|
|
65
66
|
- `auth/auth_ddl.ts` — `CREATE TABLE` / index / seed strings for the core identity tables.
|
|
66
67
|
- `auth/audit_log_schema.ts` — `AUDIT_EVENT_TYPES` (27 builtins), `AuditEventType` / `AuditEventTypeName`, `audit_metadata_schemas`, `AuditLogEvent`, `AuditLogInput`, `AuditLogConfig`, `create_audit_log_config`.
|
|
@@ -80,7 +81,7 @@ All take `deps: QueryDeps = {db}` first; `query_validate_api_token` adds `log`.
|
|
|
80
81
|
- `auth/actor_search_queries.ts` — case-insensitive prefix search on `actor.name`, scope-filtered when not admin.
|
|
81
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.
|
|
82
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`.
|
|
83
|
-
- `auth/session_queries.ts` — server-side sessions (blake3-hashed), `query_session_revoke_by_hash_unscoped` (logout only), `query_session_enforce_limit` (transaction-required).
|
|
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`), and `last_seen_at` is decorative pending its own removal migration.
|
|
84
85
|
- `auth/api_token_queries.ts` — token validation with fire-and-forget usage tracking, IDOR-guarded revoke, `query_api_token_enforce_limit` (transaction-required).
|
|
85
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).
|
|
86
87
|
- `auth/app_settings_queries.ts` — load/update for the single-row settings table.
|
|
@@ -162,7 +163,7 @@ Everything else listed under §RPC action surfaces.
|
|
|
162
163
|
- `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`, `require_token_scope`), `token_scope_surface_denial` (the direct-call form, for surfaces that aren't route specs), `refresh_role_grants`.
|
|
163
164
|
- `auth/session_middleware.ts` — `process_session_cookie` integration, `create_session_and_set_cookie` (shared by login / signup / bootstrap).
|
|
164
165
|
- `auth/bearer_auth.ts` — soft-fail bearer middleware; rejects when `Origin` or `Referer` present (browser context).
|
|
165
|
-
- `auth/daemon_token_middleware.ts` — `
|
|
166
|
+
- `auth/daemon_token_middleware.ts` — `create_daemon_token_middleware(state, deps, log)` — the credential **consumer** only (soft-fail validation, keeper account resolution). The producer (`write_daemon_token` / `start_daemon_token_rotation`) lives in `testing/daemon_token_rotation.ts` behind the dev-env guard — no production assembly mints daemon tokens, mirroring the Rust spine's `fuz_testing`-confined producer. Soft-fails — discards the credential (pass-through, no own 401/503) on **every** non-success path: browser context (`Origin`/`Referer` present), malformed/invalid token, and valid-token-but-no-keeper all `next()` through to the dispatcher's `credential_type_required` (403) gate, mirroring the bearer guard and the Rust spine's `resolve.rs`. Daemon tokens are loopback-only, so browser context never arises in practice — the discard is defense-in-depth.
|
|
166
167
|
|
|
167
168
|
See root ../../../CLAUDE.md §Middleware Ordering for canonical assembly
|
|
168
169
|
order. The auth-specific invariants are described below in §Cross-cutting
|
|
@@ -379,7 +380,7 @@ token_create role_grant_offer_supersede
|
|
|
379
380
|
token_revoke invite_create
|
|
380
381
|
token_revoke_all invite_delete
|
|
381
382
|
account_delete app_settings_update
|
|
382
|
-
account_purge
|
|
383
|
+
account_purge db_admin_row_delete
|
|
383
384
|
account_undelete
|
|
384
385
|
actor_delete
|
|
385
386
|
actor_purge
|
|
@@ -638,7 +639,7 @@ Options: `roles?: RoleSchemaResult` (drives admin-grant-path lookup),
|
|
|
638
639
|
- `account_session_list_action_spec` — read; input `z.void()`; output `{sessions}`.
|
|
639
640
|
- `account_session_revoke_action_spec` — mutation; input `{session_id}`; output `{ok, revoked}`.
|
|
640
641
|
- `account_session_revoke_all_action_spec` — mutation; input `z.void()`; output `{ok, count}`.
|
|
641
|
-
- `account_token_create_action_spec` — mutation; input `{name
|
|
642
|
+
- `account_token_create_action_spec` — mutation; input `{name?, scope, lifetime}` (both `scope` and `lifetime` required — default-deny at mint on both axes); output `{ok, token, id, name, expires_at}`.
|
|
642
643
|
- `account_token_list_action_spec` — read; input `z.void()`; output `{tokens}`.
|
|
643
644
|
- `account_token_revoke_action_spec` — mutation; input `{token_id}`; output `{ok, revoked}`.
|
|
644
645
|
|
|
@@ -55,6 +55,10 @@ export type SessionRevokeAllOutput = z.infer<typeof SessionRevokeAllOutput>;
|
|
|
55
55
|
* reversal of the 2026-02 design, where the field existed but nothing depended
|
|
56
56
|
* on it. Because `scope` has no default, the object as a whole no longer
|
|
57
57
|
* carries `.prefault({})`.
|
|
58
|
+
*
|
|
59
|
+
* `lifetime` follows the same rule — a never-expiring token is
|
|
60
|
+
* `{kind: 'eternal'}`, spelled out, so `expires_at IS NULL` always means
|
|
61
|
+
* "deliberately eternal" (see `auth/token_lifetime.ts`).
|
|
58
62
|
*/
|
|
59
63
|
export declare const TokenCreateInput: z.ZodObject<{
|
|
60
64
|
name: z.ZodDefault<z.ZodString>;
|
|
@@ -64,6 +68,12 @@ export declare const TokenCreateInput: z.ZodObject<{
|
|
|
64
68
|
kind: z.ZodLiteral<"methods">;
|
|
65
69
|
methods: z.ZodArray<z.ZodString>;
|
|
66
70
|
}, z.core.$strict>], "kind">;
|
|
71
|
+
lifetime: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
72
|
+
kind: z.ZodLiteral<"eternal">;
|
|
73
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
74
|
+
kind: z.ZodLiteral<"ttl">;
|
|
75
|
+
days: z.ZodNumber;
|
|
76
|
+
}, z.core.$strict>], "kind">;
|
|
67
77
|
}, z.core.$strict>;
|
|
68
78
|
export type TokenCreateInput = z.infer<typeof TokenCreateInput>;
|
|
69
79
|
/** Output for `account_token_create`. `token` is returned exactly once. */
|
|
@@ -72,6 +82,7 @@ export declare const TokenCreateOutput: z.ZodObject<{
|
|
|
72
82
|
token: z.ZodString;
|
|
73
83
|
id: z.ZodString;
|
|
74
84
|
name: z.ZodString;
|
|
85
|
+
expires_at: z.ZodNullable<z.ZodString>;
|
|
75
86
|
}, z.core.$strict>;
|
|
76
87
|
export type TokenCreateOutput = z.infer<typeof TokenCreateOutput>;
|
|
77
88
|
/** Input for `account_token_list`. No parameters. */
|
|
@@ -210,12 +221,19 @@ export declare const account_token_create_action_spec: {
|
|
|
210
221
|
kind: z.ZodLiteral<"methods">;
|
|
211
222
|
methods: z.ZodArray<z.ZodString>;
|
|
212
223
|
}, z.core.$strict>], "kind">;
|
|
224
|
+
lifetime: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
225
|
+
kind: z.ZodLiteral<"eternal">;
|
|
226
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
227
|
+
kind: z.ZodLiteral<"ttl">;
|
|
228
|
+
days: z.ZodNumber;
|
|
229
|
+
}, z.core.$strict>], "kind">;
|
|
213
230
|
}, z.core.$strict>;
|
|
214
231
|
output: z.ZodObject<{
|
|
215
232
|
ok: z.ZodLiteral<true>;
|
|
216
233
|
token: z.ZodString;
|
|
217
234
|
id: z.ZodString;
|
|
218
235
|
name: z.ZodString;
|
|
236
|
+
expires_at: z.ZodNullable<z.ZodString>;
|
|
219
237
|
}, z.core.$strict>;
|
|
220
238
|
async: true;
|
|
221
239
|
description: string;
|
|
@@ -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;
|
|
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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"}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { TokenScopeInput } from './token_scope.js';
|
|
12
|
+
import { TokenLifetimeInput } from './token_lifetime.js';
|
|
12
13
|
import { AuthSessionJson, ClientApiTokenJson, SessionAccountJson, SessionId } from './account_schema.js';
|
|
13
14
|
import { ApiTokenId } from './api_token.js';
|
|
14
15
|
// -- Input/output schemas ---------------------------------------------------
|
|
@@ -45,6 +46,10 @@ export const SessionRevokeAllOutput = z.strictObject({
|
|
|
45
46
|
* reversal of the 2026-02 design, where the field existed but nothing depended
|
|
46
47
|
* on it. Because `scope` has no default, the object as a whole no longer
|
|
47
48
|
* carries `.prefault({})`.
|
|
49
|
+
*
|
|
50
|
+
* `lifetime` follows the same rule — a never-expiring token is
|
|
51
|
+
* `{kind: 'eternal'}`, spelled out, so `expires_at IS NULL` always means
|
|
52
|
+
* "deliberately eternal" (see `auth/token_lifetime.ts`).
|
|
48
53
|
*/
|
|
49
54
|
export const TokenCreateInput = z.strictObject({
|
|
50
55
|
name: z
|
|
@@ -55,6 +60,10 @@ export const TokenCreateInput = z.strictObject({
|
|
|
55
60
|
description: "What the token may do. `{kind:'full'}` for full account authority, or " +
|
|
56
61
|
"`{kind:'methods',methods:[…]}` to narrow it to named RPC methods — a " +
|
|
57
62
|
'narrowed token is RPC-only and reaches no non-RPC surface.'
|
|
63
|
+
}),
|
|
64
|
+
lifetime: TokenLifetimeInput.meta({
|
|
65
|
+
description: "How long the token lives. `{kind:'eternal'}` never expires; " +
|
|
66
|
+
"`{kind:'ttl',days:N}` expires N days from mint."
|
|
58
67
|
})
|
|
59
68
|
});
|
|
60
69
|
/** Output for `account_token_create`. `token` is returned exactly once. */
|
|
@@ -62,7 +71,11 @@ export const TokenCreateOutput = z.strictObject({
|
|
|
62
71
|
ok: z.literal(true),
|
|
63
72
|
token: z.string().meta({ description: 'Raw token — shown once, store securely.' }),
|
|
64
73
|
id: ApiTokenId,
|
|
65
|
-
name: z.string()
|
|
74
|
+
name: z.string(),
|
|
75
|
+
expires_at: z
|
|
76
|
+
.string()
|
|
77
|
+
.nullable()
|
|
78
|
+
.meta({ description: 'ISO 8601 expiry, or `null` for an eternal token.' })
|
|
66
79
|
});
|
|
67
80
|
/** Input for `account_token_list`. No parameters. */
|
|
68
81
|
export const TokenListInput = z.void();
|
|
@@ -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;
|
|
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;AAwBnD,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,CAgKjB,CAAC"}
|
|
@@ -27,6 +27,7 @@ import { to_session_account } from './account_schema.js';
|
|
|
27
27
|
import { query_session_list_for_account, query_session_revoke_for_account, query_session_revoke_all_for_account } from './session_queries.js';
|
|
28
28
|
import { query_api_token_enforce_limit, query_api_token_list_for_account, query_create_api_token, query_revoke_api_token_for_account } from './api_token_queries.js';
|
|
29
29
|
import { parse_stored_token_scope, token_scope_from_input, token_scope_label } from './token_scope.js';
|
|
30
|
+
import { token_lifetime_to_expires_at } from './token_lifetime.js';
|
|
30
31
|
import { generate_api_token } from './api_token.js';
|
|
31
32
|
import { DEFAULT_MAX_TOKENS } from './account_route_schema.js';
|
|
32
33
|
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';
|
|
@@ -102,7 +103,10 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
102
103
|
// There is deliberately no permissive fallback here: an omitted scope is
|
|
103
104
|
// a validation error upstream, not a full-authority token.
|
|
104
105
|
const scope = token_scope_from_input(input.scope);
|
|
105
|
-
|
|
106
|
+
// `input.lifetime` is required for the same reason — an omitted lifetime
|
|
107
|
+
// is a validation error, never an eternal token.
|
|
108
|
+
const expires_at = token_lifetime_to_expires_at(input.lifetime);
|
|
109
|
+
await query_create_api_token(ctx, id, ctx.auth.account.id, input.name, token_hash, scope, expires_at);
|
|
106
110
|
if (max_tokens != null) {
|
|
107
111
|
await query_api_token_enforce_limit(ctx, ctx.auth.account.id, max_tokens);
|
|
108
112
|
}
|
|
@@ -116,7 +120,7 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
116
120
|
credential_type: ctx.credential_type ?? undefined
|
|
117
121
|
}
|
|
118
122
|
});
|
|
119
|
-
return { ok: true, token, id, name: input.name };
|
|
123
|
+
return { ok: true, token, id, name: input.name, expires_at: expires_at?.toISOString() ?? null };
|
|
120
124
|
};
|
|
121
125
|
const token_list_handler = async (_input, ctx) => {
|
|
122
126
|
const rows = await query_api_token_list_for_account(ctx, ctx.auth.account.id);
|
|
@@ -49,12 +49,16 @@ import { type AuditLogConfig, type AuditLogEvent, type AuditLogInput } from './a
|
|
|
49
49
|
* `pending_effects` queue. The bound emitter carries its own `log`
|
|
50
50
|
* reference inside the closure, so per-call contexts don't need one.
|
|
51
51
|
*
|
|
52
|
-
* Audit emits are eager
|
|
52
|
+
* Audit emits are eager by default: the bound emitter fires the
|
|
53
53
|
* pool write immediately and pushes the in-flight `Promise<void>` here.
|
|
54
|
-
*
|
|
55
|
-
* are already rollback-resilient because they run
|
|
56
|
-
* transaction, so
|
|
57
|
-
*
|
|
54
|
+
* Attempt/failure audits never go through `emit_after_commit` —
|
|
55
|
+
* pool-routed writes are already rollback-resilient because they run
|
|
56
|
+
* outside the request transaction, so deferring them would only delay
|
|
57
|
+
* forensic visibility without any safety benefit. The exception is a
|
|
58
|
+
* **success-only** event paired with a state mutation (the
|
|
59
|
+
* `db_admin_row_delete` emit in `http/db_routes.ts`): there the caller
|
|
60
|
+
* wraps the emit in `emit_after_commit` so the trail can't claim a
|
|
61
|
+
* mutation whose transaction failed at COMMIT.
|
|
58
62
|
*
|
|
59
63
|
* Both `RouteContext` and `ActionContext` structurally satisfy this
|
|
60
64
|
* shape (they each carry `pending_effects`), so handlers pass `route`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_emitter.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_emitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,uBAAuB,CAAC;AAE/B
|
|
1
|
+
{"version":3,"file":"audit_emitter.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_emitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,mBAAmB;IACnC,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACrE,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B;;;;;;;;;;;;;;;;OAgBG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,mBAAmB,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChF;;;;;;;OAOG;IACH,sBAAsB,CAAC,CAAC,SAAS,MAAM,EACtC,GAAG,EAAE,yBAAyB,EAC9B,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE;QACN,UAAU,EAAE,CAAC,CAAC;QACd,iBAAiB,EAAE,IAAI,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;QAC7B,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACvC,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;KAChC,GACC,IAAI,CAAC;IACR;;;;;;;;;;OAUG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,IAAI,CAAC;IAC7D,+EAA+E;IAC/E,cAAc,IAAI,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,MAAM,EAC1C,GAAG,EAAE,mBAAmB,EACxB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KACnB,IAAI,CAAC;AAEV;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW,CAAC;AAEhE,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB;IACzC,oDAAoD;IACpD,EAAE,EAAE,EAAE,CAAC;IACP,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAC;IAClC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,yBAAyB,KAAG,YAuFzE,CAAC"}
|
|
@@ -16,7 +16,7 @@ import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
|
16
16
|
* Not a security boundary — in-process code has many other paths to subvert
|
|
17
17
|
* audit logging.
|
|
18
18
|
*/
|
|
19
|
-
export declare const AUDIT_EVENT_TYPES: readonly ["login", "logout", "bootstrap", "signup", "password_change", "session_revoke", "session_revoke_all", "token_create", "token_revoke", "token_revoke_all", "role_grant_create", "role_grant_revoke", "role_grant_offer_create", "role_grant_offer_accept", "role_grant_offer_decline", "role_grant_offer_retract", "role_grant_offer_expire", "role_grant_offer_supersede", "invite_create", "invite_delete", "account_delete", "account_purge", "account_undelete", "actor_delete", "actor_purge", "actor_undelete", "app_settings_update"];
|
|
19
|
+
export declare const AUDIT_EVENT_TYPES: readonly ["login", "logout", "bootstrap", "signup", "password_change", "session_revoke", "session_revoke_all", "token_create", "token_revoke", "token_revoke_all", "role_grant_create", "role_grant_revoke", "role_grant_offer_create", "role_grant_offer_accept", "role_grant_offer_decline", "role_grant_offer_retract", "role_grant_offer_expire", "role_grant_offer_supersede", "invite_create", "invite_delete", "account_delete", "account_purge", "account_undelete", "actor_delete", "actor_purge", "actor_undelete", "app_settings_update", "db_admin_row_delete"];
|
|
20
20
|
/** Zod schema for audit event types. */
|
|
21
21
|
export declare const AuditEventType: z.ZodEnum<{
|
|
22
22
|
bootstrap: "bootstrap";
|
|
@@ -46,6 +46,7 @@ export declare const AuditEventType: z.ZodEnum<{
|
|
|
46
46
|
actor_purge: "actor_purge";
|
|
47
47
|
actor_undelete: "actor_undelete";
|
|
48
48
|
app_settings_update: "app_settings_update";
|
|
49
|
+
db_admin_row_delete: "db_admin_row_delete";
|
|
49
50
|
}>;
|
|
50
51
|
export type AuditEventType = z.infer<typeof AuditEventType>;
|
|
51
52
|
/**
|
|
@@ -230,6 +231,11 @@ export declare const audit_metadata_schemas: Readonly<{
|
|
|
230
231
|
old_value: z.ZodUnknown;
|
|
231
232
|
new_value: z.ZodUnknown;
|
|
232
233
|
}, z.core.$loose>;
|
|
234
|
+
db_admin_row_delete: z.ZodObject<{
|
|
235
|
+
table: z.ZodString;
|
|
236
|
+
pk_column: z.ZodString;
|
|
237
|
+
id: z.ZodString;
|
|
238
|
+
}, z.core.$loose>;
|
|
233
239
|
}>;
|
|
234
240
|
/** Mapped type of metadata shapes per event type, derived from Zod schemas. */
|
|
235
241
|
export type AuditMetadataMap = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAmB9C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,
|
|
1
|
+
{"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -57,7 +57,8 @@ export const AUDIT_EVENT_TYPES = Object.freeze([
|
|
|
57
57
|
'actor_delete',
|
|
58
58
|
'actor_purge',
|
|
59
59
|
'actor_undelete',
|
|
60
|
-
'app_settings_update'
|
|
60
|
+
'app_settings_update',
|
|
61
|
+
'db_admin_row_delete'
|
|
61
62
|
]);
|
|
62
63
|
/** Zod schema for audit event types. */
|
|
63
64
|
export const AuditEventType = z.enum(AUDIT_EVENT_TYPES);
|
|
@@ -344,6 +345,20 @@ export const audit_metadata_schemas = Object.freeze({
|
|
|
344
345
|
setting: z.string().meta({ description: 'Name of the setting that changed.' }),
|
|
345
346
|
old_value: z.unknown().meta({ description: 'Setting value before the update.' }),
|
|
346
347
|
new_value: z.unknown().meta({ description: 'Setting value after the update.' })
|
|
348
|
+
}),
|
|
349
|
+
// Row deleted through the generic db-admin table browser
|
|
350
|
+
// (`http/db_routes.ts` / the Rust spine's `fuz_db_admin`). Account-grain
|
|
351
|
+
// attribution (`account_id`, no `actor_id`) — the browser's gate is
|
|
352
|
+
// account-grain by construction. Emitted only on a successful delete;
|
|
353
|
+
// post-composite-refusal the delete is provably single-row, so no count.
|
|
354
|
+
db_admin_row_delete: z.looseObject({
|
|
355
|
+
table: z.string().meta({ description: 'Table the row was deleted from.' }),
|
|
356
|
+
pk_column: z.string().meta({
|
|
357
|
+
description: 'Single primary-key column the `DELETE` filtered on.'
|
|
358
|
+
}),
|
|
359
|
+
id: z.string().meta({
|
|
360
|
+
description: "Primary-key value of the deleted row, as the row's canonical `::text` rendering."
|
|
361
|
+
})
|
|
347
362
|
})
|
|
348
363
|
});
|
|
349
364
|
/**
|
|
@@ -51,8 +51,13 @@ export interface BootstrapAccountDeps {
|
|
|
51
51
|
db: Db;
|
|
52
52
|
/** Path to the bootstrap token file on disk. */
|
|
53
53
|
token_path: string;
|
|
54
|
-
/**
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Hardened secret-file read (see `FsSecureReadDeps.read_secure_file`) —
|
|
56
|
+
* the token mints the keeper account, so a symlinked, group/other-readable,
|
|
57
|
+
* or oversized file must refuse rather than be honored. Throws on refusal;
|
|
58
|
+
* every throw reads as `TOKEN_FILE_MISSING` upstream.
|
|
59
|
+
*/
|
|
60
|
+
read_secure_file: (path: string) => Promise<Uint8Array>;
|
|
56
61
|
/** Delete a file. */
|
|
57
62
|
delete_file: (path: string) => Promise<void>;
|
|
58
63
|
/** Only hashing is needed — verification happens separately during login. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap_account.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_account.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"bootstrap_account.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_account.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EACN,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,gDAAgD;AAChD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,6DAA6D;AAC7D,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IACrD,wFAAwF;IACxF,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED,gCAAgC;AAChC,MAAM,MAAM,uBAAuB,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,0BAA0B,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GACpE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,wBAAwB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAClE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAEjE,qFAAqF;AACrF,MAAM,MAAM,sBAAsB,GAAG,uBAAuB,GAAG,uBAAuB,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,EAAE,CAAC;IACP,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,6EAA6E;IAC7E,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;IAClD,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,oBAAoB,EAC1B,gBAAgB,MAAM,EACtB,OAAO,qBAAqB,KAC1B,OAAO,CAAC,sBAAsB,CA2EhC,CAAC"}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { timingSafeEqual } from 'node:crypto';
|
|
10
|
+
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
10
11
|
import { ERROR_INVALID_TOKEN, ERROR_ALREADY_BOOTSTRAPPED, ERROR_TOKEN_FILE_MISSING } from '../http/error_schemas.js';
|
|
11
12
|
import { ROLE_ADMIN, ROLE_KEEPER } from './role_schema.js';
|
|
12
13
|
import { query_create_account_with_actor } from './account_queries.js';
|
|
@@ -33,13 +34,17 @@ import { query_create_role_grant } from './role_grant_queries.js';
|
|
|
33
34
|
* @mutates filesystem - deletes the bootstrap token file after commit (reported via `token_file_deleted`)
|
|
34
35
|
*/
|
|
35
36
|
export const bootstrap_account = async (deps, provided_token, input) => {
|
|
36
|
-
const { db, token_path,
|
|
37
|
-
// 1. Read and verify token (non-destructive, before transaction)
|
|
37
|
+
const { db, token_path, read_secure_file, delete_file, password, log } = deps;
|
|
38
|
+
// 1. Read and verify token (non-destructive, before transaction). The
|
|
39
|
+
// secure read fails loud on a symlink / permissive mode / oversized file —
|
|
40
|
+
// every refusal masks as the same TOKEN_FILE_MISSING the wire always
|
|
41
|
+
// carried, with the specific reason logged for the operator.
|
|
38
42
|
let expected_token;
|
|
39
43
|
try {
|
|
40
|
-
expected_token = (await
|
|
44
|
+
expected_token = new TextDecoder().decode(await read_secure_file(token_path)).trim();
|
|
41
45
|
}
|
|
42
|
-
catch {
|
|
46
|
+
catch (err) {
|
|
47
|
+
log.warn(`bootstrap token file rejected: ${to_error_message(err)}`);
|
|
43
48
|
return { ok: false, error: ERROR_TOKEN_FILE_MISSING, status: 404 };
|
|
44
49
|
}
|
|
45
50
|
// Defense-in-depth: no .trim() on provided_token — tokens must match exactly.
|
|
@@ -14,7 +14,6 @@ import type { Db } from '../db/db.js';
|
|
|
14
14
|
import { type RouteSpec } from '../http/route_spec.js';
|
|
15
15
|
import { type RateLimiter } from '../rate_limiter.js';
|
|
16
16
|
import type { RouteFactoryDeps } from './deps.js';
|
|
17
|
-
import type { StatResult } from '../runtime/deps.js';
|
|
18
17
|
/**
|
|
19
18
|
* Bootstrap status — runtime state computed once at startup.
|
|
20
19
|
*/
|
|
@@ -54,8 +53,10 @@ export interface BootstrapRouteOptions {
|
|
|
54
53
|
* Dependencies for checking bootstrap status at startup.
|
|
55
54
|
*/
|
|
56
55
|
export interface CheckBootstrapStatusDeps {
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
/** Hardened secret-file read — the same capability the request-time read uses. */
|
|
57
|
+
read_secure_file: (path: string) => Promise<Uint8Array>;
|
|
58
|
+
/** Only the single-row `bootstrap_lock` read — narrower than the full `Db`. */
|
|
59
|
+
db: Pick<Db, 'query_one'>;
|
|
59
60
|
log: Logger;
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
@@ -63,9 +64,16 @@ export interface CheckBootstrapStatusDeps {
|
|
|
63
64
|
*
|
|
64
65
|
* Bootstrap is available when:
|
|
65
66
|
* 1. A token path is configured
|
|
66
|
-
* 2. The token file exists
|
|
67
|
+
* 2. The token file passes the secure read (exists, not a symlink, mode
|
|
68
|
+
* `0600`/`0400`, within the size cap)
|
|
67
69
|
* 3. The `bootstrap_lock` table shows `bootstrapped = false`
|
|
68
70
|
*
|
|
71
|
+
* The probe **reads through the same `read_secure_file` the request-time
|
|
72
|
+
* read uses**, so "bootstrap is available" and "the token file can actually
|
|
73
|
+
* be read" can't drift apart — an availability check laxer than the read it
|
|
74
|
+
* gates is the misconfiguration that reports green at boot and fails at
|
|
75
|
+
* request time. Twin of the Rust spine's `is_bootstrap_available`.
|
|
76
|
+
*
|
|
69
77
|
* @param deps - filesystem and database access for the check
|
|
70
78
|
* @param options - static configuration including `token_path`
|
|
71
79
|
* @returns an object with `available` (boolean) and `token_path` (string | null)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"bootstrap_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGlD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,8EAA8E;IAC9E,gBAAgB,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,uBAAuB,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E;;;;;;;;;;OAUG;IACH,yBAAyB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,kFAAkF;IAClF,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,+EAA+E;IAC/E,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,wBAAwB,EAC9B,SAAS;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,KACpC,OAAO,CAAC,eAAe,CAyBzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,gBAAgB,EACtB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CAmHjB,CAAC"}
|
|
@@ -13,28 +13,37 @@ import { bootstrap_route_shape } from './bootstrap_route_schema.js';
|
|
|
13
13
|
import { get_route_input } from '../http/route_spec.js';
|
|
14
14
|
import { get_client_ip } from '../http/client_ip.js';
|
|
15
15
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
16
|
-
import { ERROR_ALREADY_BOOTSTRAPPED } from '../http/error_schemas.js';
|
|
16
|
+
import { ERROR_ALREADY_BOOTSTRAPPED, ERROR_TOKEN_FILE_MISSING } from '../http/error_schemas.js';
|
|
17
17
|
/**
|
|
18
18
|
* Check bootstrap availability at startup.
|
|
19
19
|
*
|
|
20
20
|
* Bootstrap is available when:
|
|
21
21
|
* 1. A token path is configured
|
|
22
|
-
* 2. The token file exists
|
|
22
|
+
* 2. The token file passes the secure read (exists, not a symlink, mode
|
|
23
|
+
* `0600`/`0400`, within the size cap)
|
|
23
24
|
* 3. The `bootstrap_lock` table shows `bootstrapped = false`
|
|
24
25
|
*
|
|
26
|
+
* The probe **reads through the same `read_secure_file` the request-time
|
|
27
|
+
* read uses**, so "bootstrap is available" and "the token file can actually
|
|
28
|
+
* be read" can't drift apart — an availability check laxer than the read it
|
|
29
|
+
* gates is the misconfiguration that reports green at boot and fails at
|
|
30
|
+
* request time. Twin of the Rust spine's `is_bootstrap_available`.
|
|
31
|
+
*
|
|
25
32
|
* @param deps - filesystem and database access for the check
|
|
26
33
|
* @param options - static configuration including `token_path`
|
|
27
34
|
* @returns an object with `available` (boolean) and `token_path` (string | null)
|
|
28
35
|
*/
|
|
29
36
|
export const check_bootstrap_status = async (deps, options) => {
|
|
30
|
-
const {
|
|
37
|
+
const { read_secure_file, db, log } = deps;
|
|
31
38
|
const { token_path } = options;
|
|
32
39
|
if (!token_path) {
|
|
33
40
|
return { available: false, token_path: null };
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
try {
|
|
43
|
+
await read_secure_file(token_path);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
log.info(`Bootstrap unavailable: ${to_error_message(err)}`);
|
|
38
47
|
return { available: false, token_path };
|
|
39
48
|
}
|
|
40
49
|
const lock_row = await db.query_one('SELECT bootstrapped FROM bootstrap_lock WHERE id = 1');
|
|
@@ -81,12 +90,22 @@ export const create_bootstrap_route_specs = (deps, options) => {
|
|
|
81
90
|
const result = await bootstrap_account({
|
|
82
91
|
db: route.db,
|
|
83
92
|
token_path,
|
|
84
|
-
|
|
93
|
+
read_secure_file: deps.read_secure_file,
|
|
85
94
|
delete_file: deps.delete_file,
|
|
86
95
|
password: deps.password,
|
|
87
96
|
log: deps.log
|
|
88
97
|
}, input.token, input);
|
|
89
98
|
if (!result.ok) {
|
|
99
|
+
// An unreadable token file closes the window. Bootstrap cannot
|
|
100
|
+
// succeed without one, and `check_bootstrap_status` already reads
|
|
101
|
+
// an unreadable file as unavailable at startup — so leaving the
|
|
102
|
+
// flag set means the two disagree, and every later request takes
|
|
103
|
+
// this leg and writes another audit row. The limiter would bound
|
|
104
|
+
// that channel; closing it ends it, and stops `/status`
|
|
105
|
+
// advertising a window that can't be walked through. Twin of the
|
|
106
|
+
// Rust spine's `bootstrap_handler`, which does the same.
|
|
107
|
+
if (result.error === ERROR_TOKEN_FILE_MISSING)
|
|
108
|
+
bootstrap_status.available = false;
|
|
90
109
|
if (bootstrap_ip_rate_limiter && ip)
|
|
91
110
|
bootstrap_ip_rate_limiter.record(ip);
|
|
92
111
|
deps.audit.emit(route, {
|