@fuzdev/fuz_app 0.109.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.
Files changed (84) hide show
  1. package/dist/auth/CLAUDE.md +6 -5
  2. package/dist/auth/account_action_specs.d.ts +18 -0
  3. package/dist/auth/account_action_specs.d.ts.map +1 -1
  4. package/dist/auth/account_action_specs.js +14 -1
  5. package/dist/auth/account_actions.d.ts.map +1 -1
  6. package/dist/auth/account_actions.js +6 -2
  7. package/dist/auth/bootstrap_account.d.ts +7 -2
  8. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  9. package/dist/auth/bootstrap_account.js +9 -4
  10. package/dist/auth/bootstrap_routes.d.ts +12 -4
  11. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  12. package/dist/auth/bootstrap_routes.js +15 -6
  13. package/dist/auth/daemon_token_middleware.d.ts +7 -69
  14. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  15. package/dist/auth/daemon_token_middleware.js +8 -104
  16. package/dist/auth/deps.d.ts +9 -5
  17. package/dist/auth/deps.d.ts.map +1 -1
  18. package/dist/auth/middleware.js +1 -1
  19. package/dist/auth/request_context.d.ts +1 -3
  20. package/dist/auth/request_context.d.ts.map +1 -1
  21. package/dist/auth/request_context.js +2 -5
  22. package/dist/auth/session_cookie.d.ts +18 -36
  23. package/dist/auth/session_cookie.d.ts.map +1 -1
  24. package/dist/auth/session_cookie.js +16 -26
  25. package/dist/auth/session_middleware.d.ts +1 -1
  26. package/dist/auth/session_middleware.d.ts.map +1 -1
  27. package/dist/auth/session_middleware.js +1 -4
  28. package/dist/auth/session_queries.d.ts +14 -30
  29. package/dist/auth/session_queries.d.ts.map +1 -1
  30. package/dist/auth/session_queries.js +15 -45
  31. package/dist/auth/token_lifetime.d.ts +51 -0
  32. package/dist/auth/token_lifetime.d.ts.map +1 -0
  33. package/dist/auth/token_lifetime.js +54 -0
  34. package/dist/dev/setup.d.ts +5 -9
  35. package/dist/dev/setup.d.ts.map +1 -1
  36. package/dist/dev/setup.js +18 -21
  37. package/dist/runtime/deno.d.ts.map +1 -1
  38. package/dist/runtime/deno.js +27 -1
  39. package/dist/runtime/deps.d.ts +38 -4
  40. package/dist/runtime/deps.d.ts.map +1 -1
  41. package/dist/runtime/fs.d.ts +24 -5
  42. package/dist/runtime/fs.d.ts.map +1 -1
  43. package/dist/runtime/fs.js +38 -7
  44. package/dist/runtime/mock.d.ts +7 -0
  45. package/dist/runtime/mock.d.ts.map +1 -1
  46. package/dist/runtime/mock.js +41 -1
  47. package/dist/runtime/node.d.ts.map +1 -1
  48. package/dist/runtime/node.js +7 -1
  49. package/dist/runtime/secure_file.d.ts +71 -0
  50. package/dist/runtime/secure_file.d.ts.map +1 -0
  51. package/dist/runtime/secure_file.js +125 -0
  52. package/dist/server/app_backend.d.ts +6 -5
  53. package/dist/server/app_backend.d.ts.map +1 -1
  54. package/dist/server/app_backend.js +2 -3
  55. package/dist/testing/CLAUDE.md +15 -1
  56. package/dist/testing/admin_integration.d.ts.map +1 -1
  57. package/dist/testing/admin_integration.js +10 -2
  58. package/dist/testing/app_server.d.ts.map +1 -1
  59. package/dist/testing/app_server.js +13 -8
  60. package/dist/testing/audit_completeness.d.ts.map +1 -1
  61. package/dist/testing/audit_completeness.js +5 -1
  62. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  63. package/dist/testing/cross_backend/cell_grant_role.js +34 -1
  64. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/conformance_table.js +2 -1
  66. package/dist/testing/cross_backend/setup.js +1 -1
  67. package/dist/testing/cross_backend/token_lifetime.d.ts +15 -0
  68. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -0
  69. package/dist/testing/cross_backend/token_lifetime.js +99 -0
  70. package/dist/testing/cross_backend/token_scope_surface.js +1 -1
  71. package/dist/testing/daemon_token_rotation.d.ts +83 -0
  72. package/dist/testing/daemon_token_rotation.d.ts.map +1 -0
  73. package/dist/testing/daemon_token_rotation.js +99 -0
  74. package/dist/testing/integration.d.ts.map +1 -1
  75. package/dist/testing/integration.js +15 -3
  76. package/dist/testing/stubs.d.ts.map +1 -1
  77. package/dist/testing/stubs.js +4 -4
  78. package/dist/ui/AccountSessions.svelte +0 -5
  79. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  80. package/dist/ui/AdminOverview.svelte +7 -4
  81. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  82. package/dist/ui/AdminSessions.svelte +0 -5
  83. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  84. package/package.json +1 -1
@@ -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: `stat`, `read_text_file`, `delete_file`, `keyring`, `password`, `db`, `log`, `audit`.
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`, `create_session_config`, `fuz_session_config`, `SESSION_AGE_MAX`, `SESSION_REFRESH_THRESHOLD_S`.
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` — `start_daemon_token_rotation` + `create_daemon_token_middleware(state, deps, log)` (atomic file write, soft-fail validation, keeper account resolution). 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
+ - `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
@@ -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?}`; output `{ok, token, id, 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":"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"}
1
+ {"version":3,"file":"account_action_specs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_action_specs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAW3E,6EAA6E;AAC7E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,WAAW,CAAC;AACzC,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,yCAAyC;AACzC,eAAO,MAAM,iBAAiB;;;;;;;;kBAE5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB;;kBAE7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,iFAAiF;AACjF,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,6DAA6D;AAC7D,eAAO,MAAM,qBAAqB,WAAW,CAAC;AAC9C,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E,+CAA+C;AAC/C,eAAO,MAAM,sBAAsB;;;kBAGjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;kBAgB3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB;;;;;;kBAS5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,qDAAqD;AACrD,eAAO,MAAM,cAAc,WAAW,CAAC;AACvC,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,4DAA4D;AAC5D,eAAO,MAAM,eAAe;;;;;;;;;;;kBAE1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,wCAAwC;AACxC,eAAO,MAAM,gBAAgB;;kBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,+EAA+E;AAC/E,eAAO,MAAM,iBAAiB;;;kBAG5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAIlE,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;CAUF,CAAC;AAEtC,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;;;CAUR,CAAC;AAKtC,eAAO,MAAM,kCAAkC;;;;;;;;;;;;;;;;;;;CAUV,CAAC;AAGtC,eAAO,MAAM,sCAAsC;;;;;;;;;;;;;;;;;CAUd,CAAC;AAEtC;;;;;;;;;GASG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWR,CAAC;AAEtC,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;CAUN,CAAC;AAGtC,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAUR,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,KAAK,CAAC,yBAAyB,CAQrE,CAAC"}
@@ -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;AAoBxE,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,CAqJjB,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
- await query_create_api_token(ctx, id, ctx.auth.account.id, input.name, token_hash, scope);
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);
@@ -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
- /** Read a file's contents as a string. */
55
- read_text_file: (path: string) => Promise<string>;
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;AAEtD,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,0CAA0C;IAC1C,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,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,CAuEhC,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, read_text_file, delete_file, password, log } = deps;
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 read_text_file(token_path)).trim();
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
- stat: (path: string) => Promise<StatResult | null>;
58
- db: Db;
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 on disk
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;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD;;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,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,EAAE,EAAE,EAAE,CAAC;IACP,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,wBAAwB,EAC9B,SAAS;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,KACpC,OAAO,CAAC,eAAe,CAwBzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,gBAAgB,EACtB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CAmHjB,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"}
@@ -19,22 +19,31 @@ import { ERROR_ALREADY_BOOTSTRAPPED, ERROR_TOKEN_FILE_MISSING } from '../http/er
19
19
  *
20
20
  * Bootstrap is available when:
21
21
  * 1. A token path is configured
22
- * 2. The token file exists on disk
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 { stat, db, log } = deps;
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
- const token_stat = await stat(token_path);
36
- if (token_stat === null) {
37
- log.info('Bootstrap unavailable: token file not found');
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,7 +90,7 @@ 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
- read_text_file: deps.read_text_file,
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
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Daemon token rotation, persistence, and middleware.
2
+ * Daemon token middleware — the credential **consumer**.
3
3
  *
4
- * Manages the lifecycle of filesystem-resident daemon tokens: writing to disk,
5
- * rotation on an interval, and HTTP middleware for authentication.
4
+ * Validates a presented `X-Daemon-Token` and resolves the keeper account.
5
+ * The producer half (rotation + file persistence) lives in
6
+ * `testing/daemon_token_rotation.ts` behind the dev-env guard: no production
7
+ * assembly mints daemon tokens — the credential's only remaining role is the
8
+ * cross-process test harness's keeper channel — mirroring the Rust spine,
9
+ * whose producer is confined to `fuz_testing`.
6
10
  *
7
11
  * Pure token primitives (schema, generation, validation) live in `auth/daemon_token.ts`.
8
12
  * See docs/identity.md for design rationale.
@@ -11,40 +15,8 @@
11
15
  */
12
16
  import type { MiddlewareHandler } from 'hono';
13
17
  import type { Logger } from '@fuzdev/fuz_util/log.ts';
14
- import { type FsWriteDeps, type FsRemoveDeps, type EnvDeps } from '../runtime/deps.js';
15
18
  import type { QueryDeps } from '../db/query_deps.js';
16
19
  import { type DaemonTokenState } from './daemon_token.js';
17
- /** Default rotation interval in milliseconds (30 seconds). */
18
- export declare const DEFAULT_ROTATION_INTERVAL_MS = 30000;
19
- /** Deps for writing the daemon token to disk. */
20
- export type DaemonTokenWriteDeps = Pick<EnvDeps, 'env_get'> & Pick<FsWriteDeps, 'mkdir' | 'write_text_file' | 'rename'> & {
21
- /** Set file permissions. Optional — consumers provide when available (e.g. `Deno.chmod`). */
22
- chmod?: (path: string, mode: number) => Promise<void>;
23
- };
24
- /**
25
- * Get the daemon token file path (`~/.{name}/run/daemon_token`).
26
- *
27
- * @param runtime - runtime with `env_get` capability
28
- * @param name - application name
29
- * @returns path to `daemon_token`, or `null` if `$HOME` is not set
30
- */
31
- export declare const get_daemon_token_path: (runtime: Pick<EnvDeps, "env_get">, name: string) => string | null;
32
- /**
33
- * Write the current token to disk atomically.
34
- *
35
- * Uses `write_file_atomic` (temp file + rename) and optionally sets mode 0600.
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
- *
42
- * @param runtime - runtime with file write capabilities
43
- * @param token_path - path to write the token
44
- * @param token - the raw token string
45
- * @mutates filesystem - writes `token_path` atomically and `chmod 0600` when supported
46
- */
47
- export declare const write_daemon_token: (runtime: DaemonTokenWriteDeps, token_path: string, token: string) => Promise<void>;
48
20
  /**
49
21
  * Resolve the keeper account ID by querying for the account with an active
50
22
  * keeper role_grant.
@@ -61,40 +33,6 @@ export declare const write_daemon_token: (runtime: DaemonTokenWriteDeps, token_p
61
33
  * @returns the keeper account ID, or `null` if no keeper exists yet (pre-bootstrap)
62
34
  */
63
35
  export declare const resolve_keeper_account_id: (deps: QueryDeps) => Promise<string | null>;
64
- /** Options for daemon token rotation. */
65
- export interface DaemonTokenRotationOptions {
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;
74
- /** Rotation interval in ms. Default: `30000` (30s). */
75
- rotation_interval_ms?: number;
76
- }
77
- /** Result of starting daemon token rotation. */
78
- export interface DaemonTokenRotation {
79
- /** The mutable runtime state. Pass to `create_daemon_token_middleware`. */
80
- state: DaemonTokenState;
81
- /** Stop rotation, clean up the interval, and delete the token file. Call on graceful shutdown. */
82
- stop: () => Promise<void>;
83
- }
84
- /**
85
- * Start daemon token rotation.
86
- *
87
- * Generates an initial token, writes it to disk, resolves the keeper account,
88
- * and sets up periodic rotation. Returns the mutable state object and a stop function.
89
- *
90
- * @param runtime - runtime with file and remove capabilities
91
- * @param deps - query dependencies for resolving keeper account
92
- * @param options - rotation configuration
93
- * @param log - the logger instance
94
- * @returns rotation state and stop function
95
- * @mutates filesystem - writes the token file on each rotation; `stop` removes it
96
- */
97
- export declare const start_daemon_token_rotation: (runtime: DaemonTokenWriteDeps & FsRemoveDeps, deps: QueryDeps, options: DaemonTokenRotationOptions, log: Logger) => Promise<DaemonTokenRotation>;
98
36
  /**
99
37
  * Create middleware that authenticates via daemon token.
100
38
  *
@@ -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;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAYvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,8BAA8B,GAC1C,OAAO,gBAAgB,EACvB,MAAM,SAAS,EACf,KAAK,MAAM,KACT,iBA0EF,CAAC"}
1
+ {"version":3,"file":"daemon_token_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/daemon_token_middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAWtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,yBAAyB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAEtF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,8BAA8B,GAC1C,OAAO,gBAAgB,EACvB,MAAM,SAAS,EACf,KAAK,MAAM,KACT,iBA0EF,CAAC"}
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Daemon token rotation, persistence, and middleware.
2
+ * Daemon token middleware — the credential **consumer**.
3
3
  *
4
- * Manages the lifecycle of filesystem-resident daemon tokens: writing to disk,
5
- * rotation on an interval, and HTTP middleware for authentication.
4
+ * Validates a presented `X-Daemon-Token` and resolves the keeper account.
5
+ * The producer half (rotation + file persistence) lives in
6
+ * `testing/daemon_token_rotation.ts` behind the dev-env guard: no production
7
+ * assembly mints daemon tokens — the credential's only remaining role is the
8
+ * cross-process test harness's keeper channel — mirroring the Rust spine,
9
+ * whose producer is confined to `fuz_testing`.
6
10
  *
7
11
  * Pure token primitives (schema, generation, validation) live in `auth/daemon_token.ts`.
8
12
  * See docs/identity.md for design rationale.
@@ -10,49 +14,12 @@
10
14
  * @module
11
15
  */
12
16
  import { DEV } from 'esm-env';
13
- import {} from '../runtime/deps.js';
14
- import { write_file_atomic } from '../runtime/fs.js';
15
- import { get_app_dir } from '../cli/config.js';
16
17
  import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY } from '../hono_context.js';
17
18
  import { token_scope_full } from './token_scope.js';
18
19
  import { is_browser_context } from '../http/origin.js';
19
20
  import { query_role_grant_find_account_id_for_role } from './role_grant_queries.js';
20
21
  import { ROLE_KEEPER } from './role_schema.js';
21
- import { DaemonToken, DAEMON_TOKEN_HEADER, generate_daemon_token, validate_daemon_token } from './daemon_token.js';
22
- /** Default rotation interval in milliseconds (30 seconds). */
23
- export const DEFAULT_ROTATION_INTERVAL_MS = 30_000;
24
- /**
25
- * Get the daemon token file path (`~/.{name}/run/daemon_token`).
26
- *
27
- * @param runtime - runtime with `env_get` capability
28
- * @param name - application name
29
- * @returns path to `daemon_token`, or `null` if `$HOME` is not set
30
- */
31
- export const get_daemon_token_path = (runtime, name) => {
32
- const app_dir = get_app_dir(runtime, name);
33
- return app_dir ? `${app_dir}/run/daemon_token` : null;
34
- };
35
- /**
36
- * Write the current token to disk atomically.
37
- *
38
- * Uses `write_file_atomic` (temp file + rename) and optionally sets mode 0600.
39
- *
40
- * On-disk format is JSON `{"token": "..."}` — the wrapper leaves room for
41
- * future fields (rotated_at, version) without changing every reader. Both
42
- * the TS cross-backend harness reader (`spawn_backend.read_daemon_token`)
43
- * and the Rust daemon-token writer match this shape.
44
- *
45
- * @param runtime - runtime with file write capabilities
46
- * @param token_path - path to write the token
47
- * @param token - the raw token string
48
- * @mutates filesystem - writes `token_path` atomically and `chmod 0600` when supported
49
- */
50
- export const write_daemon_token = async (runtime, token_path, token) => {
51
- await write_file_atomic(runtime, token_path, JSON.stringify({ token }) + '\n');
52
- if (runtime.chmod) {
53
- await runtime.chmod(token_path, 0o600);
54
- }
55
- };
22
+ import { DaemonToken, DAEMON_TOKEN_HEADER, validate_daemon_token } from './daemon_token.js';
56
23
  /**
57
24
  * Resolve the keeper account ID by querying for the account with an active
58
25
  * keeper role_grant.
@@ -71,69 +38,6 @@ export const write_daemon_token = async (runtime, token_path, token) => {
71
38
  export const resolve_keeper_account_id = async (deps) => {
72
39
  return query_role_grant_find_account_id_for_role(deps, ROLE_KEEPER);
73
40
  };
74
- /**
75
- * Start daemon token rotation.
76
- *
77
- * Generates an initial token, writes it to disk, resolves the keeper account,
78
- * and sets up periodic rotation. Returns the mutable state object and a stop function.
79
- *
80
- * @param runtime - runtime with file and remove capabilities
81
- * @param deps - query dependencies for resolving keeper account
82
- * @param options - rotation configuration
83
- * @param log - the logger instance
84
- * @returns rotation state and stop function
85
- * @mutates filesystem - writes the token file on each rotation; `stop` removes it
86
- */
87
- export const start_daemon_token_rotation = async (runtime, deps, options, log) => {
88
- const { token_path, rotation_interval_ms = DEFAULT_ROTATION_INTERVAL_MS } = options;
89
- // ensure parent directory exists
90
- const last_slash = token_path.lastIndexOf('/');
91
- if (last_slash > 0) {
92
- await runtime.mkdir(token_path.slice(0, last_slash), { recursive: true });
93
- }
94
- // resolve keeper account (may be null pre-bootstrap; the middleware
95
- // lazily refreshes on the first null hit to cover the
96
- // rotation-starts-before-bootstrap case)
97
- const keeper_account_id = await resolve_keeper_account_id(deps);
98
- // generate initial token and write to disk
99
- const initial_token = generate_daemon_token();
100
- await write_daemon_token(runtime, token_path, initial_token);
101
- const state = {
102
- current_token: initial_token,
103
- previous_token: null,
104
- rotated_at: new Date(),
105
- keeper_account_id
106
- };
107
- let writing = false;
108
- const interval_id = setInterval(async () => {
109
- if (writing)
110
- return; // skip if previous rotation write still in progress
111
- writing = true;
112
- try {
113
- const new_token = generate_daemon_token();
114
- state.previous_token = state.current_token;
115
- state.current_token = new_token;
116
- state.rotated_at = new Date();
117
- await write_daemon_token(runtime, token_path, new_token);
118
- }
119
- catch (err) {
120
- log.error('Failed to write rotated token:', err);
121
- }
122
- finally {
123
- writing = false;
124
- }
125
- }, rotation_interval_ms);
126
- const stop = async () => {
127
- clearInterval(interval_id);
128
- try {
129
- await runtime.remove(token_path);
130
- }
131
- catch {
132
- // already removed or never written
133
- }
134
- };
135
- return { state, stop };
136
- };
137
41
  /**
138
42
  * Create middleware that authenticates via daemon token.
139
43
  *
@@ -12,7 +12,6 @@ import type { FactStore } from '@fuzdev/fuz_util/fact_store.ts';
12
12
  import type { Keyring } from './keyring.js';
13
13
  import type { PasswordHashDeps } from './password.js';
14
14
  import type { Db } from '../db/db.js';
15
- import type { StatResult } from '../runtime/deps.js';
16
15
  import type { AuditEmitter } from './audit_emitter.js';
17
16
  /**
18
17
  * Stateless capabilities bundle for fuz_app backends.
@@ -21,10 +20,15 @@ import type { AuditEmitter } from './audit_emitter.js';
21
20
  * Does not contain config (static values) or runtime state (mutable refs).
22
21
  */
23
22
  export interface AppDeps {
24
- /** Get file/directory stats, or null if path doesn't exist. */
25
- stat: (path: string) => Promise<StatResult | null>;
26
- /** Read a file as text. */
27
- read_text_file: (path: string) => Promise<string>;
23
+ /**
24
+ * Hardened secret-file read — used for the bootstrap token (the file that
25
+ * mints the keeper account). Production wiring passes the runtime's
26
+ * `read_secure_file` (`FsSecureReadDeps`), which rejects symlinks,
27
+ * group/other-accessible modes, and oversized files; both the boot-time
28
+ * availability probe and the request-time read go through this one
29
+ * capability so the probe can never be laxer than the read it gates.
30
+ */
31
+ read_secure_file: (path: string) => Promise<Uint8Array>;
28
32
  /** Delete a file. */
29
33
  delete_file: (path: string) => Promise<void>;
30
34
  /** HMAC-SHA256 cookie signing keyring. */