@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
@@ -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":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,UAAU,EACV,YAAY,EACZ,WAAW,EACX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AASrD;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB,EAAE,WAIlC,CAAC;AAEF,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC9B,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;IACjB,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,yCAAyC;AACzC,MAAM,WAAW,gBAAgB;IAChC,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,2BAA2B;IAC3B,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,kCAAkC;AAClC,MAAM,WAAW,aAAa;IAC7B,+CAA+C;IAC/C,KAAK,EAAE,OAAO,CAAC;IACf,wEAAwE;IACxE,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,OAAO,EAAE,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;CACxC;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IACrD,qEAAqE;IACrE,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,GAAG,CAAC,EAAE,WAAW,CAAC;CAClB;AAED,2CAA2C;AAC3C,MAAM,WAAW,0BAA0B;IAC1C,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,GAAG,CAAC,EAAE,WAAW,CAAC;CAClB;AAED,qCAAqC;AACrC,MAAM,WAAW,qBAAqB;IACrC,GAAG,CAAC,EAAE,WAAW,CAAC;CAClB;AAED,oCAAoC;AACpC,MAAM,WAAW,oBAAoB;IACpC,iDAAiD;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,WAAW,CAAC;CAClB;AAID;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAQpD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAU,MAAM,WAAW,KAAG,OAAO,CAAC,MAAM,CAI3E,CAAC;AAIF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,YAAY,GACxB,MAAM,IAAI,CAAC,UAAU,EAAE,gBAAgB,CAAC,EACxC,UAAU,MAAM,EAChB,MAAM,MAAM,KACV,OAAO,CAAC,MAAM,GAAG,SAAS,CAG5B,CAAC;AAIF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAC1B,MAAM,UAAU,GAAG,WAAW,GAAG,WAAW,EAC5C,UAAU,MAAM,EAChB,cAAc,MAAM,EACpB,UAAU,eAAe,KACvB,OAAO,CAAC,cAAc,CAiDxB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,OAAO,EACtD,UAAU,MAAM,EAChB,UAAU,0BAA0B,KAClC,OAAO,CAAC,gBAAgB,CA0B1B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GACjC,MAAM,UAAU,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,GAAG,OAAO,EACrE,UAAU,MAAM,EAChB,UAAU,0BAA0B,KAClC,OAAO,CAAC,gBAAgB,CAoB1B,CAAC;AAIF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAC3B,MAAM,WAAW,EACjB,SAAS,MAAM,EACf,UAAU,qBAAqB,KAC7B,OAAO,CAAC,aAAa,CAgBvB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAC1B,MAAM,WAAW,GAAG,UAAU,GAAG,YAAY,EAC7C,cAAc,MAAM,EACpB,UAAU,oBAAoB,KAC5B,OAAO,CAAC,aAAa,CA8CvB,CAAC;AAIF,mCAAmC;AACnC,MAAM,WAAW,mBAAmB;IACnC,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,KAAK,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC9B;AAED,oCAAoC;AACpC,MAAM,WAAW,oBAAoB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,2CAA2C;AAC3C,MAAM,WAAW,kBAAmB,SAAQ,SAAS;IACpD,oEAAoE;IACpE,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACrD;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,GAC5B,MAAM,kBAAkB,EACxB,OAAO,mBAAmB,EAC1B,UAAU;IAAE,GAAG,CAAC,EAAE,WAAW,CAAA;CAAE,KAC7B,OAAO,CAAC,oBAAoB,CAwC9B,CAAC"}
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
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"deno.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deno.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,WAAW,EAA6B,MAAM,WAAW,CAAC;AA6DxE;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,WAgJhE,CAAC"}
1
+ {"version":3,"file":"deno.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deno.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,WAAW,EAA6B,MAAM,WAAW,CAAC;AAoExE;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,WAuKhE,CAAC"}
@@ -8,6 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  import { to_error_message } from '@fuzdev/fuz_util/error.ts';
11
+ import { assert_secure_mode, assert_secure_size, read_secure_bounded } from './secure_file.js';
11
12
  /**
12
13
  * Create a runtime backed by Deno APIs.
13
14
  *
@@ -44,6 +45,28 @@ export const create_deno_runtime = (args) => ({
44
45
  mkdir: (path, options) => Deno.mkdir(path, options),
45
46
  read_text_file: (path) => Deno.readTextFile(path),
46
47
  read_file: (path) => Deno.readFile(path),
48
+ read_secure_file: async (path) => {
49
+ // Deno exposes no O_NOFOLLOW, so the symlink check is a pre-open lstat
50
+ // (a TOCTOU window the Node impl doesn't have — the mode check below
51
+ // still runs on the open descriptor, so a post-open swap can't defeat
52
+ // it). The checks themselves are the shared `runtime/secure_file.ts`
53
+ // helpers, same as `load_secure_file_node`.
54
+ const l = await Deno.lstat(path);
55
+ if (l.isSymlink)
56
+ throw new Error(`secure file is a symlink: ${path}`);
57
+ const file = await Deno.open(path, { read: true });
58
+ try {
59
+ const s = await file.stat();
60
+ // `mode` is null where POSIX modes don't apply (Windows).
61
+ if (s.mode !== null)
62
+ assert_secure_mode(path, s.mode);
63
+ assert_secure_size(path, s.size);
64
+ return await read_secure_bounded(path, (target) => file.read(target));
65
+ }
66
+ finally {
67
+ file.close();
68
+ }
69
+ },
47
70
  read_file_stream: async (path) => (await Deno.open(path, { read: true })).readable,
48
71
  write_file_stream: async (path, data) => {
49
72
  const file = await Deno.open(path, { write: true, create: true, truncate: true });
@@ -78,7 +101,10 @@ export const create_deno_runtime = (args) => ({
78
101
  names.push(entry.name);
79
102
  return names;
80
103
  },
81
- write_text_file: (path, content) => Deno.writeTextFile(path, content),
104
+ write_text_file: (path, content, options) => Deno.writeTextFile(path, content, {
105
+ mode: options?.mode,
106
+ createNew: options?.exclusive
107
+ }),
82
108
  write_file: (path, data) => Deno.writeFile(path, data),
83
109
  rename: (old_path, new_path) => Deno.rename(old_path, new_path),
84
110
  fsync: async (path) => {
@@ -79,6 +79,39 @@ export interface ReadTextFromOffsetResult {
79
79
  /** Total file size at the time of the read (for truncation detection). */
80
80
  file_size: number;
81
81
  }
82
+ /**
83
+ * Options for `FsWriteDeps.write_text_file`.
84
+ */
85
+ export interface WriteFileOptions {
86
+ /**
87
+ * POSIX mode applied at creation (e.g. `0o600`). Creation-time only — an
88
+ * existing file keeps its mode (pair with `exclusive` when the mode must
89
+ * hold). No-op where modes don't apply (mock runtime, Windows).
90
+ */
91
+ mode?: number;
92
+ /**
93
+ * Fail if the path already exists (`O_EXCL`). Load-bearing for secret
94
+ * files: without it a crash-leftover temp file is reopened `O_TRUNC` and
95
+ * keeps its old (possibly permissive) mode, since `mode` only applies at
96
+ * creation.
97
+ */
98
+ exclusive?: boolean;
99
+ }
100
+ /**
101
+ * Hardened read for secret files (bootstrap tokens, keys) — the TS twin of
102
+ * the Rust spine's `fuz_sys::secure_file::load_secure_file`.
103
+ */
104
+ export interface FsSecureReadDeps {
105
+ /**
106
+ * Read a small secret file with fail-loud checks: rejects symlinks, any
107
+ * group/other-accessible mode (must be `0600`/`0400`; skipped where POSIX
108
+ * modes don't apply), and files over a 4 KiB cap. Permission checks run on
109
+ * the open descriptor where the platform allows, so the file can't be
110
+ * swapped between check and read. Throws on any refusal — a refused secret
111
+ * must never degrade to an empty read.
112
+ */
113
+ read_secure_file: (path: string) => Promise<Uint8Array>;
114
+ }
82
115
  /**
83
116
  * File system read operations.
84
117
  */
@@ -104,12 +137,13 @@ export interface FsReadDeps {
104
137
  * File system write operations.
105
138
  */
106
139
  export interface FsWriteDeps {
107
- /** Create a directory. */
140
+ /** Create a directory. `mode` applies at creation (no-op where modes don't apply). */
108
141
  mkdir: (path: string, options?: {
109
142
  recursive?: boolean;
143
+ mode?: number;
110
144
  }) => Promise<void>;
111
- /** Write text to a file. */
112
- write_text_file: (path: string, content: string) => Promise<void>;
145
+ /** Write text to a file. See `WriteFileOptions` for `mode` / `exclusive`. */
146
+ write_text_file: (path: string, content: string, options?: WriteFileOptions) => Promise<void>;
113
147
  /** Write bytes to a file. */
114
148
  write_file: (path: string, data: Uint8Array) => Promise<void>;
115
149
  /** Rename (move) a file. */
@@ -213,7 +247,7 @@ export interface ProcessDeps {
213
247
  * Functions should accept narrow `*Deps` interfaces, not this full type —
214
248
  * this type is for the wiring layer that creates and passes the runtime.
215
249
  */
216
- export interface RuntimeDeps extends EnvDeps, FsReadDeps, FsWriteDeps, FsStreamDeps, FsRemoveDeps, CommandDeps, FetchDeps, TerminalDeps, ProcessDeps, LogDeps {
250
+ export interface RuntimeDeps extends EnvDeps, FsReadDeps, FsSecureReadDeps, FsWriteDeps, FsStreamDeps, FsRemoveDeps, CommandDeps, FetchDeps, TerminalDeps, ProcessDeps, LogDeps {
217
251
  /** Get all environment variables. */
218
252
  env_all: () => Record<string, string>;
219
253
  /** CLI arguments passed to the program. */
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deps.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deps.ts"],"names":[],"mappings":"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"}
@@ -3,15 +3,34 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import type { FsWriteDeps } from './deps.js';
6
+ import type { FsRemoveDeps, FsWriteDeps } from './deps.js';
7
+ /** Options for `write_file_atomic`. */
8
+ export interface WriteFileAtomicOptions {
9
+ /**
10
+ * POSIX mode for the written file (e.g. `0o600` for secrets). Applied at
11
+ * temp-file creation, so the published file never exists with a laxer
12
+ * mode. Omitted = the runtime default (umask).
13
+ */
14
+ mode?: number;
15
+ }
7
16
  /**
8
17
  * Write a file atomically via temp file + rename.
9
18
  *
10
- * Writes to `<path>.tmp` then renames over `path` so readers either see the
11
- * old contents or the full new contents — never a partial write.
19
+ * Writes to a unique `.{name}.tmp.{pid}.{counter}` sibling with exclusive
20
+ * create (`O_EXCL`), then renames over `path` so readers either see the old
21
+ * contents or the full new contents — never a partial write. The unique name
22
+ * + exclusive create are load-bearing for secret files: a fixed temp name
23
+ * would let a crash-leftover temp be reopened `O_TRUNC` with its old
24
+ * (possibly permissive) mode — `mode` only applies at creation — and the
25
+ * rename would publish that stale inode. Twin of the Rust spine's
26
+ * `fuz_sys::secure_file::write_secure_file` naming scheme.
12
27
  *
13
- * @mutates filesystem - creates `<path>.tmp` then renames it to `path`
28
+ * @param deps - write capabilities; pass `remove` too for best-effort temp cleanup on failure
29
+ * @param path - destination path
30
+ * @param content - full file content
31
+ * @param options - optional `mode` applied at creation
32
+ * @mutates filesystem - creates the unique temp file then renames it to `path`
14
33
  * @throws Error if `write_text_file` or `rename` rejects (permissions, disk full, cross-device rename, etc.)
15
34
  */
16
- export declare const write_file_atomic: (deps: Pick<FsWriteDeps, "write_text_file" | "rename">, path: string, content: string) => Promise<void>;
35
+ export declare const write_file_atomic: (deps: Pick<FsWriteDeps, "write_text_file" | "rename"> & Partial<FsRemoveDeps>, path: string, content: string, options?: WriteFileAtomicOptions) => Promise<void>;
17
36
  //# sourceMappingURL=fs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/fs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,IAAI,CAAC,WAAW,EAAE,iBAAiB,GAAG,QAAQ,CAAC,EACrD,MAAM,MAAM,EACZ,SAAS,MAAM,KACb,OAAO,CAAC,IAAI,CAId,CAAC"}
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/fs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE3D,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,IAAI,CAAC,WAAW,EAAE,iBAAiB,GAAG,QAAQ,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,EAC7E,MAAM,MAAM,EACZ,SAAS,MAAM,EACf,UAAU,sBAAsB,KAC9B,OAAO,CAAC,IAAI,CAoBd,CAAC"}
@@ -3,17 +3,48 @@
3
3
  *
4
4
  * @module
5
5
  */
6
+ import process from 'node:process';
7
+ /** Per-process counter for unique temp names (concurrent same-path writes). */
8
+ let temp_file_counter = 0;
6
9
  /**
7
10
  * Write a file atomically via temp file + rename.
8
11
  *
9
- * Writes to `<path>.tmp` then renames over `path` so readers either see the
10
- * old contents or the full new contents — never a partial write.
12
+ * Writes to a unique `.{name}.tmp.{pid}.{counter}` sibling with exclusive
13
+ * create (`O_EXCL`), then renames over `path` so readers either see the old
14
+ * contents or the full new contents — never a partial write. The unique name
15
+ * + exclusive create are load-bearing for secret files: a fixed temp name
16
+ * would let a crash-leftover temp be reopened `O_TRUNC` with its old
17
+ * (possibly permissive) mode — `mode` only applies at creation — and the
18
+ * rename would publish that stale inode. Twin of the Rust spine's
19
+ * `fuz_sys::secure_file::write_secure_file` naming scheme.
11
20
  *
12
- * @mutates filesystem - creates `<path>.tmp` then renames it to `path`
21
+ * @param deps - write capabilities; pass `remove` too for best-effort temp cleanup on failure
22
+ * @param path - destination path
23
+ * @param content - full file content
24
+ * @param options - optional `mode` applied at creation
25
+ * @mutates filesystem - creates the unique temp file then renames it to `path`
13
26
  * @throws Error if `write_text_file` or `rename` rejects (permissions, disk full, cross-device rename, etc.)
14
27
  */
15
- export const write_file_atomic = async (deps, path, content) => {
16
- const temp_path = path + '.tmp';
17
- await deps.write_text_file(temp_path, content);
18
- await deps.rename(temp_path, path);
28
+ export const write_file_atomic = async (deps, path, content, options) => {
29
+ const slash = path.lastIndexOf('/');
30
+ const dir = slash === -1 ? '' : path.slice(0, slash + 1);
31
+ const name = slash === -1 ? path : path.slice(slash + 1);
32
+ const temp_path = `${dir}.${name}.tmp.${process.pid}.${temp_file_counter++}`;
33
+ try {
34
+ await deps.write_text_file(temp_path, content, {
35
+ mode: options?.mode,
36
+ exclusive: true
37
+ });
38
+ await deps.rename(temp_path, path);
39
+ }
40
+ catch (err) {
41
+ // best-effort cleanup so a failed write doesn't strand the temp
42
+ try {
43
+ await deps.remove?.(temp_path);
44
+ }
45
+ catch {
46
+ // already gone or not removable — the original error wins
47
+ }
48
+ throw err;
49
+ }
19
50
  };
@@ -18,6 +18,13 @@ export interface MockRuntime extends RuntimeDeps {
18
18
  mock_fs: Map<string, string>;
19
19
  /** Mock binary file system (path -> bytes). */
20
20
  mock_fs_bytes: Map<string, Uint8Array>;
21
+ /**
22
+ * Recorded POSIX modes (path -> mode), written by `write_text_file`'s
23
+ * `mode` option. A file with no recorded mode reads as `0o644` (the
24
+ * default-umask hazard) — so `read_secure_file` refuses it unless the
25
+ * writer asked for `0o600` explicitly, mirroring the real runtimes.
26
+ */
27
+ mock_fs_modes: Map<string, number>;
21
28
  /** Mock directories that exist. */
22
29
  mock_dirs: Set<string>;
23
30
  /** Exit calls recorded (exit codes). */
@@ -1 +1 @@
1
- {"version":3,"file":"mock.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/mock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAc,aAAa,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAI3F;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,kCAAkC;IAClC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,0CAA0C;IAC1C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,+CAA+C;IAC/C,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,mCAAmC;IACnC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,wCAAwC;IACxC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,gGAAgG;IAChG,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC,CAAC;IACxF,sCAAsC;IACtC,qBAAqB,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;IACnE,8BAA8B;IAC9B,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,4CAA4C;IAC5C,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjD,yCAAyC;IACzC,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,4BAA4B;IAC5B,WAAW,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IAC1E,wDAAwD;IACxD,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAM,KAAK,CAAC,MAAM,CAAM,KAAG,WAwR9D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,KAAG,IAazD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,KAAG,IAEpE,CAAC;AAEF;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM;CAKxB"}
1
+ {"version":3,"file":"mock.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/mock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAc,aAAa,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAK3F;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,kCAAkC;IAClC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,0CAA0C;IAC1C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,+CAA+C;IAC/C,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC;;;;;OAKG;IACH,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,mCAAmC;IACnC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,wCAAwC;IACxC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,gGAAgG;IAChG,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC,CAAC;IACxF,sCAAsC;IACtC,qBAAqB,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;IACnE,8BAA8B;IAC9B,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,4CAA4C;IAC5C,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjD,yCAAyC;IACzC,YAAY,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,4BAA4B;IAC5B,WAAW,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IAC1E,wDAAwD;IACxD,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAM,KAAK,CAAC,MAAM,CAAM,KAAG,WA8T9D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,KAAG,IAazD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,KAAG,IAEpE,CAAC;AAEF;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM;CAKxB"}
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * @module
9
9
  */
10
+ import { assert_secure_mode, assert_secure_size } from './secure_file.js';
10
11
  /**
11
12
  * Create a mock `RuntimeDeps` for testing.
12
13
  *
@@ -32,6 +33,7 @@ export const create_mock_runtime = (args = []) => {
32
33
  const mock_env = new Map();
33
34
  const mock_fs = new Map();
34
35
  const mock_fs_bytes = new Map();
36
+ const mock_fs_modes = new Map();
35
37
  const mock_dirs = new Set();
36
38
  const exit_calls = [];
37
39
  const command_calls = [];
@@ -46,6 +48,7 @@ export const create_mock_runtime = (args = []) => {
46
48
  mock_env,
47
49
  mock_fs,
48
50
  mock_fs_bytes,
51
+ mock_fs_modes,
49
52
  mock_dirs,
50
53
  exit_calls,
51
54
  command_calls,
@@ -122,6 +125,26 @@ export const create_mock_runtime = (args = []) => {
122
125
  error.code = 'ENOENT';
123
126
  throw error;
124
127
  },
128
+ read_secure_file: async (path) => {
129
+ let bytes = mock_fs_bytes.get(path);
130
+ if (bytes === undefined) {
131
+ const content = mock_fs.get(path);
132
+ if (content !== undefined)
133
+ bytes = new TextEncoder().encode(content);
134
+ }
135
+ if (bytes === undefined) {
136
+ const error = new Error(`ENOENT: no such file or directory: ${path}`);
137
+ error.code = 'ENOENT';
138
+ throw error;
139
+ }
140
+ // A file written without an explicit mode reads as 0o644 — the
141
+ // default-umask hazard — so the secure read refuses it unless the
142
+ // writer asked for 0600, mirroring the real runtimes (simulated
143
+ // modes, so no platform gate).
144
+ assert_secure_mode(path, mock_fs_modes.get(path) ?? 0o644);
145
+ assert_secure_size(path, bytes.length);
146
+ return bytes;
147
+ },
125
148
  read_file_stream: async (path) => {
126
149
  let bytes = mock_fs_bytes.get(path);
127
150
  if (bytes === undefined) {
@@ -193,8 +216,15 @@ export const create_mock_runtime = (args = []) => {
193
216
  }
194
217
  return Array.from(seen).sort();
195
218
  },
196
- write_text_file: async (path, content) => {
219
+ write_text_file: async (path, content, options) => {
220
+ if (options?.exclusive && (mock_fs.has(path) || mock_fs_bytes.has(path))) {
221
+ const error = new Error(`EEXIST: file already exists: ${path}`);
222
+ error.code = 'EEXIST';
223
+ throw error;
224
+ }
197
225
  mock_fs.set(path, content);
226
+ if (options?.mode !== undefined)
227
+ mock_fs_modes.set(path, options.mode);
198
228
  },
199
229
  write_file: async (path, data) => {
200
230
  mock_fs_bytes.set(path, data);
@@ -236,10 +266,20 @@ export const create_mock_runtime = (args = []) => {
236
266
  mock_fs_bytes.set(new_path, bytes);
237
267
  mock_fs_bytes.delete(old_path);
238
268
  }
269
+ // the inode's mode travels with the rename (POSIX semantics)
270
+ const mode = mock_fs_modes.get(old_path);
271
+ if (mode !== undefined) {
272
+ mock_fs_modes.set(new_path, mode);
273
+ mock_fs_modes.delete(old_path);
274
+ }
275
+ else {
276
+ mock_fs_modes.delete(new_path);
277
+ }
239
278
  },
240
279
  remove: async (path, options) => {
241
280
  mock_fs.delete(path);
242
281
  mock_fs_bytes.delete(path);
282
+ mock_fs_modes.delete(path);
243
283
  mock_dirs.delete(path);
244
284
  if (options?.recursive) {
245
285
  const prefix = path.endsWith('/') ? path : path + '/';
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,OAAO,KAAK,EAAE,WAAW,EAA6B,MAAM,WAAW,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,OAAM,aAAa,CAAC,MAAM,CAAyB,KACjD,WAkMD,CAAC"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/node.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,OAAO,KAAK,EAAE,WAAW,EAA6B,MAAM,WAAW,CAAC;AAGxE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,OAAM,aAAa,CAAC,MAAM,CAAyB,KACjD,WAwMD,CAAC"}
@@ -13,6 +13,7 @@ import { stat, mkdir, readFile, readdir, writeFile, rename, rm, open } from 'nod
13
13
  import process from 'node:process';
14
14
  import { Readable } from 'node:stream';
15
15
  import { pipeline } from 'node:stream/promises';
16
+ import { load_secure_file_node } from './secure_file.js';
16
17
  /**
17
18
  * Create a `RuntimeDeps` backed by Node.js APIs.
18
19
  *
@@ -50,6 +51,7 @@ export const create_node_runtime = (args = process.argv.slice(2)) => ({
50
51
  },
51
52
  read_text_file: (path) => readFile(path, 'utf-8'),
52
53
  read_file: (path) => readFile(path).then((buf) => new Uint8Array(buf)),
54
+ read_secure_file: load_secure_file_node,
53
55
  // `Readable.toWeb` / `fromWeb` bridge Node streams to the web stream shape
54
56
  // the interface speaks. The casts cross Node's `stream/web` ReadableStream
55
57
  // and the global DOM `ReadableStream` (structurally identical here).
@@ -84,7 +86,11 @@ export const create_node_runtime = (args = process.argv.slice(2)) => ({
84
86
  }
85
87
  },
86
88
  readdir: (path) => readdir(path),
87
- write_text_file: (path, content) => writeFile(path, content, 'utf-8'),
89
+ write_text_file: (path, content, options) => writeFile(path, content, {
90
+ encoding: 'utf-8',
91
+ mode: options?.mode,
92
+ flag: options?.exclusive ? 'wx' : 'w'
93
+ }),
88
94
  write_file: (path, data) => writeFile(path, data),
89
95
  rename: (old_path, new_path) => rename(old_path, new_path),
90
96
  fsync: async (path) => {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Hardened secret-file read — the TS twin of the Rust spine's
3
+ * `fuz_sys::secure_file::load_secure_file`.
4
+ *
5
+ * The file this guards is typically the highest-value credential on the host
6
+ * (the bootstrap token mints the keeper account), so the read fails loud
7
+ * rather than degrading: a symlink, a group/other-accessible mode, or an
8
+ * oversized file is refused, never returned. The deploy recipe places the
9
+ * token at `0600`, so the check ratifies the shape zap produces; a
10
+ * hand-placed `0644` file fails at boot instead of being read.
11
+ *
12
+ * The Node implementation lives here (`load_secure_file_node`, wired as
13
+ * `create_node_runtime().read_secure_file`); the Deno runtime implements the
14
+ * same contract over `Deno.open` in `runtime/deno.ts`, and the mock honors
15
+ * the mode/size checks over its in-memory map. All three refuse through the
16
+ * shared `assert_secure_mode` / `assert_secure_size` / `read_secure_bounded`
17
+ * helpers so the checks — and their operator-facing messages — can't drift
18
+ * between runtimes.
19
+ *
20
+ * @module
21
+ */
22
+ /**
23
+ * Maximum size for secure files. Prevents DoS from unexpectedly large files;
24
+ * 4 KiB is generous for token/key material. Matches the Rust twin's
25
+ * `MAX_SECURE_FILE_SIZE`.
26
+ */
27
+ export declare const MAX_SECURE_FILE_SIZE = 4096;
28
+ /**
29
+ * Refuse any group/other-accessible mode (only `0600`/`0400` pass).
30
+ *
31
+ * Callers own the platform gating: the real runtimes skip the check where
32
+ * modes aren't meaningful (Node on Windows, a `null` Deno mode); the mock
33
+ * checks its simulated modes unconditionally.
34
+ *
35
+ * @throws Error naming the path, the offending mode, and the `chmod` fix
36
+ */
37
+ export declare const assert_secure_mode: (path: string, mode: number) => void;
38
+ /**
39
+ * Refuse a byte count over `MAX_SECURE_FILE_SIZE`.
40
+ *
41
+ * @throws Error naming the path, the size, and the cap
42
+ */
43
+ export declare const assert_secure_size: (path: string, size: number) => void;
44
+ /**
45
+ * Drain `read_chunk` into a cap-bounded buffer.
46
+ *
47
+ * Reads up to cap + 1 bytes so a file growing between stat and read is still
48
+ * caught (the stat-time size check alone races). `read_chunk` fills the given
49
+ * target from the file's current position and returns the bytes read
50
+ * (`null`/`0` = EOF) — the seam that lets the Node and Deno handle APIs share
51
+ * one loop.
52
+ *
53
+ * @throws Error when more than `MAX_SECURE_FILE_SIZE` bytes arrive
54
+ */
55
+ export declare const read_secure_bounded: (path: string, read_chunk: (target: Uint8Array) => Promise<number | null>) => Promise<Uint8Array>;
56
+ /**
57
+ * Read a secret file with fail-loud checks (Node implementation).
58
+ *
59
+ * - `O_NOFOLLOW` atomically rejects symlinks during open (no TOCTOU window)
60
+ * - the permission check runs on the open descriptor, not the path, so the
61
+ * file can't be swapped between check and read — any group/other access
62
+ * (not `0600`/`0400`) is refused
63
+ * - a size cap bounds the read; the read itself is limited to cap + 1 so a
64
+ * file growing between stat and read is still caught
65
+ *
66
+ * @param path - path to the secret file
67
+ * @returns the file's bytes
68
+ * @throws Error on a missing file, symlink, permissive mode, oversized file, or I/O failure
69
+ */
70
+ export declare const load_secure_file_node: (path: string) => Promise<Uint8Array>;
71
+ //# sourceMappingURL=secure_file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure_file.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/secure_file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAUzC;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,MAAM,MAAM,KAAG,IAM/D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,MAAM,MAAM,KAAG,IAI/D,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,MAAM,EACZ,YAAY,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,KACxD,OAAO,CAAC,UAAU,CAUpB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,UAAU,CAuB5E,CAAC"}