@fuzdev/fuz_app 0.108.0 → 0.110.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/actions/CLAUDE.md +1 -1
- package/dist/auth/CLAUDE.md +7 -6
- package/dist/auth/account_action_specs.d.ts +18 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +14 -1
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +6 -2
- package/dist/auth/audit_emitter.d.ts +9 -5
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.d.ts +7 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +16 -1
- package/dist/auth/bootstrap_account.d.ts +7 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +9 -4
- package/dist/auth/bootstrap_routes.d.ts +12 -4
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +26 -7
- package/dist/auth/daemon_token_middleware.d.ts +7 -69
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +8 -104
- package/dist/auth/deps.d.ts +9 -5
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/middleware.js +1 -1
- package/dist/auth/migrations.d.ts +7 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +25 -8
- package/dist/auth/request_context.d.ts +1 -3
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +2 -5
- package/dist/auth/session_cookie.d.ts +18 -36
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +16 -26
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +1 -4
- package/dist/auth/session_queries.d.ts +14 -30
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +15 -45
- package/dist/auth/token_lifetime.d.ts +51 -0
- package/dist/auth/token_lifetime.d.ts.map +1 -0
- package/dist/auth/token_lifetime.js +54 -0
- package/dist/db/CLAUDE.md +9 -5
- package/dist/db/cell_audit_queries.d.ts +4 -2
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +4 -2
- package/dist/db/pg_error.d.ts +12 -0
- package/dist/db/pg_error.d.ts.map +1 -1
- package/dist/db/pg_error.js +15 -1
- package/dist/dev/setup.d.ts +5 -9
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +18 -21
- package/dist/http/CLAUDE.md +78 -18
- package/dist/http/db_routes.d.ts +96 -3
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +263 -46
- package/dist/http/error_schemas.d.ts +12 -1
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -1
- package/dist/http/pending_effects.d.ts +5 -5
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +27 -1
- package/dist/runtime/deps.d.ts +38 -4
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/fs.d.ts +24 -5
- package/dist/runtime/fs.d.ts.map +1 -1
- package/dist/runtime/fs.js +38 -7
- package/dist/runtime/mock.d.ts +7 -0
- package/dist/runtime/mock.d.ts.map +1 -1
- package/dist/runtime/mock.js +41 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +7 -1
- package/dist/runtime/secure_file.d.ts +71 -0
- package/dist/runtime/secure_file.d.ts.map +1 -0
- package/dist/runtime/secure_file.js +125 -0
- package/dist/server/app_backend.d.ts +6 -5
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +2 -3
- package/dist/testing/CLAUDE.md +36 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +10 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +13 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +10 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +34 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +53 -1
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +2 -1
- package/dist/testing/cross_backend/query_shape.d.ts +10 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
- package/dist/testing/cross_backend/query_shape.js +128 -0
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.d.ts +15 -0
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -0
- package/dist/testing/cross_backend/token_lifetime.js +99 -0
- package/dist/testing/cross_backend/token_scope_surface.js +1 -1
- package/dist/testing/daemon_token_rotation.d.ts +83 -0
- package/dist/testing/daemon_token_rotation.d.ts.map +1 -0
- package/dist/testing/daemon_token_rotation.js +99 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +4 -4
- package/dist/ui/AccountSessions.svelte +0 -5
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +7 -4
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +0 -5
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +9 -4
- package/dist/ui/table_state.svelte.d.ts +18 -5
- package/dist/ui/table_state.svelte.d.ts.map +1 -1
- package/dist/ui/table_state.svelte.js +22 -7
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/dist/ui/ui_format.js +4 -0
- package/package.json +1 -1
|
@@ -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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
import { constants } from 'node:fs';
|
|
23
|
+
import { open } from 'node:fs/promises';
|
|
24
|
+
import process from 'node:process';
|
|
25
|
+
/**
|
|
26
|
+
* Maximum size for secure files. Prevents DoS from unexpectedly large files;
|
|
27
|
+
* 4 KiB is generous for token/key material. Matches the Rust twin's
|
|
28
|
+
* `MAX_SECURE_FILE_SIZE`.
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_SECURE_FILE_SIZE = 4096;
|
|
31
|
+
/**
|
|
32
|
+
* Whether POSIX file modes are meaningful on this platform. On Windows Node
|
|
33
|
+
* synthesizes modes (always group/other-readable), so enforcing the check
|
|
34
|
+
* would refuse every file; the Rust twin is Unix-only and never faces this.
|
|
35
|
+
* The mock runtime checks unconditionally — its modes are simulated.
|
|
36
|
+
*/
|
|
37
|
+
const modes_apply = process.platform !== 'win32';
|
|
38
|
+
/**
|
|
39
|
+
* Refuse any group/other-accessible mode (only `0600`/`0400` pass).
|
|
40
|
+
*
|
|
41
|
+
* Callers own the platform gating: the real runtimes skip the check where
|
|
42
|
+
* modes aren't meaningful (Node on Windows, a `null` Deno mode); the mock
|
|
43
|
+
* checks its simulated modes unconditionally.
|
|
44
|
+
*
|
|
45
|
+
* @throws Error naming the path, the offending mode, and the `chmod` fix
|
|
46
|
+
*/
|
|
47
|
+
export const assert_secure_mode = (path, mode) => {
|
|
48
|
+
if ((mode & 0o077) !== 0) {
|
|
49
|
+
throw new Error(`insecure permissions on ${path}: ${(mode & 0o777).toString(8)} (expected 0600) — fix with: chmod 600 ${path}`);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Refuse a byte count over `MAX_SECURE_FILE_SIZE`.
|
|
54
|
+
*
|
|
55
|
+
* @throws Error naming the path, the size, and the cap
|
|
56
|
+
*/
|
|
57
|
+
export const assert_secure_size = (path, size) => {
|
|
58
|
+
if (size > MAX_SECURE_FILE_SIZE) {
|
|
59
|
+
throw new Error(`secure file too large: ${path} (${size} bytes, max ${MAX_SECURE_FILE_SIZE})`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Drain `read_chunk` into a cap-bounded buffer.
|
|
64
|
+
*
|
|
65
|
+
* Reads up to cap + 1 bytes so a file growing between stat and read is still
|
|
66
|
+
* caught (the stat-time size check alone races). `read_chunk` fills the given
|
|
67
|
+
* target from the file's current position and returns the bytes read
|
|
68
|
+
* (`null`/`0` = EOF) — the seam that lets the Node and Deno handle APIs share
|
|
69
|
+
* one loop.
|
|
70
|
+
*
|
|
71
|
+
* @throws Error when more than `MAX_SECURE_FILE_SIZE` bytes arrive
|
|
72
|
+
*/
|
|
73
|
+
export const read_secure_bounded = async (path, read_chunk) => {
|
|
74
|
+
const buffer = new Uint8Array(MAX_SECURE_FILE_SIZE + 1);
|
|
75
|
+
let total = 0;
|
|
76
|
+
while (total < buffer.length) {
|
|
77
|
+
const n = await read_chunk(buffer.subarray(total));
|
|
78
|
+
if (n === null || n === 0)
|
|
79
|
+
break;
|
|
80
|
+
total += n;
|
|
81
|
+
}
|
|
82
|
+
assert_secure_size(path, total);
|
|
83
|
+
return buffer.subarray(0, total);
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Read a secret file with fail-loud checks (Node implementation).
|
|
87
|
+
*
|
|
88
|
+
* - `O_NOFOLLOW` atomically rejects symlinks during open (no TOCTOU window)
|
|
89
|
+
* - the permission check runs on the open descriptor, not the path, so the
|
|
90
|
+
* file can't be swapped between check and read — any group/other access
|
|
91
|
+
* (not `0600`/`0400`) is refused
|
|
92
|
+
* - a size cap bounds the read; the read itself is limited to cap + 1 so a
|
|
93
|
+
* file growing between stat and read is still caught
|
|
94
|
+
*
|
|
95
|
+
* @param path - path to the secret file
|
|
96
|
+
* @returns the file's bytes
|
|
97
|
+
* @throws Error on a missing file, symlink, permissive mode, oversized file, or I/O failure
|
|
98
|
+
*/
|
|
99
|
+
export const load_secure_file_node = async (path) => {
|
|
100
|
+
let handle;
|
|
101
|
+
try {
|
|
102
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// ELOOP means the path was a symlink (O_NOFOLLOW refused it).
|
|
106
|
+
if (err.code === 'ELOOP') {
|
|
107
|
+
throw new Error(`secure file is a symlink: ${path}`);
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const s = await handle.stat();
|
|
113
|
+
if (modes_apply)
|
|
114
|
+
assert_secure_mode(path, s.mode);
|
|
115
|
+
assert_secure_size(path, s.size);
|
|
116
|
+
// `position: null` reads sequentially from the handle's own cursor.
|
|
117
|
+
return await read_secure_bounded(path, async (target) => {
|
|
118
|
+
const { bytesRead } = await handle.read(target, 0, target.length, null);
|
|
119
|
+
return bytesRead;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await handle.close();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
@@ -16,7 +16,6 @@ import { type AuditEmitter } from '../auth/audit_emitter.js';
|
|
|
16
16
|
import type { DbType, Db } from '../db/db.js';
|
|
17
17
|
import type { Keyring } from '../auth/keyring.js';
|
|
18
18
|
import type { PasswordHashDeps } from '../auth/password.js';
|
|
19
|
-
import type { StatResult } from '../runtime/deps.js';
|
|
20
19
|
import { type MigrationNamespace, type MigrationResult } from '../db/migrate.js';
|
|
21
20
|
/**
|
|
22
21
|
* Result of `create_app_backend()` — database metadata + deps bundle.
|
|
@@ -83,10 +82,12 @@ export declare const default_audit_factory: AuditFactory;
|
|
|
83
82
|
* (e.g., tx uses `runtime.exit(1)` on invalid keys).
|
|
84
83
|
*/
|
|
85
84
|
export interface CreateAppBackendOptions {
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Hardened secret-file read for the bootstrap token — pass the runtime's
|
|
87
|
+
* `read_secure_file` (see `FsSecureReadDeps`). Rejects symlinks,
|
|
88
|
+
* group/other-accessible modes, and oversized files.
|
|
89
|
+
*/
|
|
90
|
+
read_secure_file: (path: string) => Promise<Uint8Array>;
|
|
90
91
|
/** Delete a file. */
|
|
91
92
|
delete_file: (path: string) => Promise<void>;
|
|
92
93
|
/** Database connection URL (`postgres://`, `file://`, or `memory://`). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,
|
|
1
|
+
{"version":3,"file":"app_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAkB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAIjG;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,oIAAoI;IACpI,QAAQ,CAAC,iBAAiB,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC3D,iEAAiE;IACjE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE;IAAE,EAAE,EAAE,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,KAAK,YAAY,CAAC;AAE7E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,EAAE,YACF,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,iFAAiF;IACjF,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAAa,EAAE,YAAY,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACzD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAU,SAAS,uBAAuB,KAAG,OAAO,CAAC,UAAU,CAiD7F,CAAC"}
|
|
@@ -39,7 +39,7 @@ export const default_audit_factory = ({ db, log }) => create_audit_emitter({ db,
|
|
|
39
39
|
* @throws Error if `migration_namespaces` contains a namespace in `reserved_migration_namespaces`
|
|
40
40
|
*/
|
|
41
41
|
export const create_app_backend = async (options) => {
|
|
42
|
-
const { database_url, keyring, password,
|
|
42
|
+
const { database_url, keyring, password, read_secure_file, delete_file, audit_factory } = options;
|
|
43
43
|
const log = options.log ?? new Logger('server');
|
|
44
44
|
const { db, close, db_type, db_name } = await create_db(database_url);
|
|
45
45
|
// Everything after `create_db` can throw — reserved-namespace check,
|
|
@@ -69,8 +69,7 @@ export const create_app_backend = async (options) => {
|
|
|
69
69
|
keyring,
|
|
70
70
|
password,
|
|
71
71
|
db,
|
|
72
|
-
|
|
73
|
-
read_text_file,
|
|
72
|
+
read_secure_file,
|
|
74
73
|
delete_file,
|
|
75
74
|
log,
|
|
76
75
|
audit
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -32,7 +32,7 @@ time (never runtime), where a throwing guard would break `vite build`.
|
|
|
32
32
|
- `stub_handler()` — fresh `Response('stub')`.
|
|
33
33
|
- `stub_mw` — pass-through middleware (`async (_c, next) => next()`).
|
|
34
34
|
- `stub_app_deps` — frozen `AppDeps`, every capability throwing, `audit` a no-op `AuditEmitter` from `create_test_audit_emitter`.
|
|
35
|
-
- `create_stub_app_deps()` — factory: fresh `AppDeps` with no-op
|
|
35
|
+
- `create_stub_app_deps()` — factory: fresh `AppDeps` with no-op keyring/password/`delete_file`, a `read_secure_file` that throws ENOENT (the no-token-file state), a `create_noop_stub` DB, silent `Logger`, no-op `audit`.
|
|
36
36
|
- `create_test_audit_emitter()` — no-op `AuditEmitter`; `emit` / `emit_role_grant_target` no-op, `emit_pool` resolves immediately, `notify` no-op, `add_listener` throws (use `create_recording_audit_emitter` for a listener-accepting emitter), `listener_count` returns 0.
|
|
37
37
|
- `create_stub_audit_sse()` — no-op `AuditLogSse` for surface-test wiring without booting real SSE. `subscribe` returns a no-op cleanup; `on_audit_event` no-op; `registry` is a fresh `SubscriberRegistry` (live `.size` / `.close_*` for registry-state tests, isolated per call). For real SSE plumbing build via `create_audit_log_sse` against `create_test_app`.
|
|
38
38
|
- `create_stub_api_middleware({include_daemon_token?})` — stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth. See `auth/CLAUDE.md` §Middleware for the real stack.
|
|
@@ -1397,6 +1397,41 @@ in-process `auth/origin_parity.db.test.ts` + the cross-process
|
|
|
1397
1397
|
Rust spine returned a plain-text body — now converged to the canonical TS
|
|
1398
1398
|
`{error: "forbidden_origin"}` via `fuz_http::forbidden_origin_response()`.
|
|
1399
1399
|
|
|
1400
|
+
### Query-string shape parity — `cross_backend/query_shape.ts`
|
|
1401
|
+
|
|
1402
|
+
`describe_query_shape_cross_tests({setup_test, capabilities, rpc_path?})` —
|
|
1403
|
+
the imperative duplicate-key / unknown-key / phase-order query-semantics
|
|
1404
|
+
suite over the three spine surfaces that read a query:
|
|
1405
|
+
`GET /api/account/status` (no query schema — duplicated `acting` first-wins,
|
|
1406
|
+
unknown keys ignored, anonymous junk query → 401 never a query 400), the
|
|
1407
|
+
JSON-RPC GET endpoint (`method` / `id` / `params` first-wins — a last-wins
|
|
1408
|
+
reader dispatches a different method per backend on the same request), and
|
|
1409
|
+
`GET /api/facts/:hash` (gated on `capabilities.fact_serving`; strict schema —
|
|
1410
|
+
unknown key / malformed `acting` → 400 `invalid_query_params` **before** the
|
|
1411
|
+
401 guard, duplicated `acting` first-wins). Imperative because duplicated
|
|
1412
|
+
query keys can't be expressed through the structured RPC helpers. Pinned
|
|
1413
|
+
after the Rust spine's derived-serde `Query` extractors were converged onto
|
|
1414
|
+
`fuz_http`'s `query_first` / `assert_known_query_keys` (a pre-handler
|
|
1415
|
+
"duplicate field" 400 is an anonymously observable backend fingerprint).
|
|
1416
|
+
Cross-process only — the per-surface in-process TS pins live beside each
|
|
1417
|
+
module's own tests (`account_status.test.ts`, `action_rpc.test.ts`,
|
|
1418
|
+
`serve_fact_route.db.test.ts`); fuz_app's own wiring is
|
|
1419
|
+
`cross_backend/query_shape.cross.test.ts`.
|
|
1420
|
+
|
|
1421
|
+
### API-token lifetime parity — `cross_backend/token_lifetime.ts`
|
|
1422
|
+
|
|
1423
|
+
`describe_token_lifetime_cross_tests({setup_test, rpc_path?})` — the
|
|
1424
|
+
imperative round-trip on `account_token_create`'s required `lifetime` union:
|
|
1425
|
+
a `{kind:'ttl', days}` mint returns and stores a bounded `expires_at`
|
|
1426
|
+
(create output + `account_token_list` read-back), `{kind:'eternal'}` stores
|
|
1427
|
+
NULL, and an omitted or out-of-range lifetime 400s identically on both
|
|
1428
|
+
spines. Exists because the action-manifest parity gate captures no param
|
|
1429
|
+
schemas while the Rust spine hand-parses params — a spine that silently
|
|
1430
|
+
dropped the field would answer the same mint with a 200 and an eternal
|
|
1431
|
+
token; only the stored-state read-back catches it. Ungated (mint + list are
|
|
1432
|
+
on every spine's standard surface). Runs both legs
|
|
1433
|
+
(`token_lifetime_parity.db.test.ts` + `token_lifetime.cross.test.ts`).
|
|
1434
|
+
|
|
1400
1435
|
### Readiness probe parity — `cross_backend/ready.ts`
|
|
1401
1436
|
|
|
1402
1437
|
`describe_ready_cross_tests({setup_test, capabilities, ready_path?})` — the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAW3E,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,0BAA0B,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,uCAAuC;IACvC,YAAY,EAAE,mBAAmB,CAAC;IAClC,uFAAuF;IACvF,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;OAGG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAiBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,
|
|
1
|
+
{"version":3,"file":"admin_integration.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/admin_integration.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAgC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAW3E,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,0BAA0B,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,mCAAmC;IACnD;;;;OAIG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,uCAAuC;IACvC,YAAY,EAAE,mBAAmB,CAAC;IAClC,uFAAuF;IACvF,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4GAA4G;IAC5G,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;OAGG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAiBD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,mCAAmC,KAC1C,IA23BF,CAAC"}
|
|
@@ -594,7 +594,11 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
594
594
|
app: { request: fixture.transport },
|
|
595
595
|
path: rpc_path,
|
|
596
596
|
spec: account_token_create_action_spec,
|
|
597
|
-
params: {
|
|
597
|
+
params: {
|
|
598
|
+
name: 'audit-test-token',
|
|
599
|
+
scope: { kind: 'full' },
|
|
600
|
+
lifetime: { kind: 'eternal' }
|
|
601
|
+
},
|
|
598
602
|
headers: fixture.create_session_headers()
|
|
599
603
|
});
|
|
600
604
|
assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
|
|
@@ -721,7 +725,11 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
721
725
|
app: { request: fixture.transport },
|
|
722
726
|
path: rpc_path,
|
|
723
727
|
spec: account_token_create_action_spec,
|
|
724
|
-
params: {
|
|
728
|
+
params: {
|
|
729
|
+
name: 'admin-b-token',
|
|
730
|
+
scope: { kind: 'full' },
|
|
731
|
+
lifetime: { kind: 'eternal' }
|
|
732
|
+
},
|
|
725
733
|
headers: create_headers(admin_b.session_cookie)
|
|
726
734
|
});
|
|
727
735
|
assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
|