@fuzdev/fuz_app 0.99.0 → 0.101.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/dist/dev/setup.d.ts +9 -1
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +11 -11
- package/dist/env/dotenv.d.ts +3 -0
- package/dist/env/dotenv.d.ts.map +1 -1
- package/dist/env/dotenv.js +12 -5
- package/dist/env/update_env_variable.d.ts +1 -0
- package/dist/env/update_env_variable.d.ts.map +1 -1
- package/dist/env/update_env_variable.js +10 -5
- package/dist/server/serve_fact_route.d.ts +12 -7
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +13 -10
- package/dist/server/x_accel.d.ts +83 -0
- package/dist/server/x_accel.d.ts.map +1 -0
- package/dist/server/x_accel.js +148 -0
- package/dist/testing/CLAUDE.md +3 -2
- package/dist/testing/cross_backend/account_lifecycle.js +1 -1
- package/dist/testing/cross_backend/actor_lookup.js +1 -1
- package/dist/testing/cross_backend/actor_search.js +1 -1
- package/dist/testing/cross_backend/app_settings.js +1 -1
- package/dist/testing/cross_backend/body_size.js +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.js +1 -1
- package/dist/testing/cross_backend/cell_crud.js +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +1 -1
- package/dist/testing/cross_backend/cell_relations.js +1 -1
- package/dist/testing/cross_backend/credential_header_robustness.js +1 -1
- package/dist/testing/cross_backend/default_spine_surface.d.ts +0 -30
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +6 -30
- package/dist/testing/cross_backend/fact_serving.js +1 -1
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +2 -1
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.js +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +47 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_surface_constants.js +46 -0
- package/dist/testing/cross_backend/testing_backdoor.js +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +0 -11
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +4 -14
- package/package.json +1 -1
package/dist/dev/setup.d.ts
CHANGED
|
@@ -96,12 +96,20 @@ export declare const generate_random_key: (deps: CommandDeps) => Promise<string>
|
|
|
96
96
|
/**
|
|
97
97
|
* Read a single env var from a dotenv-style file.
|
|
98
98
|
*
|
|
99
|
+
* Delegates to `load_env_file` so the value is tokenized exactly like every
|
|
100
|
+
* other dotenv read — surrounding quotes, inline comments, and escapes are
|
|
101
|
+
* handled, and a leading `export ` is tolerated. Returns `undefined` when the
|
|
102
|
+
* file is absent or the variable is unset; a read error other than the file
|
|
103
|
+
* not existing (permission denied, I/O failure) propagates rather than being
|
|
104
|
+
* masked as "not found".
|
|
105
|
+
*
|
|
99
106
|
* @param deps - file read capability
|
|
100
107
|
* @param env_path - path to the .env file
|
|
101
108
|
* @param name - the variable name to read
|
|
102
109
|
* @returns the value, or `undefined` if the file or variable doesn't exist
|
|
110
|
+
* @throws Error if reading fails for any reason other than `ENOENT` / `NotFound`
|
|
103
111
|
*/
|
|
104
|
-
export declare const read_env_var: (deps: Pick<FsReadDeps, "
|
|
112
|
+
export declare const read_env_var: (deps: Pick<FsReadDeps, "read_text_file">, env_path: string, name: string) => Promise<string | undefined>;
|
|
105
113
|
/**
|
|
106
114
|
* Create an env file from its example template, auto-generating `SECRET_FUZ_COOKIE_KEYS`.
|
|
107
115
|
*
|
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,EAAC,SAAS,EAAC,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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
+
import { load_env_file } from '../env/dotenv.js';
|
|
11
12
|
import { query_account_by_username, query_actors_by_account, query_create_account_with_actor, } from '../auth/account_queries.js';
|
|
12
13
|
import { query_create_role_grant } from '../auth/role_grant_queries.js';
|
|
13
14
|
/** Default logger using bracket format. */
|
|
@@ -49,23 +50,22 @@ export const generate_random_key = async (deps) => {
|
|
|
49
50
|
/**
|
|
50
51
|
* Read a single env var from a dotenv-style file.
|
|
51
52
|
*
|
|
53
|
+
* Delegates to `load_env_file` so the value is tokenized exactly like every
|
|
54
|
+
* other dotenv read — surrounding quotes, inline comments, and escapes are
|
|
55
|
+
* handled, and a leading `export ` is tolerated. Returns `undefined` when the
|
|
56
|
+
* file is absent or the variable is unset; a read error other than the file
|
|
57
|
+
* not existing (permission denied, I/O failure) propagates rather than being
|
|
58
|
+
* masked as "not found".
|
|
59
|
+
*
|
|
52
60
|
* @param deps - file read capability
|
|
53
61
|
* @param env_path - path to the .env file
|
|
54
62
|
* @param name - the variable name to read
|
|
55
63
|
* @returns the value, or `undefined` if the file or variable doesn't exist
|
|
64
|
+
* @throws Error if reading fails for any reason other than `ENOENT` / `NotFound`
|
|
56
65
|
*/
|
|
57
66
|
export const read_env_var = async (deps, env_path, name) => {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
return undefined;
|
|
61
|
-
try {
|
|
62
|
-
const content = await deps.read_text_file(env_path);
|
|
63
|
-
const match = new RegExp(`^${name}=(.+)$`, 'm').exec(content);
|
|
64
|
-
return match?.[1]?.trim();
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
67
|
+
const env = await load_env_file(deps, env_path);
|
|
68
|
+
return env?.[name];
|
|
69
69
|
};
|
|
70
70
|
// === Setup helpers ===
|
|
71
71
|
/**
|
package/dist/env/dotenv.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ import type { FsReadDeps } from '../runtime/deps.js';
|
|
|
20
20
|
* values keep `#` literal when no whitespace precedes it so URL fragments
|
|
21
21
|
* like `KEY=https://x.com#frag` round-trip unchanged.
|
|
22
22
|
*
|
|
23
|
+
* A leading `export ` on a line is ignored, so a shell-sourceable `.env`
|
|
24
|
+
* (`export KEY=value`) parses identically to a plain `KEY=value`.
|
|
25
|
+
*
|
|
23
26
|
* Trailing whitespace on unquoted values is lost (the raw value is trimmed);
|
|
24
27
|
* wrap the value in `"..."` or `'...'` to preserve surrounding spacing.
|
|
25
28
|
*
|
package/dist/env/dotenv.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dotenv.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/env/dotenv.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AAEnD
|
|
1
|
+
{"version":3,"file":"dotenv.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/env/dotenv.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAqCnE,CAAC;AAiEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GACzB,SAAS,IAAI,CAAC,UAAU,EAAE,gBAAgB,CAAC,EAC3C,MAAM,MAAM,KACV,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CASvC,CAAC"}
|
package/dist/env/dotenv.js
CHANGED
|
@@ -19,25 +19,32 @@
|
|
|
19
19
|
* values keep `#` literal when no whitespace precedes it so URL fragments
|
|
20
20
|
* like `KEY=https://x.com#frag` round-trip unchanged.
|
|
21
21
|
*
|
|
22
|
+
* A leading `export ` on a line is ignored, so a shell-sourceable `.env`
|
|
23
|
+
* (`export KEY=value`) parses identically to a plain `KEY=value`.
|
|
24
|
+
*
|
|
22
25
|
* Trailing whitespace on unquoted values is lost (the raw value is trimmed);
|
|
23
26
|
* wrap the value in `"..."` or `'...'` to preserve surrounding spacing.
|
|
24
27
|
*
|
|
25
28
|
* @param content - dotenv file content
|
|
26
29
|
* @returns parsed key-value pairs
|
|
27
30
|
*/
|
|
28
|
-
// Line tokenization (trim, skip empties/comments,
|
|
29
|
-
// mirrored in `update_env_variable.ts`'s
|
|
31
|
+
// Line tokenization (trim, skip empties/comments, strip a leading `export `,
|
|
32
|
+
// split on first `=`) is mirrored in `update_env_variable.ts`'s
|
|
33
|
+
// `find_last_key_line_index`.
|
|
30
34
|
export const parse_dotenv = (content) => {
|
|
31
35
|
const result = {};
|
|
32
36
|
for (const line of content.split('\n')) {
|
|
33
37
|
const trimmed = line.trim();
|
|
34
38
|
if (!trimmed || trimmed.startsWith('#'))
|
|
35
39
|
continue;
|
|
36
|
-
|
|
40
|
+
// tolerate a leading `export ` so shell-sourceable `.env` files parse
|
|
41
|
+
// the same as plain `KEY=value` ones
|
|
42
|
+
const assignment = trimmed.startsWith('export ') ? trimmed.slice(7) : trimmed;
|
|
43
|
+
const eq_index = assignment.indexOf('=');
|
|
37
44
|
if (eq_index === -1)
|
|
38
45
|
continue;
|
|
39
|
-
const key =
|
|
40
|
-
let value =
|
|
46
|
+
const key = assignment.slice(0, eq_index).trim();
|
|
47
|
+
let value = assignment.slice(eq_index + 1).trim();
|
|
41
48
|
if (value.startsWith('"')) {
|
|
42
49
|
const close = find_closing_double_quote(value);
|
|
43
50
|
if (close !== -1 && is_comment_or_empty_after(value, close)) {
|
|
@@ -22,6 +22,7 @@ export interface UpdateEnvVariableOptions {
|
|
|
22
22
|
* - **Duplicate keys**: updates the LAST occurrence (matches dotenv behavior)
|
|
23
23
|
* - **Inline comments**: preserved after the value (e.g., `KEY=value # comment`)
|
|
24
24
|
* - **Quote style**: preserved from original (quoted/unquoted)
|
|
25
|
+
* - **`export ` prefix**: preserved when updating an `export KEY=…` line
|
|
25
26
|
*
|
|
26
27
|
* @warning Not atomic; not safe for concurrent writers. Reads the file, mutates
|
|
27
28
|
* in memory, then writes it back — a crash or concurrent write can corrupt
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update_env_variable.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/env/update_env_variable.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,6BAA6B;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,oFAAoF;IACpF,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChF;AAED
|
|
1
|
+
{"version":3,"file":"update_env_variable.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/env/update_env_variable.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,6BAA6B;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,oFAAoF;IACpF,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,MAAM,EACX,OAAO,MAAM,EACb,SAAS,wBAAwB,KAC/B,OAAO,CAAC,IAAI,CAsDd,CAAC"}
|
|
@@ -13,6 +13,7 @@ import { resolve } from 'node:path';
|
|
|
13
13
|
* - **Duplicate keys**: updates the LAST occurrence (matches dotenv behavior)
|
|
14
14
|
* - **Inline comments**: preserved after the value (e.g., `KEY=value # comment`)
|
|
15
15
|
* - **Quote style**: preserved from original (quoted/unquoted)
|
|
16
|
+
* - **`export ` prefix**: preserved when updating an `export KEY=…` line
|
|
16
17
|
*
|
|
17
18
|
* @warning Not atomic; not safe for concurrent writers. Reads the file, mutates
|
|
18
19
|
* in memory, then writes it back — a crash or concurrent write can corrupt
|
|
@@ -49,14 +50,17 @@ export const update_env_variable = async (key, value, options) => {
|
|
|
49
50
|
const last_match_idx = find_last_key_line_index(lines, key);
|
|
50
51
|
const updated_lines = lines.map((line, idx) => {
|
|
51
52
|
if (idx === last_match_idx) {
|
|
53
|
+
// preserve a leading `export ` (the rest of the line's leading
|
|
54
|
+
// whitespace is normalized away, same as for a plain assignment)
|
|
55
|
+
const export_prefix = line.trimStart().startsWith('export ') ? 'export ' : '';
|
|
52
56
|
const equals_pos = line.indexOf('=');
|
|
53
57
|
const value_part = line.substring(equals_pos + 1);
|
|
54
58
|
const inline_comment = extract_inline_comment(value_part);
|
|
55
59
|
const trimmed_value = value_part.trim();
|
|
56
60
|
const has_quotes = is_quoted_value(trimmed_value);
|
|
57
61
|
return has_quotes
|
|
58
|
-
? `${key}=${quote_value(value)}${inline_comment}`
|
|
59
|
-
: `${key}=${value}${inline_comment}`;
|
|
62
|
+
? `${export_prefix}${key}=${quote_value(value)}${inline_comment}`
|
|
63
|
+
: `${export_prefix}${key}=${value}${inline_comment}`;
|
|
60
64
|
}
|
|
61
65
|
return line;
|
|
62
66
|
});
|
|
@@ -71,7 +75,7 @@ export const update_env_variable = async (key, value, options) => {
|
|
|
71
75
|
await write_file(file_path, updated_content, 'utf-8');
|
|
72
76
|
};
|
|
73
77
|
// Keep this tokenization aligned with `parse_dotenv` in `env/dotenv.ts`:
|
|
74
|
-
// trim, skip empties/comments, split on the first `=`.
|
|
78
|
+
// trim, skip empties/comments, strip a leading `export `, split on the first `=`.
|
|
75
79
|
const find_last_key_line_index = (lines, key) => {
|
|
76
80
|
if (!key)
|
|
77
81
|
return -1;
|
|
@@ -80,10 +84,11 @@ const find_last_key_line_index = (lines, key) => {
|
|
|
80
84
|
const trimmed = line.trim();
|
|
81
85
|
if (!trimmed || trimmed.startsWith('#'))
|
|
82
86
|
return;
|
|
83
|
-
const
|
|
87
|
+
const assignment = trimmed.startsWith('export ') ? trimmed.slice(7) : trimmed;
|
|
88
|
+
const eq_index = assignment.indexOf('=');
|
|
84
89
|
if (eq_index === -1)
|
|
85
90
|
return;
|
|
86
|
-
if (
|
|
91
|
+
if (assignment.slice(0, eq_index).trim() === key)
|
|
87
92
|
last_match_idx = idx;
|
|
88
93
|
});
|
|
89
94
|
return last_match_idx;
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
* (filesystem-backed `file:<shard>/<rest>` URLs) either return an
|
|
15
15
|
* `X-Accel-Redirect` header pointing into nginx's internal facts location
|
|
16
16
|
* (production) or stream from disk via the filesystem `FactExternalFetcher`
|
|
17
|
-
* (dev / tests). The runtime mode is selected by the optional
|
|
18
|
-
*
|
|
17
|
+
* (dev / tests). The runtime mode is selected by the optional `x_accel`
|
|
18
|
+
* factory option — a validated `XAccelConfig` (built via
|
|
19
|
+
* `create_x_accel_config` in `server/x_accel.ts`) set in prod, unset in dev.
|
|
20
|
+
* The handle can only be obtained by proving the facts nginx location is
|
|
21
|
+
* `internal;`, so X-Accel serving can't be enabled against a public location.
|
|
19
22
|
*
|
|
20
23
|
* REST, not RPC: binary responses don't fit the JSON-RPC envelope.
|
|
21
24
|
*
|
|
@@ -87,6 +90,7 @@
|
|
|
87
90
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
88
91
|
import { type RouteSpec } from '../http/route_spec.js';
|
|
89
92
|
import type { AppDeps } from '../auth/deps.js';
|
|
93
|
+
import type { XAccelConfig } from './x_accel.js';
|
|
90
94
|
export interface CreateServeFactRouteSpecOptions {
|
|
91
95
|
/**
|
|
92
96
|
* App deps reference. Currently unused at handler time (cell + fact
|
|
@@ -100,12 +104,13 @@ export interface CreateServeFactRouteSpecOptions {
|
|
|
100
104
|
facts_dir: string;
|
|
101
105
|
/**
|
|
102
106
|
* When set, external facts return an `X-Accel-Redirect` pointing at
|
|
103
|
-
* `${
|
|
104
|
-
* the bytes. When unset, external facts stream from
|
|
105
|
-
* `<facts_dir>/<shard>/<rest>` directly. Production sets
|
|
106
|
-
*
|
|
107
|
+
* `${x_accel.redirect_prefix}<shard>/<rest>` — nginx's internal facts
|
|
108
|
+
* location serves the bytes. When unset, external facts stream from
|
|
109
|
+
* `<facts_dir>/<shard>/<rest>` directly. Production sets a validated
|
|
110
|
+
* `XAccelConfig` (built via `create_x_accel_config`, which proves the
|
|
111
|
+
* facts location is `internal;`); tests + dev leave it unset.
|
|
107
112
|
*/
|
|
108
|
-
|
|
113
|
+
x_accel?: XAccelConfig;
|
|
109
114
|
log: Logger;
|
|
110
115
|
}
|
|
111
116
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAMH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAepD,OAAO,EAAsC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAM7C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,cAAc,CAAC;AA6D/C,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAgHD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SA8CF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SAgCF,CAAC"}
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
* (filesystem-backed `file:<shard>/<rest>` URLs) either return an
|
|
15
15
|
* `X-Accel-Redirect` header pointing into nginx's internal facts location
|
|
16
16
|
* (production) or stream from disk via the filesystem `FactExternalFetcher`
|
|
17
|
-
* (dev / tests). The runtime mode is selected by the optional
|
|
18
|
-
*
|
|
17
|
+
* (dev / tests). The runtime mode is selected by the optional `x_accel`
|
|
18
|
+
* factory option — a validated `XAccelConfig` (built via
|
|
19
|
+
* `create_x_accel_config` in `server/x_accel.ts`) set in prod, unset in dev.
|
|
20
|
+
* The handle can only be obtained by proving the facts nginx location is
|
|
21
|
+
* `internal;`, so X-Accel serving can't be enabled against a public location.
|
|
19
22
|
*
|
|
20
23
|
* REST, not RPC: binary responses don't fit the JSON-RPC envelope.
|
|
21
24
|
*
|
|
@@ -167,7 +170,7 @@ const params_400_error = {
|
|
|
167
170
|
* fact is genuinely absent or merely unviewable.
|
|
168
171
|
*/
|
|
169
172
|
const serve_fact_bytes = async (c, route, hash, config) => {
|
|
170
|
-
const { facts_dir,
|
|
173
|
+
const { facts_dir, x_accel, log } = config;
|
|
171
174
|
const meta = await query_get_fact_meta({ db: route.db }, hash);
|
|
172
175
|
if (!meta) {
|
|
173
176
|
return c.body(null, 404);
|
|
@@ -198,13 +201,13 @@ const serve_fact_bytes = async (c, route, hash, config) => {
|
|
|
198
201
|
return c.body(null, 404);
|
|
199
202
|
}
|
|
200
203
|
const { shard, rest } = parsed;
|
|
201
|
-
if (
|
|
202
|
-
// Production: hand off to nginx via the internal facts location.
|
|
204
|
+
if (x_accel !== undefined) {
|
|
205
|
+
// Production: hand off to nginx via the validated `internal;` facts location.
|
|
203
206
|
return c.body(null, 200, {
|
|
204
207
|
'Content-Type': content_type,
|
|
205
208
|
'Content-Length': size,
|
|
206
209
|
'Cache-Control': CACHE_CONTROL,
|
|
207
|
-
'X-Accel-Redirect': `${
|
|
210
|
+
'X-Accel-Redirect': `${x_accel.redirect_prefix}${shard}/${rest}`,
|
|
208
211
|
...FACT_SECURITY_HEADERS,
|
|
209
212
|
});
|
|
210
213
|
}
|
|
@@ -252,8 +255,8 @@ const build_public_request_context = async (c, route) => {
|
|
|
252
255
|
* from `c.var.account_id` and enforces visibility per-reference.
|
|
253
256
|
*/
|
|
254
257
|
export const create_serve_cell_fact_route_spec = (options) => {
|
|
255
|
-
const { facts_dir,
|
|
256
|
-
const config = { facts_dir,
|
|
258
|
+
const { facts_dir, x_accel, log } = options;
|
|
259
|
+
const config = { facts_dir, x_accel, log };
|
|
257
260
|
return {
|
|
258
261
|
method: 'GET',
|
|
259
262
|
path: '/api/cells/:cell_id/facts/:hash',
|
|
@@ -310,8 +313,8 @@ export const create_serve_cell_fact_route_spec = (options) => {
|
|
|
310
313
|
* hash to a non-admin.
|
|
311
314
|
*/
|
|
312
315
|
export const create_serve_fact_route_spec = (options) => {
|
|
313
|
-
const { facts_dir,
|
|
314
|
-
const config = { facts_dir,
|
|
316
|
+
const { facts_dir, x_accel, log } = options;
|
|
317
|
+
const config = { facts_dir, x_accel, log };
|
|
315
318
|
return {
|
|
316
319
|
method: 'GET',
|
|
317
320
|
path: '/api/facts/:hash',
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-loud validation of the X-Accel facts nginx location.
|
|
3
|
+
*
|
|
4
|
+
* The `X-Accel-Redirect` serving path's confidentiality depends on the facts
|
|
5
|
+
* nginx location being `internal;` — only the authz'd handler's redirect (an
|
|
6
|
+
* internal subrequest) may reach it. A *public* facts location would serve any
|
|
7
|
+
* fact's bytes to anyone who guesses the `<shard>/<rest>` path, bypassing every
|
|
8
|
+
* cell-visibility check. `XAccelConfig` makes that assertion structural: a
|
|
9
|
+
* consumer can only obtain the redirect prefix by building an `XAccelConfig`,
|
|
10
|
+
* which runs this check and throws **loudly** on a missing or non-`internal;`
|
|
11
|
+
* location — it cannot silently fall closed.
|
|
12
|
+
*
|
|
13
|
+
* This is a best-effort string check (brace-matched `location` blocks), the TS
|
|
14
|
+
* twin of the Rust `fuz_fact_serving` `nginx.rs` — not a real nginx parser, but
|
|
15
|
+
* it catches the security-critical omission. Distinct from
|
|
16
|
+
* `validate_nginx_config` (`server/validate_nginx.ts`), which validates the
|
|
17
|
+
* `/api` Authorization-strip + security headers and takes no facts-location
|
|
18
|
+
* path.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
/** Result of the facts-location check. */
|
|
24
|
+
export interface NginxFactsValidation {
|
|
25
|
+
/** `true` when the facts location exists and is `internal;`. */
|
|
26
|
+
ok: boolean;
|
|
27
|
+
/** Fatal issues — a missing or non-`internal` facts location. */
|
|
28
|
+
errors: Array<string>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Assert the nginx `location` serving the X-Accel facts prefix is `internal;`.
|
|
32
|
+
*
|
|
33
|
+
* `facts_location` is the path the X-Accel redirect prefix points at (e.g.
|
|
34
|
+
* `/_facts/`). Returns a fatal error when no matching `location` block exists,
|
|
35
|
+
* or when the matching block is not marked `internal;` — either is a public
|
|
36
|
+
* facts location that bypasses cell visibility.
|
|
37
|
+
*
|
|
38
|
+
* @param config - the nginx config template string to check
|
|
39
|
+
* @param facts_location - the facts location path the redirect prefix points at
|
|
40
|
+
* @returns `{ok, errors}` — `ok` is `true` only when the location exists and is `internal;`
|
|
41
|
+
*/
|
|
42
|
+
export declare const validate_facts_internal_location: (config: string, facts_location: string) => NginxFactsValidation;
|
|
43
|
+
/**
|
|
44
|
+
* A validated X-Accel redirect configuration — the only handle that enables the
|
|
45
|
+
* `X-Accel-Redirect` serving path in `server/serve_fact_route.ts`.
|
|
46
|
+
*
|
|
47
|
+
* The redirect prefix can be obtained **only** by passing the nginx config
|
|
48
|
+
* through `validate_facts_internal_location` (via `create_x_accel_config`), so
|
|
49
|
+
* X-Accel serving is impossible to enable without proving the facts location is
|
|
50
|
+
* `internal;` at boot — a public facts location would bypass every
|
|
51
|
+
* cell-visibility check. A Zod-branded type: the brand can't be forged without
|
|
52
|
+
* an explicit cast, so the factory is the only ordinary construction path.
|
|
53
|
+
*/
|
|
54
|
+
export declare const XAccelConfig: z.core.$ZodBranded<z.ZodObject<{
|
|
55
|
+
/** The validated redirect prefix the serving path prepends (e.g. `/_facts/`). */
|
|
56
|
+
redirect_prefix: z.ZodString;
|
|
57
|
+
}, z.core.$strict>, "XAccelConfig", "out">;
|
|
58
|
+
export type XAccelConfig = z.infer<typeof XAccelConfig>;
|
|
59
|
+
/**
|
|
60
|
+
* A misconfigured X-Accel facts location — a fail-loud boot error thrown by
|
|
61
|
+
* `create_x_accel_config`.
|
|
62
|
+
*/
|
|
63
|
+
export declare class XAccelConfigError extends Error {
|
|
64
|
+
/** The validator errors that made the location unsafe. */
|
|
65
|
+
readonly errors: Array<string>;
|
|
66
|
+
constructor(errors: Array<string>);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build a validated `XAccelConfig`, asserting the nginx `location` serving
|
|
70
|
+
* `redirect_prefix` is `internal;`.
|
|
71
|
+
*
|
|
72
|
+
* The make-impossible-states gate: serving can only emit `X-Accel-Redirect`
|
|
73
|
+
* into a location proven `internal;` at boot.
|
|
74
|
+
*
|
|
75
|
+
* @param redirect_prefix - the X-Accel redirect prefix (e.g. `/_facts/`)
|
|
76
|
+
* @param nginx_config - the nginx config template string to validate against
|
|
77
|
+
* @returns the validated `XAccelConfig` carrying `redirect_prefix`
|
|
78
|
+
* @throws `XAccelConfigError` when `nginx_config` has no matching `location` for
|
|
79
|
+
* `redirect_prefix`, or the matching block is not marked `internal;` — either
|
|
80
|
+
* is a public facts location that bypasses every cell-visibility check.
|
|
81
|
+
*/
|
|
82
|
+
export declare const create_x_accel_config: (redirect_prefix: string, nginx_config: string) => XAccelConfig;
|
|
83
|
+
//# sourceMappingURL=x_accel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"x_accel.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/x_accel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACpC,gEAAgE;IAChE,EAAE,EAAE,OAAO,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAuDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC5C,QAAQ,MAAM,EACd,gBAAgB,MAAM,KACpB,oBAqBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY;IAEvB,iFAAiF;;0CAG5D,CAAC;AACxB,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBACnB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;CAKjC;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,EACvB,cAAc,MAAM,KAClB,YAMF,CAAC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-loud validation of the X-Accel facts nginx location.
|
|
3
|
+
*
|
|
4
|
+
* The `X-Accel-Redirect` serving path's confidentiality depends on the facts
|
|
5
|
+
* nginx location being `internal;` — only the authz'd handler's redirect (an
|
|
6
|
+
* internal subrequest) may reach it. A *public* facts location would serve any
|
|
7
|
+
* fact's bytes to anyone who guesses the `<shard>/<rest>` path, bypassing every
|
|
8
|
+
* cell-visibility check. `XAccelConfig` makes that assertion structural: a
|
|
9
|
+
* consumer can only obtain the redirect prefix by building an `XAccelConfig`,
|
|
10
|
+
* which runs this check and throws **loudly** on a missing or non-`internal;`
|
|
11
|
+
* location — it cannot silently fall closed.
|
|
12
|
+
*
|
|
13
|
+
* This is a best-effort string check (brace-matched `location` blocks), the TS
|
|
14
|
+
* twin of the Rust `fuz_fact_serving` `nginx.rs` — not a real nginx parser, but
|
|
15
|
+
* it catches the security-critical omission. Distinct from
|
|
16
|
+
* `validate_nginx_config` (`server/validate_nginx.ts`), which validates the
|
|
17
|
+
* `/api` Authorization-strip + security headers and takes no facts-location
|
|
18
|
+
* path.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
/** Strip every trailing `/` so `/_facts/` and `/_facts` compare equal. */
|
|
24
|
+
const trim_trailing_slashes = (value) => value.replace(/\/+$/, '');
|
|
25
|
+
/** `true` if a location block body contains an `internal` directive. */
|
|
26
|
+
const is_internal = (content) => content.split(/[;\s]+/).some((token) => token === 'internal');
|
|
27
|
+
/**
|
|
28
|
+
* Extract `{path, content}` for each `location … { … }` block, brace-matched so
|
|
29
|
+
* nested blocks are kept whole. `path` is the last whitespace token before the
|
|
30
|
+
* opening brace (the location path, after any modifier). Best-effort: a stray
|
|
31
|
+
* `location` token in a comment can produce a spurious block, harmless for this
|
|
32
|
+
* check.
|
|
33
|
+
*/
|
|
34
|
+
const find_location_blocks = (config) => {
|
|
35
|
+
const blocks = [];
|
|
36
|
+
let search = 0;
|
|
37
|
+
let kw;
|
|
38
|
+
while ((kw = config.indexOf('location', search)) !== -1) {
|
|
39
|
+
// Require a token boundary before `location` (start or whitespace) so
|
|
40
|
+
// `relocation` etc. don't match.
|
|
41
|
+
const boundary_ok = kw === 0 || /\s/.test(config[kw - 1]);
|
|
42
|
+
const after = kw + 'location'.length;
|
|
43
|
+
const brace = config.indexOf('{', after);
|
|
44
|
+
if (brace === -1)
|
|
45
|
+
break;
|
|
46
|
+
if (!boundary_ok) {
|
|
47
|
+
search = after;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const header = config.slice(after, brace).trim();
|
|
51
|
+
const tokens = header.split(/\s+/).filter((token) => token.length > 0);
|
|
52
|
+
const path = tokens.length > 0 ? tokens[tokens.length - 1] : '';
|
|
53
|
+
// Brace-match the body.
|
|
54
|
+
let depth = 1;
|
|
55
|
+
let i = brace + 1;
|
|
56
|
+
while (i < config.length && depth > 0) {
|
|
57
|
+
const ch = config[i];
|
|
58
|
+
if (ch === '{')
|
|
59
|
+
depth++;
|
|
60
|
+
else if (ch === '}')
|
|
61
|
+
depth--;
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
blocks.push({ path, content: config.slice(brace, i) });
|
|
65
|
+
search = i;
|
|
66
|
+
}
|
|
67
|
+
return blocks;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Assert the nginx `location` serving the X-Accel facts prefix is `internal;`.
|
|
71
|
+
*
|
|
72
|
+
* `facts_location` is the path the X-Accel redirect prefix points at (e.g.
|
|
73
|
+
* `/_facts/`). Returns a fatal error when no matching `location` block exists,
|
|
74
|
+
* or when the matching block is not marked `internal;` — either is a public
|
|
75
|
+
* facts location that bypasses cell visibility.
|
|
76
|
+
*
|
|
77
|
+
* @param config - the nginx config template string to check
|
|
78
|
+
* @param facts_location - the facts location path the redirect prefix points at
|
|
79
|
+
* @returns `{ok, errors}` — `ok` is `true` only when the location exists and is `internal;`
|
|
80
|
+
*/
|
|
81
|
+
export const validate_facts_internal_location = (config, facts_location) => {
|
|
82
|
+
const errors = [];
|
|
83
|
+
const normalized = trim_trailing_slashes(facts_location);
|
|
84
|
+
const blocks = find_location_blocks(config);
|
|
85
|
+
const matching = blocks.filter((block) => trim_trailing_slashes(block.path) === normalized);
|
|
86
|
+
if (matching.length === 0) {
|
|
87
|
+
errors.push(`No nginx \`location ${facts_location}\` block found — the X-Accel facts location must ` +
|
|
88
|
+
`exist and be marked \`internal;\` so only the authz'd handler's redirect can reach it. ` +
|
|
89
|
+
`A public facts location bypasses every cell-visibility check.`);
|
|
90
|
+
}
|
|
91
|
+
else if (!matching.some((block) => is_internal(block.content))) {
|
|
92
|
+
errors.push(`nginx \`location ${facts_location}\` is NOT marked \`internal;\` — a public facts ` +
|
|
93
|
+
`location serves any fact's bytes to anyone who guesses the path, bypassing every ` +
|
|
94
|
+
`cell-visibility check. Add \`internal;\` to the block.`);
|
|
95
|
+
}
|
|
96
|
+
return { ok: errors.length === 0, errors };
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* A validated X-Accel redirect configuration — the only handle that enables the
|
|
100
|
+
* `X-Accel-Redirect` serving path in `server/serve_fact_route.ts`.
|
|
101
|
+
*
|
|
102
|
+
* The redirect prefix can be obtained **only** by passing the nginx config
|
|
103
|
+
* through `validate_facts_internal_location` (via `create_x_accel_config`), so
|
|
104
|
+
* X-Accel serving is impossible to enable without proving the facts location is
|
|
105
|
+
* `internal;` at boot — a public facts location would bypass every
|
|
106
|
+
* cell-visibility check. A Zod-branded type: the brand can't be forged without
|
|
107
|
+
* an explicit cast, so the factory is the only ordinary construction path.
|
|
108
|
+
*/
|
|
109
|
+
export const XAccelConfig = z
|
|
110
|
+
.strictObject({
|
|
111
|
+
/** The validated redirect prefix the serving path prepends (e.g. `/_facts/`). */
|
|
112
|
+
redirect_prefix: z.string(),
|
|
113
|
+
})
|
|
114
|
+
.brand('XAccelConfig');
|
|
115
|
+
/**
|
|
116
|
+
* A misconfigured X-Accel facts location — a fail-loud boot error thrown by
|
|
117
|
+
* `create_x_accel_config`.
|
|
118
|
+
*/
|
|
119
|
+
export class XAccelConfigError extends Error {
|
|
120
|
+
/** The validator errors that made the location unsafe. */
|
|
121
|
+
errors;
|
|
122
|
+
constructor(errors) {
|
|
123
|
+
super(`X-Accel facts location is not safely \`internal;\`: ${errors.join('; ')}`);
|
|
124
|
+
this.name = 'XAccelConfigError';
|
|
125
|
+
this.errors = errors;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Build a validated `XAccelConfig`, asserting the nginx `location` serving
|
|
130
|
+
* `redirect_prefix` is `internal;`.
|
|
131
|
+
*
|
|
132
|
+
* The make-impossible-states gate: serving can only emit `X-Accel-Redirect`
|
|
133
|
+
* into a location proven `internal;` at boot.
|
|
134
|
+
*
|
|
135
|
+
* @param redirect_prefix - the X-Accel redirect prefix (e.g. `/_facts/`)
|
|
136
|
+
* @param nginx_config - the nginx config template string to validate against
|
|
137
|
+
* @returns the validated `XAccelConfig` carrying `redirect_prefix`
|
|
138
|
+
* @throws `XAccelConfigError` when `nginx_config` has no matching `location` for
|
|
139
|
+
* `redirect_prefix`, or the matching block is not marked `internal;` — either
|
|
140
|
+
* is a public facts location that bypasses every cell-visibility check.
|
|
141
|
+
*/
|
|
142
|
+
export const create_x_accel_config = (redirect_prefix, nginx_config) => {
|
|
143
|
+
const validation = validate_facts_internal_location(nginx_config, redirect_prefix);
|
|
144
|
+
if (!validation.ok) {
|
|
145
|
+
throw new XAccelConfigError(validation.errors);
|
|
146
|
+
}
|
|
147
|
+
return XAccelConfig.parse({ redirect_prefix });
|
|
148
|
+
};
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -1347,8 +1347,9 @@ the same reasons the sibling `cross_backend/origin.ts` suite is imperative.
|
|
|
1347
1347
|
Both backends read the **same** committed
|
|
1348
1348
|
`testing/cross_backend/expected_schema.json` (column-presence is engine-portable,
|
|
1349
1349
|
so one fixture is the cross-impl contract): the TS spine via
|
|
1350
|
-
`create_spine_ready_route_spec
|
|
1351
|
-
|
|
1350
|
+
`create_spine_ready_route_spec`, which reads the `SPINE_EXPECTED_SCHEMA_URL`
|
|
1351
|
+
`import.meta.url` constant off `spine_surface_constants.ts`; the Rust
|
|
1352
|
+
`testing_spine_stub` via the absolute path `rust_spine_stub_backend_config`
|
|
1352
1353
|
passes through `FUZ_RUST_SPINE_STUB_EXPECTED_SCHEMA_PATH`. The fixture covers the full
|
|
1353
1354
|
spine bootstrap (auth + cell + cell_history + fact) and is regenerated +
|
|
1354
1355
|
drift-guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`
|
|
@@ -22,7 +22,7 @@ import { AccountDeleteOutput, AccountUndeleteOutput, AccountPurgeOutput, AdminAc
|
|
|
22
22
|
import { ERROR_ACCOUNT_NOT_FOUND, ERROR_AUTHENTICATION_REQUIRED } from '../../http/error_schemas.js';
|
|
23
23
|
import { test_if } from './capabilities.js';
|
|
24
24
|
import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
|
|
25
|
-
import { SPINE_RPC_PATH } from './
|
|
25
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
26
26
|
export const describe_account_lifecycle_cross_tests = (options) => {
|
|
27
27
|
const { setup_test, capabilities } = options;
|
|
28
28
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
@@ -32,7 +32,7 @@ import '../assert_dev_env.js';
|
|
|
32
32
|
*/
|
|
33
33
|
import { describe, test, assert } from 'vitest';
|
|
34
34
|
import { actor_lookup_action_spec } from '../../auth/actor_lookup_action_specs.js';
|
|
35
|
-
import { SPINE_RPC_PATH } from './
|
|
35
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
36
36
|
/** Keys that must never appear on an `actor_lookup` result row. */
|
|
37
37
|
const forbidden_row_keys = ['account_id', 'email', 'created_at', 'updated_at', 'role'];
|
|
38
38
|
/** Build the JSON-RPC envelope body for an `actor_lookup` call. */
|
|
@@ -35,7 +35,7 @@ import '../assert_dev_env.js';
|
|
|
35
35
|
*/
|
|
36
36
|
import { describe, test, assert } from 'vitest';
|
|
37
37
|
import { actor_search_action_spec, ERROR_ACTOR_SEARCH_SCOPE_REQUIRED, } from '../../auth/actor_search_action_specs.js';
|
|
38
|
-
import { SPINE_RPC_PATH } from './
|
|
38
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
39
39
|
/** Nil UUID — an unheld scope id (no actor holds a role_grant on it). */
|
|
40
40
|
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
41
41
|
/** Build the JSON-RPC envelope body for an `actor_search` call. */
|
|
@@ -33,7 +33,7 @@ import '../assert_dev_env.js';
|
|
|
33
33
|
import { describe, test, assert } from 'vitest';
|
|
34
34
|
import { app_settings_update_action_spec } from '../../auth/admin_action_specs.js';
|
|
35
35
|
import { ERROR_NO_MATCHING_INVITE } from '../../http/error_schemas.js';
|
|
36
|
-
import { SPINE_RPC_PATH } from './
|
|
36
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
37
37
|
/** REST signup path on the spine surface (`/api/account` prefix + `/signup`). */
|
|
38
38
|
const SIGNUP_PATH = '/api/account/signup';
|
|
39
39
|
/** A password that satisfies the creation-strength schema (min 12). */
|
|
@@ -55,7 +55,7 @@ import '../assert_dev_env.js';
|
|
|
55
55
|
import { describe, test, assert } from 'vitest';
|
|
56
56
|
import { account_verify_action_spec } from '../../auth/account_action_specs.js';
|
|
57
57
|
import { ERROR_PAYLOAD_TOO_LARGE } from '../../http/error_schemas.js';
|
|
58
|
-
import { SPINE_RPC_PATH } from './
|
|
58
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
59
59
|
/**
|
|
60
60
|
* The shared 1 MiB default both impls ship (`DEFAULT_MAX_BODY_SIZE` in TS,
|
|
61
61
|
* `DEFAULT_BODY_LIMIT_BYTES` in `fuz_http`). Kept as a local cross-impl
|
|
@@ -50,7 +50,7 @@ import '../assert_dev_env.js';
|
|
|
50
50
|
*/
|
|
51
51
|
import { connect } from 'node:net';
|
|
52
52
|
import { describe, test, assert } from 'vitest';
|
|
53
|
-
import { SPINE_RPC_PATH } from './
|
|
53
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
54
54
|
/** The shared 1 MiB cap (see `body_size.ts` for why it's a local constant). */
|
|
55
55
|
const BODY_LIMIT_DEFAULT_BYTES = 1024 * 1024;
|
|
56
56
|
/**
|
|
@@ -43,7 +43,7 @@ import { describe, assert } from 'vitest';
|
|
|
43
43
|
import { CellCreateOutput, CellDeleteOutput, CellGetOutput, CellListOutput, CellUpdateOutput, } from '../../auth/cell_action_specs.js';
|
|
44
44
|
import { test_if } from './capabilities.js';
|
|
45
45
|
import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
|
|
46
|
-
import { SPINE_RPC_PATH } from './
|
|
46
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
47
47
|
export const describe_cell_crud_cross_tests = (options) => {
|
|
48
48
|
const { setup_test, capabilities } = options;
|
|
49
49
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
@@ -38,7 +38,7 @@ import { CellCreateOutput, CellGetOutput, CellUpdateOutput } from '../../auth/ce
|
|
|
38
38
|
import { CellGrantCreateOutput, ERROR_CELL_GRANT_UNKNOWN_ROLE, } from '../../auth/cell_grant_action_specs.js';
|
|
39
39
|
import { test_if } from './capabilities.js';
|
|
40
40
|
import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
|
|
41
|
-
import { SPINE_CELL_EDITOR_ROLE, SPINE_RPC_PATH } from './
|
|
41
|
+
import { SPINE_CELL_EDITOR_ROLE, SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
42
42
|
/** App role the holder is seeded with; matches the spine's registered role. */
|
|
43
43
|
export const CELL_EDITOR_ROLE = SPINE_CELL_EDITOR_ROLE;
|
|
44
44
|
/** Username the fixture seeds (via `extra_accounts`) holding `CELL_EDITOR_ROLE`. */
|
|
@@ -61,7 +61,7 @@ import { CellItemDeleteOutput, CellItemInsertOutput, CellItemListOutput, CellIte
|
|
|
61
61
|
import { CellAuditListOutput } from '../../auth/cell_audit_action_specs.js';
|
|
62
62
|
import { test_if } from './capabilities.js';
|
|
63
63
|
import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
|
|
64
|
-
import { SPINE_RPC_PATH } from './
|
|
64
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
65
65
|
/** Create a cell over the wire and return its id (the parity gate parses the output). */
|
|
66
66
|
const create_cell = async (t, rpc_path, h, params) => expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', params, h), CellCreateOutput).cell
|
|
67
67
|
.id;
|
|
@@ -51,7 +51,7 @@ import '../assert_dev_env.js';
|
|
|
51
51
|
import { connect } from 'node:net';
|
|
52
52
|
import { describe, test, assert } from 'vitest';
|
|
53
53
|
import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
|
|
54
|
-
import { SPINE_RPC_PATH } from './
|
|
54
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
55
55
|
/** A well-formed UUID that never names a real row. */
|
|
56
56
|
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
57
57
|
/** A value that is never a real credential — used for the conflicting / garbage copies. */
|
|
@@ -35,14 +35,6 @@ import type { AppServerContext } from '../../server/app_server_context.js';
|
|
|
35
35
|
* (`fuz_session`) so cookie-attribute assertions + jar extraction line up.
|
|
36
36
|
*/
|
|
37
37
|
export declare const spine_session_options: SessionOptions<string>;
|
|
38
|
-
/**
|
|
39
|
-
* App role the role-shaped-`cell_grant` cross suite exercises. Registered
|
|
40
|
-
* with no grant path (`grant_paths: []`) so it stays a valid registry member
|
|
41
|
-
* without entering the admin / self-service grant flows — holders are seeded
|
|
42
|
-
* directly via `extra_accounts`. Must match the `cell_editor` entry in the
|
|
43
|
-
* Rust `testing_spine_stub`'s `known_roles` (cross-language test contract).
|
|
44
|
-
*/
|
|
45
|
-
export declare const SPINE_CELL_EDITOR_ROLE = "cell_editor";
|
|
46
38
|
/**
|
|
47
39
|
* The spine's closed role registry: built-ins plus `SPINE_CELL_EDITOR_ROLE`.
|
|
48
40
|
* Threaded into the cell spec set's role-validity gate; the Rust stub mirrors
|
|
@@ -51,16 +43,6 @@ export declare const SPINE_CELL_EDITOR_ROLE = "cell_editor";
|
|
|
51
43
|
* picks up grant-path coverage.
|
|
52
44
|
*/
|
|
53
45
|
export declare const spine_roles: RoleSchemaResult;
|
|
54
|
-
/** RPC endpoint mount path — matches the binary's `/api/rpc`. */
|
|
55
|
-
export declare const SPINE_RPC_PATH = "/api/rpc";
|
|
56
|
-
/**
|
|
57
|
-
* Audit-log SSE stream path — `/api/admin` prefix + the
|
|
58
|
-
* `create_audit_log_route_specs` `/audit/stream` route. Matches the default
|
|
59
|
-
* `BackendConfig.sse_path` and the cross-process SSE suite's default. Only
|
|
60
|
-
* mounted by the TS spine binary (which wires `audit_log_sse`); the shared
|
|
61
|
-
* surface stub leaves `ctx.audit_sse` null so the snapshot stays SSE-free.
|
|
62
|
-
*/
|
|
63
|
-
export declare const SPINE_SSE_PATH = "/api/admin/audit/stream";
|
|
64
46
|
/** Options for {@link spine_rpc_endpoints}. */
|
|
65
47
|
export interface SpineRpcEndpointsOptions {
|
|
66
48
|
/**
|
|
@@ -109,18 +91,6 @@ export declare const spine_rpc_endpoints: (ctx: AppServerContext, options?: Spin
|
|
|
109
91
|
* the stream at `SPINE_SSE_PATH`.
|
|
110
92
|
*/
|
|
111
93
|
export declare const create_spine_route_specs: (ctx: AppServerContext) => Array<RouteSpec>;
|
|
112
|
-
/**
|
|
113
|
-
* Committed expected-schema fixture for the spine `/ready` deploy gate — the
|
|
114
|
-
* column map a fresh full spine bootstrap (auth + cell + cell_history + fact)
|
|
115
|
-
* produces. Resolved relative to this module so the spawned TS binary (which
|
|
116
|
-
* imports this source under its loader) reads it off disk via `node:fs`.
|
|
117
|
-
* Regenerated + guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`.
|
|
118
|
-
*
|
|
119
|
-
* The Rust `testing_spine_stub` reads the **same** committed file (its absolute
|
|
120
|
-
* path passed via env by `rust_spine_stub_backend_config`) — column-presence is
|
|
121
|
-
* engine-portable, so one fixture is the cross-impl contract.
|
|
122
|
-
*/
|
|
123
|
-
export declare const SPINE_EXPECTED_SCHEMA_URL: URL;
|
|
124
94
|
/**
|
|
125
95
|
* The spine's `/ready` route spec — the column-presence schema-drift deploy
|
|
126
96
|
* gate, reading {@link SPINE_EXPECTED_SCHEMA_URL}. Mounted **live** by the TS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default_spine_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_spine_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAIpD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAqB,KAAK,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAIxF,OAAO,EAAqB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAC,cAAc,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"default_spine_surface.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_spine_surface.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAIpD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AACrF,OAAO,EAAqB,KAAK,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAwB,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAIxF,OAAO,EAAqB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAC,cAAc,EAAE,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAczE;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAAc,CAAC,MAAM,CAAwC,CAAC;AAElG;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,gBAExB,CAAC;AAEH,+CAA+C;AAC/C,MAAM,WAAW,wBAAwB;IACxC;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,GAC/B,KAAK,gBAAgB,EACrB,UAAU,wBAAwB,KAChC,KAAK,CAAC,eAAe,CAQvB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,gBAAgB,KAAG,KAAK,CAAC,SAAS,CA+B/E,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,KAAG,SAC6B,CAAC;AAE3F;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,QAAO,cAM1C,CAAC"}
|
|
@@ -8,19 +8,17 @@ import { create_standard_rpc_actions } from '../../auth/standard_rpc_actions.js'
|
|
|
8
8
|
import { create_ready_route_spec, load_expected_schema } from '../../http/common_routes.js';
|
|
9
9
|
import { prefix_route_specs } from '../../http/route_spec.js';
|
|
10
10
|
import { create_test_app_surface_spec } from '../stubs.js';
|
|
11
|
+
// Pure path / role / fixture-URL constants live on the hono-free
|
|
12
|
+
// `spine_surface_constants.ts` leaf so cross-process suite modules can import them
|
|
13
|
+
// without dragging this module's eager in-process route handlers
|
|
14
|
+
// (→ `session_middleware` → `hono/cookie`). Import only the constants this
|
|
15
|
+
// module uses internally; callers that need a bare constant reach for the leaf.
|
|
16
|
+
import { SPINE_CELL_EDITOR_ROLE, SPINE_EXPECTED_SCHEMA_URL, SPINE_RPC_PATH, } from './spine_surface_constants.js';
|
|
11
17
|
/**
|
|
12
18
|
* Session config — cookie name matches the binary's issued session cookie
|
|
13
19
|
* (`fuz_session`) so cookie-attribute assertions + jar extraction line up.
|
|
14
20
|
*/
|
|
15
21
|
export const spine_session_options = create_session_config('fuz_session');
|
|
16
|
-
/**
|
|
17
|
-
* App role the role-shaped-`cell_grant` cross suite exercises. Registered
|
|
18
|
-
* with no grant path (`grant_paths: []`) so it stays a valid registry member
|
|
19
|
-
* without entering the admin / self-service grant flows — holders are seeded
|
|
20
|
-
* directly via `extra_accounts`. Must match the `cell_editor` entry in the
|
|
21
|
-
* Rust `testing_spine_stub`'s `known_roles` (cross-language test contract).
|
|
22
|
-
*/
|
|
23
|
-
export const SPINE_CELL_EDITOR_ROLE = 'cell_editor';
|
|
24
22
|
/**
|
|
25
23
|
* The spine's closed role registry: built-ins plus `SPINE_CELL_EDITOR_ROLE`.
|
|
26
24
|
* Threaded into the cell spec set's role-validity gate; the Rust stub mirrors
|
|
@@ -31,16 +29,6 @@ export const SPINE_CELL_EDITOR_ROLE = 'cell_editor';
|
|
|
31
29
|
export const spine_roles = create_role_schema([
|
|
32
30
|
{ name: SPINE_CELL_EDITOR_ROLE, grant_paths: [] },
|
|
33
31
|
]);
|
|
34
|
-
/** RPC endpoint mount path — matches the binary's `/api/rpc`. */
|
|
35
|
-
export const SPINE_RPC_PATH = '/api/rpc';
|
|
36
|
-
/**
|
|
37
|
-
* Audit-log SSE stream path — `/api/admin` prefix + the
|
|
38
|
-
* `create_audit_log_route_specs` `/audit/stream` route. Matches the default
|
|
39
|
-
* `BackendConfig.sse_path` and the cross-process SSE suite's default. Only
|
|
40
|
-
* mounted by the TS spine binary (which wires `audit_log_sse`); the shared
|
|
41
|
-
* surface stub leaves `ctx.audit_sse` null so the snapshot stays SSE-free.
|
|
42
|
-
*/
|
|
43
|
-
export const SPINE_SSE_PATH = '/api/admin/audit/stream';
|
|
44
32
|
/**
|
|
45
33
|
* Factory-form RPC endpoints over the per-test `ctx.deps`. `create_app_server`
|
|
46
34
|
* (in the binary) owns live dispatch; the surface builder invokes the factory
|
|
@@ -105,18 +93,6 @@ export const create_spine_route_specs = (ctx) => [
|
|
|
105
93
|
? prefix_route_specs('/api/admin', create_audit_log_route_specs({ stream: ctx.audit_sse }))
|
|
106
94
|
: []),
|
|
107
95
|
];
|
|
108
|
-
/**
|
|
109
|
-
* Committed expected-schema fixture for the spine `/ready` deploy gate — the
|
|
110
|
-
* column map a fresh full spine bootstrap (auth + cell + cell_history + fact)
|
|
111
|
-
* produces. Resolved relative to this module so the spawned TS binary (which
|
|
112
|
-
* imports this source under its loader) reads it off disk via `node:fs`.
|
|
113
|
-
* Regenerated + guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`.
|
|
114
|
-
*
|
|
115
|
-
* The Rust `testing_spine_stub` reads the **same** committed file (its absolute
|
|
116
|
-
* path passed via env by `rust_spine_stub_backend_config`) — column-presence is
|
|
117
|
-
* engine-portable, so one fixture is the cross-impl contract.
|
|
118
|
-
*/
|
|
119
|
-
export const SPINE_EXPECTED_SCHEMA_URL = new URL('./expected_schema.json', import.meta.url);
|
|
120
96
|
/**
|
|
121
97
|
* The spine's `/ready` route spec — the column-presence schema-drift deploy
|
|
122
98
|
* gate, reading {@link SPINE_EXPECTED_SCHEMA_URL}. Mounted **live** by the TS
|
|
@@ -42,7 +42,7 @@ import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
|
42
42
|
import { CellCreateOutput } from '../../auth/cell_action_specs.js';
|
|
43
43
|
import { test_if } from './capabilities.js';
|
|
44
44
|
import { cross_rpc_call, expect_output } from './cell_cross_helpers.js';
|
|
45
|
-
import { SPINE_RPC_PATH } from './
|
|
45
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
46
46
|
/** A blake3 hash that is a valid form but references no seeded fact. */
|
|
47
47
|
const UNRELATED_HASH = `blake3:${'0'.repeat(64)}`;
|
|
48
48
|
/** Nil UUID — a valid `Uuid` param that resolves to no cell. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"full_spine_mount.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/full_spine_mount.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAE3D,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AAKrF,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"full_spine_mount.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/full_spine_mount.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAE3D,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AAKrF,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAQzE,2FAA2F;AAC3F,MAAM,WAAW,qBAAqB;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CA0BjB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,GACpC,KAAK,gBAAgB,EACrB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,eAAe,CAEvB,CAAC"}
|
|
@@ -5,7 +5,8 @@ import { create_all_cell_actions } from '../../auth/all_cell_actions.js';
|
|
|
5
5
|
import { create_actor_lookup_actions } from '../../auth/actor_lookup_actions.js';
|
|
6
6
|
import { create_actor_search_actions } from '../../auth/actor_search_actions.js';
|
|
7
7
|
import { create_testing_action_manifest_action, create_testing_actions, } from './testing_reset_actions.js';
|
|
8
|
-
import {
|
|
8
|
+
import { spine_roles, spine_session_options } from './default_spine_surface.js';
|
|
9
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
9
10
|
/**
|
|
10
11
|
* Build the complete live RPC action list the spine test binary mounts on
|
|
11
12
|
* its single endpoint: the declared `create_standard_rpc_actions` bundle plus
|
|
@@ -29,7 +29,7 @@ import '../assert_dev_env.js';
|
|
|
29
29
|
import { describe, test, assert } from 'vitest';
|
|
30
30
|
import { account_verify_action_spec } from '../../auth/account_action_specs.js';
|
|
31
31
|
import { ERROR_FORBIDDEN_ORIGIN } from '../../http/error_schemas.js';
|
|
32
|
-
import { SPINE_RPC_PATH } from './
|
|
32
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
33
33
|
/** Build the JSON-RPC envelope body for a nullary `account_verify` call. */
|
|
34
34
|
const verify_envelope = (id) => JSON.stringify({ jsonrpc: '2.0', method: account_verify_action_spec.method, id });
|
|
35
35
|
export const describe_origin_cross_tests = (options) => {
|
|
@@ -46,7 +46,7 @@ import { describe, test, assert } from 'vitest';
|
|
|
46
46
|
import { rpc_call } from '../rpc_helpers.js';
|
|
47
47
|
import { ROLE_ADMIN } from '../../auth/role_schema.js';
|
|
48
48
|
import { ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH, ERROR_ROLE_GRANT_OFFER_NOT_FOUND, } from '../../auth/role_grant_offer_action_specs.js';
|
|
49
|
-
import { SPINE_RPC_PATH } from './
|
|
49
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
50
50
|
/** A well-formed UUID that never names a real offer — exercises the not-found arm. */
|
|
51
51
|
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
52
52
|
/**
|
|
@@ -35,7 +35,7 @@ import '../assert_dev_env.js';
|
|
|
35
35
|
*/
|
|
36
36
|
import { fileURLToPath } from 'node:url';
|
|
37
37
|
import { build_test_backend_paths } from './build_test_backend_paths.js';
|
|
38
|
-
import { SPINE_EXPECTED_SCHEMA_URL } from './
|
|
38
|
+
import { SPINE_EXPECTED_SCHEMA_URL } from './spine_surface_constants.js';
|
|
39
39
|
import { LOGIN_RATE_LIMIT_ENABLED_ENV, make_default_rust_backend_config, rust_default_capabilities, } from './default_backend_configs.js';
|
|
40
40
|
/** Env var naming the prebuilt `testing_spine_stub` binary. Required when `binary_path` is omitted. */
|
|
41
41
|
export const RUST_SPINE_STUB_BIN_ENV = 'FUZ_TESTING_RUST_SPINE_STUB_BIN';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure spine-surface path + role constants — the hono-free leaf split out of
|
|
4
|
+
* `default_spine_surface.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Cross-process suite modules (which drive a separately-spawned backend binary
|
|
7
|
+
* over HTTP) need only the wire path / role / fixture-URL, not the in-process
|
|
8
|
+
* route handlers. Importing them from `default_spine_surface.ts` used to drag
|
|
9
|
+
* its eager `account_routes.ts` / `signup_routes.ts` imports — and through them
|
|
10
|
+
* `session_middleware` → `hono/cookie` — onto a backend-spawning consumer with
|
|
11
|
+
* no `hono` peer installed (a Rust-only spine consumer). Keeping these constants
|
|
12
|
+
* on this handler-free leaf lets such a consumer import the path without the
|
|
13
|
+
* peer. `default_spine_surface.ts` re-exports them for in-process callers.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/** RPC endpoint mount path — matches the binary's `/api/rpc`. */
|
|
18
|
+
export declare const SPINE_RPC_PATH = "/api/rpc";
|
|
19
|
+
/**
|
|
20
|
+
* Audit-log SSE stream path — `/api/admin` prefix + the
|
|
21
|
+
* `create_audit_log_route_specs` `/audit/stream` route. Matches the default
|
|
22
|
+
* `BackendConfig.sse_path` and the cross-process SSE suite's default. Only
|
|
23
|
+
* mounted by the TS spine binary (which wires `audit_log_sse`); the shared
|
|
24
|
+
* surface stub leaves `ctx.audit_sse` null so the snapshot stays SSE-free.
|
|
25
|
+
*/
|
|
26
|
+
export declare const SPINE_SSE_PATH = "/api/admin/audit/stream";
|
|
27
|
+
/**
|
|
28
|
+
* App role the role-shaped-`cell_grant` cross suite exercises. Registered
|
|
29
|
+
* with no grant path (`grant_paths: []`) so it stays a valid registry member
|
|
30
|
+
* without entering the admin / self-service grant flows — holders are seeded
|
|
31
|
+
* directly via `extra_accounts`. Must match the `cell_editor` entry in the
|
|
32
|
+
* Rust `testing_spine_stub`'s `known_roles` (cross-language test contract).
|
|
33
|
+
*/
|
|
34
|
+
export declare const SPINE_CELL_EDITOR_ROLE = "cell_editor";
|
|
35
|
+
/**
|
|
36
|
+
* Committed expected-schema fixture for the spine `/ready` deploy gate — the
|
|
37
|
+
* column map a fresh full spine bootstrap (auth + cell + cell_history + fact)
|
|
38
|
+
* produces. Resolved relative to this module so the spawned TS binary (which
|
|
39
|
+
* imports this source under its loader) reads it off disk via `node:fs`.
|
|
40
|
+
* Regenerated + guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`.
|
|
41
|
+
*
|
|
42
|
+
* The Rust `testing_spine_stub` reads the **same** committed file (its absolute
|
|
43
|
+
* path passed via env by `rust_spine_stub_backend_config`) — column-presence is
|
|
44
|
+
* engine-portable, so one fixture is the cross-impl contract.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SPINE_EXPECTED_SCHEMA_URL: URL;
|
|
47
|
+
//# sourceMappingURL=spine_surface_constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spine_surface_constants.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spine_surface_constants.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;GAcG;AAEH,iEAAiE;AACjE,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,4BAA4B,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,EAAE,GAAwD,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure spine-surface path + role constants — the hono-free leaf split out of
|
|
4
|
+
* `default_spine_surface.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Cross-process suite modules (which drive a separately-spawned backend binary
|
|
7
|
+
* over HTTP) need only the wire path / role / fixture-URL, not the in-process
|
|
8
|
+
* route handlers. Importing them from `default_spine_surface.ts` used to drag
|
|
9
|
+
* its eager `account_routes.ts` / `signup_routes.ts` imports — and through them
|
|
10
|
+
* `session_middleware` → `hono/cookie` — onto a backend-spawning consumer with
|
|
11
|
+
* no `hono` peer installed (a Rust-only spine consumer). Keeping these constants
|
|
12
|
+
* on this handler-free leaf lets such a consumer import the path without the
|
|
13
|
+
* peer. `default_spine_surface.ts` re-exports them for in-process callers.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/** RPC endpoint mount path — matches the binary's `/api/rpc`. */
|
|
18
|
+
export const SPINE_RPC_PATH = '/api/rpc';
|
|
19
|
+
/**
|
|
20
|
+
* Audit-log SSE stream path — `/api/admin` prefix + the
|
|
21
|
+
* `create_audit_log_route_specs` `/audit/stream` route. Matches the default
|
|
22
|
+
* `BackendConfig.sse_path` and the cross-process SSE suite's default. Only
|
|
23
|
+
* mounted by the TS spine binary (which wires `audit_log_sse`); the shared
|
|
24
|
+
* surface stub leaves `ctx.audit_sse` null so the snapshot stays SSE-free.
|
|
25
|
+
*/
|
|
26
|
+
export const SPINE_SSE_PATH = '/api/admin/audit/stream';
|
|
27
|
+
/**
|
|
28
|
+
* App role the role-shaped-`cell_grant` cross suite exercises. Registered
|
|
29
|
+
* with no grant path (`grant_paths: []`) so it stays a valid registry member
|
|
30
|
+
* without entering the admin / self-service grant flows — holders are seeded
|
|
31
|
+
* directly via `extra_accounts`. Must match the `cell_editor` entry in the
|
|
32
|
+
* Rust `testing_spine_stub`'s `known_roles` (cross-language test contract).
|
|
33
|
+
*/
|
|
34
|
+
export const SPINE_CELL_EDITOR_ROLE = 'cell_editor';
|
|
35
|
+
/**
|
|
36
|
+
* Committed expected-schema fixture for the spine `/ready` deploy gate — the
|
|
37
|
+
* column map a fresh full spine bootstrap (auth + cell + cell_history + fact)
|
|
38
|
+
* produces. Resolved relative to this module so the spawned TS binary (which
|
|
39
|
+
* imports this source under its loader) reads it off disk via `node:fs`.
|
|
40
|
+
* Regenerated + guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`.
|
|
41
|
+
*
|
|
42
|
+
* The Rust `testing_spine_stub` reads the **same** committed file (its absolute
|
|
43
|
+
* path passed via env by `rust_spine_stub_backend_config`) — column-presence is
|
|
44
|
+
* engine-portable, so one fixture is the cross-impl contract.
|
|
45
|
+
*/
|
|
46
|
+
export const SPINE_EXPECTED_SCHEMA_URL = new URL('./expected_schema.json', import.meta.url);
|
|
@@ -53,7 +53,7 @@ import '../assert_dev_env.js';
|
|
|
53
53
|
import { describe, test, assert } from 'vitest';
|
|
54
54
|
import { ERROR_CREDENTIAL_TYPE_REQUIRED } from '../../http/error_schemas.js';
|
|
55
55
|
import { rpc_call } from '../rpc_helpers.js';
|
|
56
|
-
import { SPINE_RPC_PATH } from './
|
|
56
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
57
57
|
/** A well-formed UUID that never names a real row. */
|
|
58
58
|
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
59
59
|
/**
|
|
@@ -19,17 +19,6 @@ import '../assert_dev_env.js';
|
|
|
19
19
|
import type { BackendConfig } from './backend_config.js';
|
|
20
20
|
/** Env var naming the backend root dir; `{dir}/run/daemon_token` must match `bootstrap.daemon_token_path`. */
|
|
21
21
|
export declare const TS_SPINE_DIR_ENV = "FUZ_TESTING_TS_SPINE_DIR";
|
|
22
|
-
/**
|
|
23
|
-
* Audit-log SSE stream path the TS spine binary serves (it wires
|
|
24
|
-
* `audit_log_sse`). Matches `SPINE_SSE_PATH` in `testing/cross_backend/default_spine_surface.ts`
|
|
25
|
-
* and the cross-process SSE suite's default. Scoped to the TS configs
|
|
26
|
-
* (which advertise `capabilities.sse: true`) — the shared
|
|
27
|
-
* `ts_default_capabilities` stays `sse: false` because not every consumer
|
|
28
|
-
* wires `audit_log_sse`, so the default stays honest for backends that don't
|
|
29
|
-
* serve the stream. (The Rust `testing_spine_stub` does serve it and opts in
|
|
30
|
-
* via `rust_spine_stub_backend_config`.)
|
|
31
|
-
*/
|
|
32
|
-
export declare const TS_SPINE_SSE_PATH = "/api/admin/audit/stream";
|
|
33
22
|
/** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
|
|
34
23
|
export declare const TS_SPINE_NODE_DEFAULT_PORT = 1178;
|
|
35
24
|
/** Default port for the Deno TS spine binary. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AASvD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AA+B3D,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAC3C;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgCF,CAAC"}
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
2
|
import { build_test_backend_paths } from './build_test_backend_paths.js';
|
|
3
3
|
import { LOGIN_RATE_LIMIT_ENABLED_ENV, make_default_ts_backend_config, ts_default_capabilities, } from './default_backend_configs.js';
|
|
4
|
+
import { SPINE_SSE_PATH } from './spine_surface_constants.js';
|
|
4
5
|
/** Env var naming the backend root dir; `{dir}/run/daemon_token` must match `bootstrap.daemon_token_path`. */
|
|
5
6
|
export const TS_SPINE_DIR_ENV = 'FUZ_TESTING_TS_SPINE_DIR';
|
|
6
|
-
/**
|
|
7
|
-
* Audit-log SSE stream path the TS spine binary serves (it wires
|
|
8
|
-
* `audit_log_sse`). Matches `SPINE_SSE_PATH` in `testing/cross_backend/default_spine_surface.ts`
|
|
9
|
-
* and the cross-process SSE suite's default. Scoped to the TS configs
|
|
10
|
-
* (which advertise `capabilities.sse: true`) — the shared
|
|
11
|
-
* `ts_default_capabilities` stays `sse: false` because not every consumer
|
|
12
|
-
* wires `audit_log_sse`, so the default stays honest for backends that don't
|
|
13
|
-
* serve the stream. (The Rust `testing_spine_stub` does serve it and opts in
|
|
14
|
-
* via `rust_spine_stub_backend_config`.)
|
|
15
|
-
*/
|
|
16
|
-
export const TS_SPINE_SSE_PATH = '/api/admin/audit/stream';
|
|
17
7
|
/**
|
|
18
8
|
* Capabilities for the TS spine binary — `ts_default_capabilities` plus `sse`
|
|
19
9
|
* (the binary wires `audit_log_sse`), `ready` (the binary live-mounts the
|
|
@@ -81,7 +71,7 @@ export const ts_spine_node_backend_config = (options = {}) => {
|
|
|
81
71
|
},
|
|
82
72
|
capabilities: ts_spine_capabilities,
|
|
83
73
|
}),
|
|
84
|
-
sse_path:
|
|
74
|
+
sse_path: SPINE_SSE_PATH,
|
|
85
75
|
};
|
|
86
76
|
};
|
|
87
77
|
/**
|
|
@@ -107,7 +97,7 @@ export const ts_spine_bun_backend_config = (options = {}) => {
|
|
|
107
97
|
},
|
|
108
98
|
capabilities: ts_spine_bun_capabilities,
|
|
109
99
|
}),
|
|
110
|
-
sse_path:
|
|
100
|
+
sse_path: SPINE_SSE_PATH,
|
|
111
101
|
};
|
|
112
102
|
};
|
|
113
103
|
/**
|
|
@@ -151,6 +141,6 @@ export const ts_spine_deno_backend_config = (options = {}) => {
|
|
|
151
141
|
},
|
|
152
142
|
capabilities: ts_spine_capabilities,
|
|
153
143
|
}),
|
|
154
|
-
sse_path:
|
|
144
|
+
sse_path: SPINE_SSE_PATH,
|
|
155
145
|
};
|
|
156
146
|
};
|