@fuzdev/fuz_app 0.109.0 → 0.110.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/CLAUDE.md +6 -5
- package/dist/auth/account_action_specs.d.ts +18 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +14 -1
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +6 -2
- package/dist/auth/bootstrap_account.d.ts +7 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +9 -4
- package/dist/auth/bootstrap_routes.d.ts +12 -4
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -6
- package/dist/auth/daemon_token_middleware.d.ts +7 -69
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +8 -104
- package/dist/auth/deps.d.ts +9 -5
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/middleware.js +1 -1
- package/dist/auth/request_context.d.ts +1 -3
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +2 -5
- package/dist/auth/session_cookie.d.ts +18 -36
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +16 -26
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +1 -4
- package/dist/auth/session_queries.d.ts +14 -30
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +15 -45
- package/dist/auth/token_lifetime.d.ts +51 -0
- package/dist/auth/token_lifetime.d.ts.map +1 -0
- package/dist/auth/token_lifetime.js +54 -0
- package/dist/dev/setup.d.ts +5 -9
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +18 -21
- package/dist/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +27 -1
- package/dist/runtime/deps.d.ts +38 -4
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/fs.d.ts +24 -5
- package/dist/runtime/fs.d.ts.map +1 -1
- package/dist/runtime/fs.js +38 -7
- package/dist/runtime/mock.d.ts +7 -0
- package/dist/runtime/mock.d.ts.map +1 -1
- package/dist/runtime/mock.js +41 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +7 -1
- package/dist/runtime/secure_file.d.ts +71 -0
- package/dist/runtime/secure_file.d.ts.map +1 -0
- package/dist/runtime/secure_file.js +125 -0
- package/dist/server/app_backend.d.ts +6 -5
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +2 -3
- package/dist/testing/CLAUDE.md +15 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +57 -20
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +13 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +5 -1
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +34 -1
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +2 -1
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.d.ts +15 -0
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -0
- package/dist/testing/cross_backend/token_lifetime.js +99 -0
- package/dist/testing/cross_backend/token_scope_surface.js +1 -1
- package/dist/testing/daemon_token_rotation.d.ts +83 -0
- package/dist/testing/daemon_token_rotation.d.ts.map +1 -0
- package/dist/testing/daemon_token_rotation.js +99 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +4 -4
- package/dist/ui/AccountSessions.svelte +0 -5
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +7 -4
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +0 -5
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/dev/setup.d.ts
CHANGED
|
@@ -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,
|
|
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)
|
|
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,
|
|
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
|
|
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,
|
|
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
|
*/
|
package/dist/dev/setup.d.ts.map
CHANGED
|
@@ -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;
|
|
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,
|
|
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)
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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;
|
|
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"}
|
package/dist/runtime/deno.js
CHANGED
|
@@ -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) => {
|
package/dist/runtime/deps.d.ts
CHANGED
|
@@ -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":"AAAA;;;;;;;;GAQG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACvB,yCAAyC;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC9C,mCAAmC;IACnC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,+DAA+D;IAC/D,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,8DAA8D;IAC9D,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,+DAA+D;IAC/D,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACjD;;;;;;OAMG;IACH,qBAAqB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC3F,8FAA8F;IAC9F,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;CAClD;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,
|
|
1
|
+
{"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/deps.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/fs.d.ts
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
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
|
-
* @
|
|
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
|
package/dist/runtime/fs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/runtime/fs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
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"}
|
package/dist/runtime/fs.js
CHANGED
|
@@ -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
|
|
10
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
};
|
package/dist/runtime/mock.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/runtime/mock.js
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/runtime/node.js
CHANGED
|
@@ -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,
|
|
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"}
|