@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.
Files changed (122) hide show
  1. package/README.md +4 -0
  2. package/dist/actions/CLAUDE.md +1 -1
  3. package/dist/auth/CLAUDE.md +7 -6
  4. package/dist/auth/account_action_specs.d.ts +18 -0
  5. package/dist/auth/account_action_specs.d.ts.map +1 -1
  6. package/dist/auth/account_action_specs.js +14 -1
  7. package/dist/auth/account_actions.d.ts.map +1 -1
  8. package/dist/auth/account_actions.js +6 -2
  9. package/dist/auth/audit_emitter.d.ts +9 -5
  10. package/dist/auth/audit_emitter.d.ts.map +1 -1
  11. package/dist/auth/audit_log_schema.d.ts +7 -1
  12. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  13. package/dist/auth/audit_log_schema.js +16 -1
  14. package/dist/auth/bootstrap_account.d.ts +7 -2
  15. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  16. package/dist/auth/bootstrap_account.js +9 -4
  17. package/dist/auth/bootstrap_routes.d.ts +12 -4
  18. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  19. package/dist/auth/bootstrap_routes.js +26 -7
  20. package/dist/auth/daemon_token_middleware.d.ts +7 -69
  21. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  22. package/dist/auth/daemon_token_middleware.js +8 -104
  23. package/dist/auth/deps.d.ts +9 -5
  24. package/dist/auth/deps.d.ts.map +1 -1
  25. package/dist/auth/middleware.js +1 -1
  26. package/dist/auth/migrations.d.ts +7 -0
  27. package/dist/auth/migrations.d.ts.map +1 -1
  28. package/dist/auth/migrations.js +25 -8
  29. package/dist/auth/request_context.d.ts +1 -3
  30. package/dist/auth/request_context.d.ts.map +1 -1
  31. package/dist/auth/request_context.js +2 -5
  32. package/dist/auth/session_cookie.d.ts +18 -36
  33. package/dist/auth/session_cookie.d.ts.map +1 -1
  34. package/dist/auth/session_cookie.js +16 -26
  35. package/dist/auth/session_middleware.d.ts +1 -1
  36. package/dist/auth/session_middleware.d.ts.map +1 -1
  37. package/dist/auth/session_middleware.js +1 -4
  38. package/dist/auth/session_queries.d.ts +14 -30
  39. package/dist/auth/session_queries.d.ts.map +1 -1
  40. package/dist/auth/session_queries.js +15 -45
  41. package/dist/auth/token_lifetime.d.ts +51 -0
  42. package/dist/auth/token_lifetime.d.ts.map +1 -0
  43. package/dist/auth/token_lifetime.js +54 -0
  44. package/dist/db/CLAUDE.md +9 -5
  45. package/dist/db/cell_audit_queries.d.ts +4 -2
  46. package/dist/db/cell_audit_queries.d.ts.map +1 -1
  47. package/dist/db/cell_audit_queries.js +4 -2
  48. package/dist/db/pg_error.d.ts +12 -0
  49. package/dist/db/pg_error.d.ts.map +1 -1
  50. package/dist/db/pg_error.js +15 -1
  51. package/dist/dev/setup.d.ts +5 -9
  52. package/dist/dev/setup.d.ts.map +1 -1
  53. package/dist/dev/setup.js +18 -21
  54. package/dist/http/CLAUDE.md +78 -18
  55. package/dist/http/db_routes.d.ts +96 -3
  56. package/dist/http/db_routes.d.ts.map +1 -1
  57. package/dist/http/db_routes.js +263 -46
  58. package/dist/http/error_schemas.d.ts +12 -1
  59. package/dist/http/error_schemas.d.ts.map +1 -1
  60. package/dist/http/error_schemas.js +12 -1
  61. package/dist/http/pending_effects.d.ts +5 -5
  62. package/dist/http/pending_effects.d.ts.map +1 -1
  63. package/dist/http/pending_effects.js +1 -1
  64. package/dist/runtime/deno.d.ts.map +1 -1
  65. package/dist/runtime/deno.js +27 -1
  66. package/dist/runtime/deps.d.ts +38 -4
  67. package/dist/runtime/deps.d.ts.map +1 -1
  68. package/dist/runtime/fs.d.ts +24 -5
  69. package/dist/runtime/fs.d.ts.map +1 -1
  70. package/dist/runtime/fs.js +38 -7
  71. package/dist/runtime/mock.d.ts +7 -0
  72. package/dist/runtime/mock.d.ts.map +1 -1
  73. package/dist/runtime/mock.js +41 -1
  74. package/dist/runtime/node.d.ts.map +1 -1
  75. package/dist/runtime/node.js +7 -1
  76. package/dist/runtime/secure_file.d.ts +71 -0
  77. package/dist/runtime/secure_file.d.ts.map +1 -0
  78. package/dist/runtime/secure_file.js +125 -0
  79. package/dist/server/app_backend.d.ts +6 -5
  80. package/dist/server/app_backend.d.ts.map +1 -1
  81. package/dist/server/app_backend.js +2 -3
  82. package/dist/testing/CLAUDE.md +36 -1
  83. package/dist/testing/admin_integration.d.ts.map +1 -1
  84. package/dist/testing/admin_integration.js +10 -2
  85. package/dist/testing/app_server.d.ts.map +1 -1
  86. package/dist/testing/app_server.js +13 -8
  87. package/dist/testing/audit_completeness.d.ts.map +1 -1
  88. package/dist/testing/audit_completeness.js +10 -2
  89. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  90. package/dist/testing/cross_backend/cell_grant_role.js +34 -1
  91. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  92. package/dist/testing/cross_backend/cell_relations.js +53 -1
  93. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  94. package/dist/testing/cross_backend/conformance_table.js +2 -1
  95. package/dist/testing/cross_backend/query_shape.d.ts +10 -0
  96. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
  97. package/dist/testing/cross_backend/query_shape.js +128 -0
  98. package/dist/testing/cross_backend/setup.js +1 -1
  99. package/dist/testing/cross_backend/token_lifetime.d.ts +15 -0
  100. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/token_lifetime.js +99 -0
  102. package/dist/testing/cross_backend/token_scope_surface.js +1 -1
  103. package/dist/testing/daemon_token_rotation.d.ts +83 -0
  104. package/dist/testing/daemon_token_rotation.d.ts.map +1 -0
  105. package/dist/testing/daemon_token_rotation.js +99 -0
  106. package/dist/testing/integration.d.ts.map +1 -1
  107. package/dist/testing/integration.js +15 -3
  108. package/dist/testing/stubs.d.ts.map +1 -1
  109. package/dist/testing/stubs.js +4 -4
  110. package/dist/ui/AccountSessions.svelte +0 -5
  111. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  112. package/dist/ui/AdminOverview.svelte +7 -4
  113. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  114. package/dist/ui/AdminSessions.svelte +0 -5
  115. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  116. package/dist/ui/CLAUDE.md +9 -4
  117. package/dist/ui/table_state.svelte.d.ts +18 -5
  118. package/dist/ui/table_state.svelte.d.ts.map +1 -1
  119. package/dist/ui/table_state.svelte.js +22 -7
  120. package/dist/ui/ui_format.d.ts.map +1 -1
  121. package/dist/ui/ui_format.js +4 -0
  122. package/package.json +1 -1
@@ -35,7 +35,7 @@ export declare const clear_session_cookie: <T>(c: Context, options: SessionOptio
35
35
  *
36
36
  * @param keyring - key ring for cookie verification
37
37
  * @param options - session configuration
38
- * @mutates Hono context - sets `options.context_key` and may refresh or clear the session cookie
38
+ * @mutates Hono context - sets `options.context_key` and may clear the session cookie
39
39
  */
40
40
  export declare const create_session_middleware: <TIdentity>(keyring: Keyring, options: SessionOptions<TIdentity>) => MiddlewareHandler;
41
41
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACN,KAAK,cAAc,EAKnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,MAAM,GAAG,SAEX,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,OAAO,MAAM,EACb,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,IAOF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,cAAc,CAAC,CAAC,CAAC,KAAG,IAMhF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,EAClD,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,KAChC,iBAgBF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAChD,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,2CAA2C;IAC3C,CAAC,EAAE,OAAO,CAAC;IACX,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,gCAAgC,KACvC,OAAO,CAAC,IAAI,CAad,CAAC"}
1
+ {"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACN,KAAK,cAAc,EAKnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,MAAM,GAAG,SAEX,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,OAAO,MAAM,EACb,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,IAOF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,cAAc,CAAC,CAAC,CAAC,KAAG,IAMhF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,EAClD,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,KAChC,iBAcF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAChD,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,2CAA2C;IAC3C,CAAC,EAAE,OAAO,CAAC;IACX,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,gCAAgC,KACvC,OAAO,CAAC,IAAI,CAad,CAAC"}
@@ -49,7 +49,7 @@ export const clear_session_cookie = (c, options) => {
49
49
  *
50
50
  * @param keyring - key ring for cookie verification
51
51
  * @param options - session configuration
52
- * @mutates Hono context - sets `options.context_key` and may refresh or clear the session cookie
52
+ * @mutates Hono context - sets `options.context_key` and may clear the session cookie
53
53
  */
54
54
  export const create_session_middleware = (keyring, options) => {
55
55
  return async (c, next) => {
@@ -60,9 +60,6 @@ export const create_session_middleware = (keyring, options) => {
60
60
  if (result.action === 'clear') {
61
61
  clear_session_cookie(c, options);
62
62
  }
63
- else if (result.action === 'refresh' && result.new_signed_value) {
64
- set_session_cookie(c, result.new_signed_value, options);
65
- }
66
63
  await next();
67
64
  };
68
65
  };
@@ -6,13 +6,17 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import type { Logger } from '@fuzdev/fuz_util/log.ts';
10
9
  import type { QueryDeps } from '../db/query_deps.js';
11
10
  import type { AuthSession, SessionId } from './account_schema.js';
12
- /** Session lifetime in milliseconds (30 days). */
11
+ /**
12
+ * Session lifetime in milliseconds (30 days).
13
+ *
14
+ * An **absolute** cap: `expires_at` is set once at mint and never extended —
15
+ * there is deliberately no touch/renewal query on either spine (a sliding
16
+ * window renews a leaked cookie forever; see `docs/security.md` §Session
17
+ * Security). The cookie's `SESSION_AGE_MAX` mirrors this value.
18
+ */
13
19
  export declare const AUTH_SESSION_LIFETIME_MS: number;
14
- /** Extend session when it has less than this remaining (1 day in ms). */
15
- export declare const AUTH_SESSION_EXTEND_THRESHOLD_MS: number;
16
20
  /**
17
21
  * Hash a session token to its storage key using blake3.
18
22
  *
@@ -46,16 +50,6 @@ export declare const query_create_session: (deps: QueryDeps, token_hash: string,
46
50
  * @param token_hash - blake3 hash of the session token
47
51
  */
48
52
  export declare const query_session_get_valid: (deps: QueryDeps, token_hash: string) => Promise<AuthSession | undefined>;
49
- /**
50
- * Update `last_seen_at` and optionally extend expiry for a session.
51
- *
52
- * Extends if less than `AUTH_SESSION_EXTEND_THRESHOLD_MS` remaining.
53
- *
54
- * @param deps - query dependencies
55
- * @param token_hash - blake3 hash of the session token
56
- * @mutates `auth_session` row - updates `last_seen_at` and conditionally `expires_at`
57
- */
58
- export declare const query_session_touch: (deps: QueryDeps, token_hash: string) => Promise<void>;
59
53
  /**
60
54
  * Revoke (delete) a session by its token hash, with no account scoping.
61
55
  *
@@ -137,9 +131,14 @@ export declare const query_session_enforce_limit: (deps: QueryDeps, account_id:
137
131
  /**
138
132
  * List all active sessions across all accounts with usernames.
139
133
  *
134
+ * Ordered by `created_at DESC` (newest session first), matching the
135
+ * per-account listing. `last_seen_at` is still selected for the wire shape
136
+ * but is decorative — nothing updates it post-mint (it always equals
137
+ * `created_at`); it is slated for removal on its own twin migration.
138
+ *
140
139
  * @param deps - query dependencies
141
140
  * @param limit - maximum entries to return
142
- * @returns active sessions joined with account usernames, newest activity first
141
+ * @returns active sessions joined with account usernames, newest first
143
142
  */
144
143
  export declare const query_session_list_all_active: (deps: QueryDeps, limit?: number) => Promise<Array<AuthSession & {
145
144
  username: string;
@@ -151,19 +150,4 @@ export declare const query_session_list_all_active: (deps: QueryDeps, limit?: nu
151
150
  * @mutates `auth_session` table - deletes every row past `expires_at`
152
151
  */
153
152
  export declare const query_session_cleanup_expired: (deps: QueryDeps) => Promise<number>;
154
- /**
155
- * Touch a session without blocking the caller.
156
- *
157
- * Errors are logged to console — session touching never breaks request flows.
158
- * Pass `pending_effects` (from `c.var.pending_effects`) to register
159
- * the promise for test flushing.
160
- *
161
- * @param deps - query dependencies
162
- * @param token_hash - blake3 hash of the session token
163
- * @param pending_effects - optional array to register the effect for later awaiting
164
- * @param log - the logger instance
165
- * @returns the settled promise (callers may ignore it — fire-and-forget semantics preserved)
166
- * @mutates `pending_effects` - pushes the in-flight settled promise when provided
167
- */
168
- export declare const session_touch_fire_and_forget: (deps: QueryDeps, token_hash: string, pending_effects: Array<Promise<void>> | undefined, log: Logger) => Promise<void>;
169
153
  //# sourceMappingURL=session_queries.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE,kDAAkD;AAClD,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE,yEAAyE;AACzE,eAAO,MAAM,gCAAgC,QAAsB,CAAC;AAEpE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAU,MAAM,SAAS,EAAE,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAY3F,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,iBAAiB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,EACjD,KAAK,MAAM,KACT,OAAO,CAAC,IAAI,CAMd,CAAC"}
1
+ {"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC"}
@@ -8,10 +8,15 @@
8
8
  */
9
9
  import { hash_blake3 } from '@fuzdev/fuz_util/hash_blake3.ts';
10
10
  import { generate_random_base64url } from '../crypto.js';
11
- /** Session lifetime in milliseconds (30 days). */
11
+ /**
12
+ * Session lifetime in milliseconds (30 days).
13
+ *
14
+ * An **absolute** cap: `expires_at` is set once at mint and never extended —
15
+ * there is deliberately no touch/renewal query on either spine (a sliding
16
+ * window renews a leaked cookie forever; see `docs/security.md` §Session
17
+ * Security). The cookie's `SESSION_AGE_MAX` mirrors this value.
18
+ */
12
19
  export const AUTH_SESSION_LIFETIME_MS = 30 * 24 * 60 * 60 * 1000;
13
- /** Extend session when it has less than this remaining (1 day in ms). */
14
- export const AUTH_SESSION_EXTEND_THRESHOLD_MS = 24 * 60 * 60 * 1000;
15
20
  /**
16
21
  * Hash a session token to its storage key using blake3.
17
22
  *
@@ -57,25 +62,6 @@ export const query_create_session = async (deps, token_hash, account_id, expires
57
62
  export const query_session_get_valid = async (deps, token_hash) => {
58
63
  return deps.db.query_one(`SELECT * FROM auth_session WHERE id = $1 AND expires_at > NOW()`, [token_hash]);
59
64
  };
60
- /**
61
- * Update `last_seen_at` and optionally extend expiry for a session.
62
- *
63
- * Extends if less than `AUTH_SESSION_EXTEND_THRESHOLD_MS` remaining.
64
- *
65
- * @param deps - query dependencies
66
- * @param token_hash - blake3 hash of the session token
67
- * @mutates `auth_session` row - updates `last_seen_at` and conditionally `expires_at`
68
- */
69
- export const query_session_touch = async (deps, token_hash) => {
70
- const new_expires = new Date(Date.now() + AUTH_SESSION_LIFETIME_MS);
71
- await deps.db.query(`UPDATE auth_session
72
- SET last_seen_at = NOW(),
73
- expires_at = CASE
74
- WHEN expires_at - NOW() < INTERVAL '1 day' THEN $2::timestamptz
75
- ELSE expires_at
76
- END
77
- WHERE id = $1`, [token_hash, new_expires.toISOString()]);
78
- };
79
65
  /**
80
66
  * Revoke (delete) a session by its token hash, with no account scoping.
81
67
  *
@@ -176,16 +162,21 @@ export const query_session_enforce_limit = async (deps, account_id, max_sessions
176
162
  /**
177
163
  * List all active sessions across all accounts with usernames.
178
164
  *
165
+ * Ordered by `created_at DESC` (newest session first), matching the
166
+ * per-account listing. `last_seen_at` is still selected for the wire shape
167
+ * but is decorative — nothing updates it post-mint (it always equals
168
+ * `created_at`); it is slated for removal on its own twin migration.
169
+ *
179
170
  * @param deps - query dependencies
180
171
  * @param limit - maximum entries to return
181
- * @returns active sessions joined with account usernames, newest activity first
172
+ * @returns active sessions joined with account usernames, newest first
182
173
  */
183
174
  export const query_session_list_all_active = async (deps, limit = 200) => {
184
175
  return deps.db.query(`SELECT s.id, s.account_id, s.created_at, s.expires_at, s.last_seen_at, a.username
185
176
  FROM auth_session s
186
177
  JOIN account a ON a.id = s.account_id
187
178
  WHERE s.expires_at > NOW()
188
- ORDER BY s.last_seen_at DESC LIMIT $1`, [limit]);
179
+ ORDER BY s.created_at DESC, s.id DESC LIMIT $1`, [limit]);
189
180
  };
190
181
  /**
191
182
  * Delete expired sessions.
@@ -197,24 +188,3 @@ export const query_session_cleanup_expired = async (deps) => {
197
188
  const rows = await deps.db.query(`DELETE FROM auth_session WHERE expires_at <= NOW() RETURNING id`);
198
189
  return rows.length;
199
190
  };
200
- /**
201
- * Touch a session without blocking the caller.
202
- *
203
- * Errors are logged to console — session touching never breaks request flows.
204
- * Pass `pending_effects` (from `c.var.pending_effects`) to register
205
- * the promise for test flushing.
206
- *
207
- * @param deps - query dependencies
208
- * @param token_hash - blake3 hash of the session token
209
- * @param pending_effects - optional array to register the effect for later awaiting
210
- * @param log - the logger instance
211
- * @returns the settled promise (callers may ignore it — fire-and-forget semantics preserved)
212
- * @mutates `pending_effects` - pushes the in-flight settled promise when provided
213
- */
214
- export const session_touch_fire_and_forget = (deps, token_hash, pending_effects, log) => {
215
- const p = query_session_touch(deps, token_hash).catch((err) => {
216
- log.error('Session touch failed:', err);
217
- });
218
- pending_effects?.push(p);
219
- return p;
220
- };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * API token lifetime — the temporal axis of a minted credential.
3
+ *
4
+ * `lifetime` is **required** on `account_token_create`, mirroring `scope`:
5
+ * there is deliberately no default. A caller who wants a never-expiring token
6
+ * spells `{kind: 'eternal'}`, and the request records that they did — an
7
+ * omitted lifetime is a validation error upstream, not an eternal token.
8
+ * `expires_at IS NULL` therefore means "deliberately eternal", never "minted
9
+ * before the policy existed".
10
+ *
11
+ * Lifetime is deliberately **not** part of the scope document — `TokenScope`
12
+ * is strict on both spines and sits on the hot resolve path. The expiry lives
13
+ * in the indexed `api_token.expires_at` column where the SQL validation gate
14
+ * already enforces it (`expires_at IS NULL OR expires_at > NOW()`).
15
+ *
16
+ * A framework-level ceiling (`max_token_ttl_days`) is deferred until `fuzf`
17
+ * has an expiry story (an expiry-aware read + a 401 hint); see
18
+ * `docs/security.md` §API tokens.
19
+ *
20
+ * @module
21
+ */
22
+ import { z } from 'zod';
23
+ /**
24
+ * Upper bound on `ttl.days` — a sanity cap (100 years), not a policy ceiling.
25
+ * Shared with the Rust twin so out-of-range requests 400 identically on both
26
+ * spines.
27
+ */
28
+ export declare const TOKEN_TTL_DAYS_MAX = 36500;
29
+ /**
30
+ * Caller-provided lifetime for `account_token_create`.
31
+ *
32
+ * `eternal` mints a never-expiring token (`expires_at` NULL); `ttl` bounds it
33
+ * to `days` from mint time. `eternal` is deliberately the first variant so
34
+ * schema-driven test generators produce it by default.
35
+ */
36
+ export declare const TokenLifetimeInput: z.ZodDiscriminatedUnion<[z.ZodObject<{
37
+ kind: z.ZodLiteral<"eternal">;
38
+ }, z.core.$strict>, z.ZodObject<{
39
+ kind: z.ZodLiteral<"ttl">;
40
+ days: z.ZodNumber;
41
+ }, z.core.$strict>], "kind">;
42
+ export type TokenLifetimeInput = z.infer<typeof TokenLifetimeInput>;
43
+ /**
44
+ * Resolve a lifetime input to the `expires_at` value the mint query stores.
45
+ *
46
+ * @param lifetime - the validated lifetime input
47
+ * @param now - mint time (injectable for tests; defaults to the current time)
48
+ * @returns the expiry `Date`, or `null` for an eternal token
49
+ */
50
+ export declare const token_lifetime_to_expires_at: (lifetime: TokenLifetimeInput, now?: Date) => Date | null;
51
+ //# sourceMappingURL=token_lifetime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token_lifetime.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/token_lifetime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;;4BAM7B,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAU,kBAAkB,EAC5B,MAAK,IAAiB,KACpB,IAAI,GAAG,IAGT,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * API token lifetime — the temporal axis of a minted credential.
3
+ *
4
+ * `lifetime` is **required** on `account_token_create`, mirroring `scope`:
5
+ * there is deliberately no default. A caller who wants a never-expiring token
6
+ * spells `{kind: 'eternal'}`, and the request records that they did — an
7
+ * omitted lifetime is a validation error upstream, not an eternal token.
8
+ * `expires_at IS NULL` therefore means "deliberately eternal", never "minted
9
+ * before the policy existed".
10
+ *
11
+ * Lifetime is deliberately **not** part of the scope document — `TokenScope`
12
+ * is strict on both spines and sits on the hot resolve path. The expiry lives
13
+ * in the indexed `api_token.expires_at` column where the SQL validation gate
14
+ * already enforces it (`expires_at IS NULL OR expires_at > NOW()`).
15
+ *
16
+ * A framework-level ceiling (`max_token_ttl_days`) is deferred until `fuzf`
17
+ * has an expiry story (an expiry-aware read + a 401 hint); see
18
+ * `docs/security.md` §API tokens.
19
+ *
20
+ * @module
21
+ */
22
+ import { z } from 'zod';
23
+ /**
24
+ * Upper bound on `ttl.days` — a sanity cap (100 years), not a policy ceiling.
25
+ * Shared with the Rust twin so out-of-range requests 400 identically on both
26
+ * spines.
27
+ */
28
+ export const TOKEN_TTL_DAYS_MAX = 36_500;
29
+ /**
30
+ * Caller-provided lifetime for `account_token_create`.
31
+ *
32
+ * `eternal` mints a never-expiring token (`expires_at` NULL); `ttl` bounds it
33
+ * to `days` from mint time. `eternal` is deliberately the first variant so
34
+ * schema-driven test generators produce it by default.
35
+ */
36
+ export const TokenLifetimeInput = z.discriminatedUnion('kind', [
37
+ z.strictObject({ kind: z.literal('eternal') }),
38
+ z.strictObject({
39
+ kind: z.literal('ttl'),
40
+ days: z.number().int().min(1).max(TOKEN_TTL_DAYS_MAX)
41
+ })
42
+ ]);
43
+ /**
44
+ * Resolve a lifetime input to the `expires_at` value the mint query stores.
45
+ *
46
+ * @param lifetime - the validated lifetime input
47
+ * @param now - mint time (injectable for tests; defaults to the current time)
48
+ * @returns the expiry `Date`, or `null` for an eternal token
49
+ */
50
+ export const token_lifetime_to_expires_at = (lifetime, now = new Date()) => {
51
+ if (lifetime.kind === 'eternal')
52
+ return null;
53
+ return new Date(now.getTime() + lifetime.days * 86_400_000);
54
+ };
package/dist/db/CLAUDE.md CHANGED
@@ -25,7 +25,7 @@ the code.
25
25
  `baseline`), `Migration` / `MigrationNamespace` / `MigrationError`.
26
26
  - `query_deps.ts` — `QueryDeps = {db}`, the first param to every `query_*`.
27
27
  - `assert_row.ts` — `assert_row(row, context)` for INSERT … RETURNING.
28
- - `pg_error.ts` — `is_pg_unique_violation` (Postgres `23505`).
28
+ - `pg_error.ts` — `pg_error_code` (SQLSTATE extractor for `pg`/PGlite errors), `is_pg_unique_violation` (Postgres `23505`).
29
29
  - `sql_identifier.ts` — `assert_valid_sql_identifier`.
30
30
  - `status.ts` — CLI DB status utility.
31
31
  - `schema_ready.ts` — `/ready` deploy-gate core: `query_public_columns`
@@ -95,7 +95,8 @@ child_id`, fractional-index keyed): `query_cell_item_insert` (throws
95
95
  - **`cell_audit_queries.ts`** — `query_audit_log_list_by_cell`: matches
96
96
  `audit_log` rows whose `metadata` names the cell on any of six cell-domain
97
97
  keys (`cell_id` / `source_id` / `new_id` / `parent_id` / `child_id` /
98
- `target_id`), bitmap-OR over the metadata GIN.
98
+ `target_id`), bitmap-OR over `idx_audit_log_metadata` (the `jsonb_path_ops`
99
+ GIN the `audit_log_metadata_gin_index` auth migration ships).
99
100
 
100
101
  ## Fact layer
101
102
 
@@ -155,6 +156,9 @@ auth_migration_ns (built-in, fuz_auth)
155
156
 
156
157
  ### Pre-stable migration policy
157
158
 
158
- Pre-stable: when an introducing migration needs to change shape, **rewrite
159
- the original in place** rather than appending a follow-up — consumers
160
- re-bootstrap dev DBs on upgrade. See ../../../docs/migrations.md.
159
+ Scoped to the namespaces on this page (`fuz_cell` / `fuz_cell_history` /
160
+ `fuz_facts`): each is a single `full_*_schema` entry, so when it needs to
161
+ change shape, **rewrite the original in place** rather than appending a
162
+ follow-up — consumers re-bootstrap dev DBs on upgrade. The `fuz_auth`
163
+ namespace is the opposite — frozen and append-only, since consumers hold
164
+ deployed databases on it. See ../../../docs/migrations.md §Schema stability.
@@ -5,8 +5,10 @@
5
5
  * `new_id` (clone), `source_id` / `target_id` (cell_field events),
6
6
  * `parent_id` / `child_id` (cell_item events).
7
7
  *
8
- * Each `metadata @> '{...}'::jsonb` clause hits the existing GIN on
9
- * `audit_log.metadata`; Postgres bitmap-ORs the index scans together.
8
+ * Each `metadata @> '{...}'::jsonb` clause hits `idx_audit_log_metadata`
9
+ * (the `jsonb_path_ops` GIN on `audit_log.metadata`, shipped by the
10
+ * `audit_log_metadata_gin_index` migration); Postgres bitmap-ORs the index
11
+ * scans together.
10
12
  *
11
13
  * @module
12
14
  */
@@ -1 +1 @@
1
- {"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAoBnD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,SAAS,EACf,SAAS,IAAI,EACb,SAAS,oBAAoB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwB9B,CAAC"}
1
+ {"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAoBnD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,SAAS,EACf,SAAS,IAAI,EACb,SAAS,oBAAoB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwB9B,CAAC"}
@@ -5,8 +5,10 @@
5
5
  * `new_id` (clone), `source_id` / `target_id` (cell_field events),
6
6
  * `parent_id` / `child_id` (cell_item events).
7
7
  *
8
- * Each `metadata @> '{...}'::jsonb` clause hits the existing GIN on
9
- * `audit_log.metadata`; Postgres bitmap-ORs the index scans together.
8
+ * Each `metadata @> '{...}'::jsonb` clause hits `idx_audit_log_metadata`
9
+ * (the `jsonb_path_ops` GIN on `audit_log.metadata`, shipped by the
10
+ * `audit_log_metadata_gin_index` migration); Postgres bitmap-ORs the index
11
+ * scans together.
10
12
  *
11
13
  * @module
12
14
  */
@@ -6,6 +6,18 @@
6
6
  *
7
7
  * @module
8
8
  */
9
+ /**
10
+ * Extract the error-code string from a caught error, for SQLSTATE checks.
11
+ *
12
+ * Other error families also carry string `code`s (Node FS errors:
13
+ * `ENOENT`, `ENOSPC`, …) — compare the result against specific SQLSTATE
14
+ * values, never against truthiness.
15
+ *
16
+ * @param error - the caught error
17
+ * @returns the `code` string, or `null` when the value isn't an `Error`
18
+ * or carries no string `code`
19
+ */
20
+ export declare const pg_error_code: (error: unknown) => string | null;
9
21
  /**
10
22
  * Check if an error is a PostgreSQL unique constraint violation (error code 23505).
11
23
  *
@@ -1 +1 @@
1
- {"version":3,"file":"pg_error.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/pg_error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OACmC,CAAC"}
1
+ {"version":3,"file":"pg_error.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/pg_error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAGhD,CAAC;AAET;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OAA2C,CAAC"}
@@ -6,10 +6,24 @@
6
6
  *
7
7
  * @module
8
8
  */
9
+ /**
10
+ * Extract the error-code string from a caught error, for SQLSTATE checks.
11
+ *
12
+ * Other error families also carry string `code`s (Node FS errors:
13
+ * `ENOENT`, `ENOSPC`, …) — compare the result against specific SQLSTATE
14
+ * values, never against truthiness.
15
+ *
16
+ * @param error - the caught error
17
+ * @returns the `code` string, or `null` when the value isn't an `Error`
18
+ * or carries no string `code`
19
+ */
20
+ export const pg_error_code = (error) => error instanceof Error && 'code' in error && typeof error.code === 'string'
21
+ ? error.code
22
+ : null;
9
23
  /**
10
24
  * Check if an error is a PostgreSQL unique constraint violation (error code 23505).
11
25
  *
12
26
  * @param error - the caught error
13
27
  * @returns `true` if the error is a unique constraint violation
14
28
  */
15
- export const is_pg_unique_violation = (error) => error instanceof Error && 'code' in error && error.code === '23505';
29
+ export const is_pg_unique_violation = (error) => pg_error_code(error) === '23505';
@@ -57,16 +57,12 @@ export interface SetupEnvOptions {
57
57
  * Replaces `^KEY=$` (empty value) patterns in the env file.
58
58
  */
59
59
  replacements?: Record<string, () => Promise<string>>;
60
- /** Optional callback to set file permissions (e.g. `Deno.chmod`). */
61
- set_permissions?: (path: string, mode: number) => Promise<void>;
62
60
  log?: SetupLogger;
63
61
  }
64
62
  /** Options for `setup_bootstrap_token`. */
65
63
  export interface SetupBootstrapTokenOptions {
66
64
  /** State directory override. Defaults to `~/.{app_name}`. */
67
65
  state_dir?: string;
68
- /** Optional callback to set file/directory permissions. */
69
- set_permissions?: (path: string, mode: number) => Promise<void>;
70
66
  log?: SetupLogger;
71
67
  }
72
68
  /** Options for `create_database`. */
@@ -119,9 +115,9 @@ export declare const read_env_var: (deps: Pick<FsReadDeps, "read_text_file">, en
119
115
  * @param deps - file read, write, and command capabilities
120
116
  * @param env_path - path for the env file (e.g. `.env.development`)
121
117
  * @param example_path - path to the example template
122
- * @param options - extra replacements, permissions, logger
118
+ * @param options - extra replacements, logger
123
119
  * @returns result indicating whether the file was created or updated
124
- * @mutates filesystem - writes `env_path` (creating from `example_path` if missing) and optionally chmods to `0o600`
120
+ * @mutates filesystem - writes `env_path` atomically at mode `0o600` (creating from `example_path` if missing)
125
121
  */
126
122
  export declare const setup_env_file: (deps: FsReadDeps & FsWriteDeps & CommandDeps, env_path: string, example_path: string, options?: SetupEnvOptions) => Promise<SetupEnvResult>;
127
123
  /**
@@ -132,9 +128,9 @@ export declare const setup_env_file: (deps: FsReadDeps & FsWriteDeps & CommandDe
132
128
  *
133
129
  * @param deps - file, command, and env capabilities
134
130
  * @param app_name - application name (used for default state directory)
135
- * @param options - state_dir override, permissions, logger
131
+ * @param options - state_dir override, logger
136
132
  * @returns result indicating whether a token was created
137
- * @mutates filesystem - creates state directory and writes the token file (optionally chmods to `0o700` / `0o600`)
133
+ * @mutates filesystem - creates the state directory (`0o700`) and writes the token file atomically at `0o600`
138
134
  */
139
135
  export declare const setup_bootstrap_token: (deps: FsReadDeps & FsWriteDeps & CommandDeps & EnvDeps, app_name: string, options?: SetupBootstrapTokenOptions) => Promise<SetupTokenResult>;
140
136
  /**
@@ -142,7 +138,7 @@ export declare const setup_bootstrap_token: (deps: FsReadDeps & FsWriteDeps & Co
142
138
  *
143
139
  * @param deps - file, command, env, and remove capabilities
144
140
  * @param app_name - application name
145
- * @param options - state_dir override, permissions, logger
141
+ * @param options - state_dir override, logger
146
142
  * @returns result from creating the new token
147
143
  * @mutates filesystem - removes the existing token file (if any) then writes a fresh one
148
144
  */
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/dev/setup.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/dev/setup.ts"],"names":[],"mappings":"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"}
package/dist/dev/setup.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * @module
10
10
  */
11
11
  import { load_env_file } from '../env/dotenv.js';
12
+ import { write_file_atomic } from '../runtime/fs.js';
12
13
  import { query_account_by_username, query_actors_by_account, query_create_account_with_actor } from '../auth/account_queries.js';
13
14
  import { query_create_role_grant } from '../auth/role_grant_queries.js';
14
15
  /** Default logger using bracket format. */
@@ -77,13 +78,12 @@ export const read_env_var = async (deps, env_path, name) => {
77
78
  * @param deps - file read, write, and command capabilities
78
79
  * @param env_path - path for the env file (e.g. `.env.development`)
79
80
  * @param example_path - path to the example template
80
- * @param options - extra replacements, permissions, logger
81
+ * @param options - extra replacements, logger
81
82
  * @returns result indicating whether the file was created or updated
82
- * @mutates filesystem - writes `env_path` (creating from `example_path` if missing) and optionally chmods to `0o600`
83
+ * @mutates filesystem - writes `env_path` atomically at mode `0o600` (creating from `example_path` if missing)
83
84
  */
84
85
  export const setup_env_file = async (deps, env_path, example_path, options) => {
85
86
  const log = options?.log ?? default_setup_logger;
86
- const set_permissions = options?.set_permissions;
87
87
  // build the full replacement map (SECRET_FUZ_COOKIE_KEYS + extras)
88
88
  const replacements = {
89
89
  SECRET_FUZ_COOKIE_KEYS: () => generate_random_key(deps),
@@ -104,9 +104,10 @@ export const setup_env_file = async (deps, env_path, example_path, options) => {
104
104
  }
105
105
  }
106
106
  if (changed) {
107
- await deps.write_text_file(env_path, content);
108
- if (set_permissions)
109
- await set_permissions(env_path, 0o600);
107
+ // Atomic replace at 0600 — the backfill just wrote secrets into the
108
+ // file, so publishing a fresh owner-only inode also repairs a
109
+ // hand-made permissive .env in the same step the old chmod covered.
110
+ await write_file_atomic(deps, env_path, content, { mode: 0o600 });
110
111
  }
111
112
  else {
112
113
  log.skip(`${env_path} already configured`);
@@ -122,9 +123,7 @@ export const setup_env_file = async (deps, env_path, example_path, options) => {
122
123
  content = content.replace(pattern, `${key}=${value}`);
123
124
  }
124
125
  }
125
- await deps.write_text_file(env_path, content);
126
- if (set_permissions)
127
- await set_permissions(env_path, 0o600);
126
+ await write_file_atomic(deps, env_path, content, { mode: 0o600 });
128
127
  log.ok(`Created ${env_path} with generated secrets`);
129
128
  return { created: true, updated: true, path: env_path };
130
129
  };
@@ -136,13 +135,12 @@ export const setup_env_file = async (deps, env_path, example_path, options) => {
136
135
  *
137
136
  * @param deps - file, command, and env capabilities
138
137
  * @param app_name - application name (used for default state directory)
139
- * @param options - state_dir override, permissions, logger
138
+ * @param options - state_dir override, logger
140
139
  * @returns result indicating whether a token was created
141
- * @mutates filesystem - creates state directory and writes the token file (optionally chmods to `0o700` / `0o600`)
140
+ * @mutates filesystem - creates the state directory (`0o700`) and writes the token file atomically at `0o600`
142
141
  */
143
142
  export const setup_bootstrap_token = async (deps, app_name, options) => {
144
143
  const log = options?.log ?? default_setup_logger;
145
- const set_permissions = options?.set_permissions;
146
144
  const home = deps.env_get('HOME');
147
145
  if (!home) {
148
146
  log.skip('$HOME not set, skipping bootstrap token');
@@ -155,13 +153,13 @@ export const setup_bootstrap_token = async (deps, app_name, options) => {
155
153
  log.skip(`~/.${app_name}/secret_bootstrap_token already exists`);
156
154
  return { created: false, token_path };
157
155
  }
158
- await deps.mkdir(state_dir, { recursive: true });
159
- if (set_permissions)
160
- await set_permissions(state_dir, 0o700);
156
+ // Modes apply at creation — an already-existing state dir keeps its mode.
157
+ // The token itself is always freshly created here (the stat above skipped
158
+ // existing files), so its 0600 holds unconditionally; the server-side
159
+ // `read_secure_file` refuses anything laxer at read time.
160
+ await deps.mkdir(state_dir, { recursive: true, mode: 0o700 });
161
161
  const key = await generate_random_key(deps);
162
- await deps.write_text_file(token_path, key + '\n');
163
- if (set_permissions)
164
- await set_permissions(token_path, 0o600);
162
+ await write_file_atomic(deps, token_path, key + '\n', { mode: 0o600 });
165
163
  log.ok(`Created ~/.${app_name}/secret_bootstrap_token (one-shot, deleted after first use)`);
166
164
  return { created: true, token_path };
167
165
  };
@@ -170,13 +168,12 @@ export const setup_bootstrap_token = async (deps, app_name, options) => {
170
168
  *
171
169
  * @param deps - file, command, env, and remove capabilities
172
170
  * @param app_name - application name
173
- * @param options - state_dir override, permissions, logger
171
+ * @param options - state_dir override, logger
174
172
  * @returns result from creating the new token
175
173
  * @mutates filesystem - removes the existing token file (if any) then writes a fresh one
176
174
  */
177
175
  export const reset_bootstrap_token = async (deps, app_name, options) => {
178
176
  const log = options?.log ?? default_setup_logger;
179
- const set_permissions = options?.set_permissions;
180
177
  const home = deps.env_get('HOME');
181
178
  if (!home) {
182
179
  log.skip('$HOME not set');
@@ -189,7 +186,7 @@ export const reset_bootstrap_token = async (deps, app_name, options) => {
189
186
  await deps.remove(token_path);
190
187
  log.ok('Removed existing bootstrap token');
191
188
  }
192
- return setup_bootstrap_token(deps, app_name, { state_dir, set_permissions, log });
189
+ return setup_bootstrap_token(deps, app_name, { state_dir, log });
193
190
  };
194
191
  // === Database helpers ===
195
192
  /**