@fuzdev/fuz_app 0.139.0 → 0.141.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/actions/CLAUDE.md +62 -17
- package/dist/actions/action_rpc.d.ts +5 -1
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +9 -1
- package/dist/actions/connection_closer.d.ts +25 -2
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +8 -1
- package/dist/actions/register_action_ws.d.ts +42 -6
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +87 -19
- package/dist/actions/register_ws_endpoint.d.ts +15 -6
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -2
- package/dist/actions/socket.svelte.d.ts +19 -4
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +31 -4
- package/dist/actions/transports.d.ts +30 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +30 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +24 -10
- package/dist/actions/transports_ws_backend.d.ts +51 -5
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +62 -9
- package/dist/actions/ws_endpoint_spec.d.ts +11 -10
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +11 -9
- package/dist/auth/account_route_schema.d.ts +1 -0
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +4 -0
- package/dist/auth/api_token.d.ts +20 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +21 -1
- package/dist/auth/api_token_queries.d.ts +22 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +11 -6
- package/dist/auth/audit_log_routes.d.ts +10 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +20 -0
- package/dist/auth/audit_log_schema.d.ts +30 -5
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +30 -0
- package/dist/auth/bearer_auth.d.ts +4 -2
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +19 -8
- package/dist/auth/bootstrap_route_schema.d.ts +1 -0
- package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -0
- package/dist/auth/cleanup.d.ts +9 -9
- package/dist/auth/cleanup.js +9 -9
- package/dist/auth/credential_deadline.d.ts +95 -0
- package/dist/auth/credential_deadline.d.ts.map +1 -0
- package/dist/auth/credential_deadline.js +139 -0
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +4 -1
- package/dist/auth/middleware.d.ts +90 -12
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +115 -16
- package/dist/auth/request_context.d.ts +4 -2
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +14 -3
- package/dist/auth/resolved_auth.d.ts +13 -2
- package/dist/auth/resolved_auth.d.ts.map +1 -1
- package/dist/auth/resolved_auth.js +5 -4
- package/dist/auth/session_cookie.d.ts +4 -0
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +3 -2
- 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 +4 -1
- package/dist/auth/session_queries.d.ts +19 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +8 -2
- package/dist/auth/signup_route_schema.d.ts.map +1 -1
- package/dist/auth/signup_route_schema.js +1 -0
- package/dist/hono_context.d.ts +23 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +19 -0
- package/dist/http/CLAUDE.md +41 -15
- package/dist/http/middleware_spec.d.ts +16 -0
- package/dist/http/middleware_spec.d.ts.map +1 -1
- package/dist/http/origin.d.ts +113 -18
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +247 -127
- package/dist/http/route_spec.d.ts +21 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/schema_helpers.d.ts +22 -0
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +24 -0
- package/dist/http/surface.d.ts +6 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +12 -5
- package/dist/realtime/sse_auth_guard.d.ts +22 -6
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +31 -12
- package/dist/realtime/subscriber_registry.d.ts +10 -1
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +10 -1
- package/dist/server/app_server.d.ts +20 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +8 -3
- package/dist/server/env.d.ts +2 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +7 -17
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +1 -0
- package/dist/testing/CLAUDE.md +154 -22
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -6
- package/dist/testing/connection_closer_helpers.d.ts +6 -1
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +4 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +14 -65
- package/dist/testing/cross_backend/capabilities.d.ts +26 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +9 -3
- package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +6 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +20 -19
- package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
- package/dist/testing/cross_backend/credential_deadline.js +185 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +13 -2
- package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +7 -1
- package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
- package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
- package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +6 -26
- package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
- package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_conformance.js +136 -0
- package/dist/testing/cross_backend/origin_conformance.json +1679 -0
- package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_scope.js +140 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +19 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +22 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +20 -7
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +25 -21
- package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
- package/dist/testing/cross_backend/transport_composition.js +364 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
- package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_role_gated.js +142 -0
- package/dist/testing/entities.d.ts +23 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +37 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +3 -1
- package/dist/testing/middleware.d.ts +10 -0
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +31 -7
- package/dist/testing/stubs.d.ts +16 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +20 -7
- package/dist/testing/surface_invariants.js +2 -2
- package/package.json +12 -12
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { CapabilityGatedCrossSuiteOptions } from './setup.js';
|
|
3
|
+
/** Options for the origin-scope parity suite. */
|
|
4
|
+
export interface OriginScopeCrossTestOptions extends CapabilityGatedCrossSuiteOptions {
|
|
5
|
+
/** A public path outside `/api` the backend serves with `200`. Default `/ready`. */
|
|
6
|
+
readonly probe_path?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The backend's base URL, for the over-cap row: given one, the request
|
|
9
|
+
* head is written to the socket alone, declaring a length one byte over
|
|
10
|
+
* the cap, so the `413` has to arrive before any body byte. `null` (the
|
|
11
|
+
* default) sends the whole body through the fixture's in-process transport.
|
|
12
|
+
*/
|
|
13
|
+
readonly base_url?: string | null;
|
|
14
|
+
}
|
|
15
|
+
export declare const describe_origin_scope_cross_tests: (options: OriginScopeCrossTestOptions) => void;
|
|
16
|
+
//# sourceMappingURL=origin_scope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origin_scope.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/origin_scope.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4C9B,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,YAAY,CAAC;AAcnE,iDAAiD;AACjD,MAAM,WAAW,2BAA4B,SAAQ,gCAAgC;IACpF,oFAAoF;IACpF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IAmGxF,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for the origin check's scope: every path, not
|
|
4
|
+
* only `/api`.
|
|
5
|
+
*
|
|
6
|
+
* Both spines check the `Origin` of every request — the TS spine mounts the
|
|
7
|
+
* check on every path (`ORIGIN_MIDDLEWARE_PATH`), the Rust spine behind every
|
|
8
|
+
* route group of `fuz_http::compose_stack`, the fallback included — and leave
|
|
9
|
+
* a path out only by a named carve-out with its reason. The front-door table
|
|
10
|
+
* (`transport_composition.ts`) pins the check on the `/api` transports; this
|
|
11
|
+
* suite pins it off `/api`, on the route both spine test servers serve there,
|
|
12
|
+
* `GET /ready`:
|
|
13
|
+
*
|
|
14
|
+
* - **foreign `Origin` → 403 `forbidden_origin`** — the check runs on a path
|
|
15
|
+
* outside `/api`.
|
|
16
|
+
* - **absent `Origin` → passes** — a probe or CLI client sends none, so the
|
|
17
|
+
* wider scope costs it nothing.
|
|
18
|
+
* - **allowed `Origin` → passes** — the control: the check admits its own
|
|
19
|
+
* origin rather than refusing every browser.
|
|
20
|
+
*
|
|
21
|
+
* And on a path neither server routes, which reaches each spine's fallback:
|
|
22
|
+
*
|
|
23
|
+
* - **foreign `Origin` → 403 `forbidden_origin`**, **absent → 404** — the
|
|
24
|
+
* fallback is behind the check, not attached after it.
|
|
25
|
+
* - **an over-cap `POST` with a declared `Content-Length` and a foreign
|
|
26
|
+
* `Origin` → 413 `payload_too_large`** — the body cap comes ahead of the
|
|
27
|
+
* origin check on the fallback too (413-first holds for a declared length).
|
|
28
|
+
*
|
|
29
|
+
* The `/ready` rows are gated on `capabilities.ready`, which every spine test
|
|
30
|
+
* server advertises; the unrouted rows run on every backend.
|
|
31
|
+
* Runs both legs via the shared `{setup_test, capabilities}` protocol: the
|
|
32
|
+
* in-process leg (`cross_backend/origin_scope_parity.db.test.ts`, plain `gro
|
|
33
|
+
* test`) and the cross-process leg (`cross_backend/origin_scope.cross.test.ts`,
|
|
34
|
+
* the TS spine binaries + Rust `testing_spine_stub` over real HTTP).
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
import { describe, assert, test } from 'vitest';
|
|
39
|
+
import { ERROR_FORBIDDEN_ORIGIN, ERROR_PAYLOAD_TOO_LARGE } from '../../http/error_schemas.js';
|
|
40
|
+
import { raw_http_exchange, raw_http_parse_response } from '../transports/raw_http.js';
|
|
41
|
+
import { test_if } from './capabilities.js';
|
|
42
|
+
/** An origin no spine test server allows. */
|
|
43
|
+
const FOREIGN_ORIGIN = 'https://evil.example';
|
|
44
|
+
/** A path no spine test server routes — it reaches the fallback. */
|
|
45
|
+
const UNROUTED_PATH = '/origin-scope-unrouted';
|
|
46
|
+
/** The shared 1 MiB default cap both spines ship, as in `body_size.ts`. */
|
|
47
|
+
const BODY_LIMIT_DEFAULT_BYTES = 1024 * 1024;
|
|
48
|
+
/** How long a raw exchange waits for the refusal. */
|
|
49
|
+
const RAW_READ_TIMEOUT_MS = 2000;
|
|
50
|
+
export const describe_origin_scope_cross_tests = (options) => {
|
|
51
|
+
const { setup_test, capabilities } = options;
|
|
52
|
+
const probe_path = options.probe_path ?? '/ready';
|
|
53
|
+
const base_url = options.base_url ?? null;
|
|
54
|
+
describe('origin check scope parity', () => {
|
|
55
|
+
test_if(capabilities.ready, `a foreign Origin on ${probe_path} → 403 forbidden_origin (the check covers every path)`, async () => {
|
|
56
|
+
const fixture = await setup_test();
|
|
57
|
+
// the header set on the request, not as the transport's default — an
|
|
58
|
+
// in-process transport sends no default Origin
|
|
59
|
+
const res = await fixture.fresh_transport({ origin: null })(probe_path, {
|
|
60
|
+
method: 'GET',
|
|
61
|
+
headers: { origin: FOREIGN_ORIGIN }
|
|
62
|
+
});
|
|
63
|
+
assert.strictEqual(res.status, 403, 'a foreign Origin off /api must be refused');
|
|
64
|
+
const body = (await res.json().catch(() => undefined));
|
|
65
|
+
assert.strictEqual(body?.error, ERROR_FORBIDDEN_ORIGIN);
|
|
66
|
+
});
|
|
67
|
+
test_if(capabilities.ready, `an absent Origin on ${probe_path} → passes`, async () => {
|
|
68
|
+
const fixture = await setup_test();
|
|
69
|
+
const res = await fixture.fresh_transport({ origin: null })(probe_path, { method: 'GET' });
|
|
70
|
+
assert.strictEqual(res.status, 200, 'a request with no Origin must pass the check');
|
|
71
|
+
});
|
|
72
|
+
test_if(capabilities.ready, `the allowed Origin on ${probe_path} → passes`, async () => {
|
|
73
|
+
const fixture = await setup_test();
|
|
74
|
+
// a cross-process transport sends the backend's own, allowed Origin by
|
|
75
|
+
// default (an in-process one sends none, which passes too)
|
|
76
|
+
const res = await fixture.fresh_transport()(probe_path, { method: 'GET' });
|
|
77
|
+
assert.strictEqual(res.status, 200, 'the allowed Origin must pass the check');
|
|
78
|
+
});
|
|
79
|
+
test(`a foreign Origin on an unrouted path → 403 forbidden_origin (the fallback is checked)`, async () => {
|
|
80
|
+
const fixture = await setup_test();
|
|
81
|
+
const res = await fixture.fresh_transport({ origin: null })(UNROUTED_PATH, {
|
|
82
|
+
method: 'GET',
|
|
83
|
+
headers: { origin: FOREIGN_ORIGIN }
|
|
84
|
+
});
|
|
85
|
+
assert.strictEqual(res.status, 403, 'a foreign Origin on the fallback must be refused');
|
|
86
|
+
const body = (await res.json().catch(() => undefined));
|
|
87
|
+
assert.strictEqual(body?.error, ERROR_FORBIDDEN_ORIGIN);
|
|
88
|
+
});
|
|
89
|
+
test('an absent Origin on an unrouted path → 404', async () => {
|
|
90
|
+
const fixture = await setup_test();
|
|
91
|
+
const res = await fixture.fresh_transport({ origin: null })(UNROUTED_PATH, {
|
|
92
|
+
method: 'GET'
|
|
93
|
+
});
|
|
94
|
+
assert.strictEqual(res.status, 404, 'the fallback answers its own 404 past the check');
|
|
95
|
+
});
|
|
96
|
+
test('an over-cap POST to an unrouted path with a foreign Origin → 413 payload_too_large', async () => {
|
|
97
|
+
const over = BODY_LIMIT_DEFAULT_BYTES + 1;
|
|
98
|
+
let status;
|
|
99
|
+
let text;
|
|
100
|
+
if (base_url === null) {
|
|
101
|
+
const fixture = await setup_test();
|
|
102
|
+
const res = await fixture.fresh_transport({ origin: null })(UNROUTED_PATH, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { origin: FOREIGN_ORIGIN, 'content-type': 'application/json' },
|
|
105
|
+
body: 'x'.repeat(over)
|
|
106
|
+
});
|
|
107
|
+
status = res.status;
|
|
108
|
+
text = await res.text();
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// the head alone, declaring the length: the 413 must not wait on a body byte
|
|
112
|
+
const url = new URL(base_url);
|
|
113
|
+
const request = `POST ${UNROUTED_PATH} HTTP/1.1\r\n` +
|
|
114
|
+
`host: ${url.host}\r\n` +
|
|
115
|
+
`origin: ${FOREIGN_ORIGIN}\r\n` +
|
|
116
|
+
`content-type: application/json\r\n` +
|
|
117
|
+
`content-length: ${over}\r\n` +
|
|
118
|
+
`\r\n`;
|
|
119
|
+
const { received } = await raw_http_exchange(base_url, request, {
|
|
120
|
+
read_timeout_ms: RAW_READ_TIMEOUT_MS,
|
|
121
|
+
until: 'response'
|
|
122
|
+
});
|
|
123
|
+
const response = raw_http_parse_response(received);
|
|
124
|
+
assert.ok(response, `no whole response; received: ${received.slice(0, 200)}`);
|
|
125
|
+
status = response.status;
|
|
126
|
+
text = response.body;
|
|
127
|
+
}
|
|
128
|
+
assert.strictEqual(status, 413, `the cap comes ahead of the origin check (body: ${text})`);
|
|
129
|
+
const body = (() => {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(text);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
assert.strictEqual(body?.error, ERROR_PAYLOAD_TOO_LARGE);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { TestAccountFixture, TestFixture } from './setup.js';
|
|
3
|
+
/** Options for the role-grant fixtures. */
|
|
4
|
+
export interface RoleGrantFixtureOptions {
|
|
5
|
+
/** The role to offer. Default `ROLE_ADMIN`, the one builtin admin-grantable role. */
|
|
6
|
+
readonly role?: string;
|
|
7
|
+
/** The JSON-RPC endpoint path. Default `SPINE_RPC_PATH`. */
|
|
8
|
+
readonly rpc_path?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Create a fresh pending offer of `role` from `grantor_headers` (defaults to
|
|
12
|
+
* the keeper) to `to_account_id`, returning the created offer.
|
|
13
|
+
*
|
|
14
|
+
* @param fixture - the per-test fixture
|
|
15
|
+
* @param to_account_id - the recipient account
|
|
16
|
+
* @param grantor_headers - the grantor's session headers; the keeper's when omitted
|
|
17
|
+
* @param options - the role and the RPC path
|
|
18
|
+
* @returns the created offer
|
|
19
|
+
*/
|
|
20
|
+
export declare const create_pending_role_grant_offer: (fixture: TestFixture, to_account_id: string, grantor_headers?: Record<string, string>, options?: RoleGrantFixtureOptions) => Promise<{
|
|
21
|
+
id: string;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* Materialize an active global grant of `role` on `recipient` through a fresh
|
|
25
|
+
* keeper offer and the recipient's accept, returning its `role_grant_id` — the
|
|
26
|
+
* handle `role_grant_revoke` keys on. Idempotent on a grant the account
|
|
27
|
+
* already holds: the accept returns that grant's id.
|
|
28
|
+
*
|
|
29
|
+
* @param fixture - the per-test fixture
|
|
30
|
+
* @param recipient - the account that accepts the offer
|
|
31
|
+
* @param options - the role and the RPC path
|
|
32
|
+
* @returns the active grant's id
|
|
33
|
+
*/
|
|
34
|
+
export declare const create_active_role_grant: (fixture: TestFixture, recipient: TestAccountFixture, options?: RoleGrantFixtureOptions) => Promise<string>;
|
|
35
|
+
//# sourceMappingURL=role_grant_cross_helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role_grant_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoB9B,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAElE,2CAA2C;AAC3C,MAAM,WAAW,uBAAuB;IACvC,qFAAqF;IACrF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,+BAA+B,GAC3C,SAAS,WAAW,EACpB,eAAe,MAAM,EACrB,kBAAiB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAoC,EAC1E,UAAS,uBAA4B,KACnC,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAWxB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,GACpC,SAAS,WAAW,EACpB,WAAW,kBAAkB,EAC7B,UAAS,uBAA4B,KACnC,OAAO,CAAC,MAAM,CAgBhB,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Role-grant fixtures for the cross-process suites — a pending offer, and an
|
|
4
|
+
* active grant materialized through the real offer + accept handlers — shared
|
|
5
|
+
* by the suites that need a grant to revoke
|
|
6
|
+
* (`describe_role_grant_offer_notification_ws_tests`,
|
|
7
|
+
* `describe_ws_role_gated_cross_tests`).
|
|
8
|
+
*
|
|
9
|
+
* Both drive the standard role-grant-offer RPC actions over the fixture's
|
|
10
|
+
* HTTP transport, as the keeper unless a grantor is given.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { assert } from 'vitest';
|
|
15
|
+
import { rpc_call } from '../rpc_helpers.js';
|
|
16
|
+
import { ROLE_ADMIN } from '../../auth/role_schema.js';
|
|
17
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
18
|
+
/**
|
|
19
|
+
* Create a fresh pending offer of `role` from `grantor_headers` (defaults to
|
|
20
|
+
* the keeper) to `to_account_id`, returning the created offer.
|
|
21
|
+
*
|
|
22
|
+
* @param fixture - the per-test fixture
|
|
23
|
+
* @param to_account_id - the recipient account
|
|
24
|
+
* @param grantor_headers - the grantor's session headers; the keeper's when omitted
|
|
25
|
+
* @param options - the role and the RPC path
|
|
26
|
+
* @returns the created offer
|
|
27
|
+
*/
|
|
28
|
+
export const create_pending_role_grant_offer = async (fixture, to_account_id, grantor_headers = fixture.create_session_headers(), options = {}) => {
|
|
29
|
+
const { role = ROLE_ADMIN, rpc_path = SPINE_RPC_PATH } = options;
|
|
30
|
+
const res = await rpc_call({
|
|
31
|
+
app: fixture.transport,
|
|
32
|
+
path: rpc_path,
|
|
33
|
+
method: 'role_grant_offer_create',
|
|
34
|
+
params: { to_account_id, role },
|
|
35
|
+
headers: grantor_headers
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok)
|
|
38
|
+
assert.fail(`offer create failed: ${JSON.stringify(res)}`);
|
|
39
|
+
return res.result.offer;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Materialize an active global grant of `role` on `recipient` through a fresh
|
|
43
|
+
* keeper offer and the recipient's accept, returning its `role_grant_id` — the
|
|
44
|
+
* handle `role_grant_revoke` keys on. Idempotent on a grant the account
|
|
45
|
+
* already holds: the accept returns that grant's id.
|
|
46
|
+
*
|
|
47
|
+
* @param fixture - the per-test fixture
|
|
48
|
+
* @param recipient - the account that accepts the offer
|
|
49
|
+
* @param options - the role and the RPC path
|
|
50
|
+
* @returns the active grant's id
|
|
51
|
+
*/
|
|
52
|
+
export const create_active_role_grant = async (fixture, recipient, options = {}) => {
|
|
53
|
+
const offer = await create_pending_role_grant_offer(fixture, recipient.account.id, fixture.create_session_headers(), options);
|
|
54
|
+
const accepted = await rpc_call({
|
|
55
|
+
app: fixture.transport,
|
|
56
|
+
path: options.rpc_path ?? SPINE_RPC_PATH,
|
|
57
|
+
method: 'role_grant_offer_accept',
|
|
58
|
+
params: { offer_id: offer.id },
|
|
59
|
+
headers: recipient.create_session_headers()
|
|
60
|
+
});
|
|
61
|
+
if (!accepted.ok)
|
|
62
|
+
assert.fail(`accept failed: ${JSON.stringify(accepted)}`);
|
|
63
|
+
return accepted.result.role_grant_id;
|
|
64
|
+
};
|
|
@@ -11,6 +11,14 @@ export interface RoleGrantOfferNotificationWsTestOptions {
|
|
|
11
11
|
readonly base_url: string;
|
|
12
12
|
/** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
|
|
13
13
|
readonly ws_path: string;
|
|
14
|
+
/**
|
|
15
|
+
* The roles `ws_path` gates its upgrade on (its `required_roles`), or
|
|
16
|
+
* omitted / `[]` for an ungated endpoint. Declare them for a backend whose
|
|
17
|
+
* socket is role-gated: revoking `ROLE_ADMIN` then closes the revokee's
|
|
18
|
+
* socket with `WS_CLOSE_PERMISSION_REVOKED`, and the revoke case asserts
|
|
19
|
+
* that close instead of the notification frame.
|
|
20
|
+
*/
|
|
21
|
+
readonly ws_required_roles?: ReadonlyArray<string>;
|
|
14
22
|
}
|
|
15
23
|
/**
|
|
16
24
|
* Register the role-grant-offer WS notification suite — seven cases over a real
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role_grant_offer_notification_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_offer_notification_ws.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"role_grant_offer_notification_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_offer_notification_ws.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4E9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AAKtE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAKzD,2EAA2E;AAC3E,MAAM,WAAW,uCAAuC;IACvD,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACnD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,+CAA+C,GAC3D,SAAS,uCAAuC,KAC9C,IA6VF,CAAC"}
|
|
@@ -38,6 +38,12 @@ import '../assert_dev_env.js';
|
|
|
38
38
|
* guard), and accept stays idempotent on the role_grant while still superseding
|
|
39
39
|
* pending siblings. Gated on `capabilities.ws`.
|
|
40
40
|
*
|
|
41
|
+
* The revoke case revokes the revokee's only `ROLE_ADMIN` grant. On an endpoint
|
|
42
|
+
* gated on that role (`ws_required_roles`) the revokee's socket loses its
|
|
43
|
+
* access by the same revoke: it is closed with `WS_CLOSE_PERMISSION_REVOKED`,
|
|
44
|
+
* and the notification may or may not reach it first (nothing orders the two),
|
|
45
|
+
* so the case asserts the close there and the frame on an ungated endpoint.
|
|
46
|
+
*
|
|
41
47
|
* Cross-process only: `create_admitted_ws_transport` needs a real bound socket, so wire
|
|
42
48
|
* it from a `*.cross.test.ts` file, never an in-process setup. Authed cookies
|
|
43
49
|
* come from the per-account session minted by `fixture.create_account` /
|
|
@@ -50,8 +56,10 @@ import { rpc_call } from '../rpc_helpers.js';
|
|
|
50
56
|
import { create_admitted_ws_transport } from '../transports/ws_transport.js';
|
|
51
57
|
import { is_notification_with } from '../transports/ws_client.js';
|
|
52
58
|
import { ROLE_ADMIN } from '../../auth/role_schema.js';
|
|
59
|
+
import { WS_CLOSE_PERMISSION_REVOKED } from '../../actions/transports.js';
|
|
53
60
|
import { ROLE_GRANT_OFFER_RECEIVED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_ACCEPTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_DECLINED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_RETRACTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_SUPERSEDE_NOTIFICATION_METHOD, ROLE_GRANT_REVOKE_NOTIFICATION_METHOD, RoleGrantOfferReceivedParams, RoleGrantOfferAcceptedParams, RoleGrantOfferDeclinedParams, RoleGrantOfferRetractedParams, RoleGrantOfferSupersedeParams, RoleGrantRevokeParams } from '../../auth/role_grant_offer_notifications.js';
|
|
54
61
|
import { test_if } from './capabilities.js';
|
|
62
|
+
import { create_active_role_grant, create_pending_role_grant_offer } from './role_grant_cross_helpers.js';
|
|
55
63
|
/** JSON-RPC endpoint path — matches the spine's `/api/rpc` (and the forge's). */
|
|
56
64
|
const RPC_PATH = '/api/rpc';
|
|
57
65
|
/**
|
|
@@ -63,7 +71,8 @@ const RPC_PATH = '/api/rpc';
|
|
|
63
71
|
* params schema. Gated on `capabilities.ws`.
|
|
64
72
|
*/
|
|
65
73
|
export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
66
|
-
const { setup_test, capabilities, base_url, ws_path } = options;
|
|
74
|
+
const { setup_test, capabilities, base_url, ws_path, ws_required_roles = [] } = options;
|
|
75
|
+
const revoke_closes_the_socket = ws_required_roles.includes(ROLE_ADMIN);
|
|
67
76
|
// -- shared helpers -------------------------------------------------------
|
|
68
77
|
/**
|
|
69
78
|
* Open a WS transport for a single session cookie (`<name>=<value>`),
|
|
@@ -81,29 +90,10 @@ export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
|
81
90
|
};
|
|
82
91
|
/** Drive a JSON-RPC call over the fixture's HTTP transport. */
|
|
83
92
|
const rpc = (fixture, method, params, headers) => rpc_call({ app: fixture.transport, path: RPC_PATH, method, params, headers });
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const create_pending_offer = async (fixture, to_account_id, grantor_headers = fixture.create_session_headers()) => {
|
|
89
|
-
const res = await rpc(fixture, 'role_grant_offer_create', { to_account_id, role: ROLE_ADMIN }, grantor_headers);
|
|
90
|
-
if (!res.ok)
|
|
91
|
-
assert.fail(`offer create failed: ${JSON.stringify(res)}`);
|
|
92
|
-
return res.result.offer;
|
|
93
|
-
};
|
|
94
|
-
/**
|
|
95
|
-
* Materialize an active `ROLE_ADMIN` role_grant on `recipient` via a fresh
|
|
96
|
-
* keeper offer + recipient accept (idempotent on a grant the account already
|
|
97
|
-
* holds), returning its `role_grant_id` — the handle `role_grant_revoke`
|
|
98
|
-
* keys on.
|
|
99
|
-
*/
|
|
100
|
-
const create_active_role_grant = async (fixture, recipient) => {
|
|
101
|
-
const offer = await create_pending_offer(fixture, recipient.account.id);
|
|
102
|
-
const accepted = await rpc(fixture, 'role_grant_offer_accept', { offer_id: offer.id }, recipient.create_session_headers());
|
|
103
|
-
if (!accepted.ok)
|
|
104
|
-
assert.fail(`accept failed: ${JSON.stringify(accepted)}`);
|
|
105
|
-
return accepted.result.role_grant_id;
|
|
106
|
-
};
|
|
93
|
+
/** A fresh pending `ROLE_ADMIN` offer, from the keeper unless a grantor is given. */
|
|
94
|
+
const create_pending_offer = (fixture, to_account_id, grantor_headers) => create_pending_role_grant_offer(fixture, to_account_id, grantor_headers, {
|
|
95
|
+
rpc_path: RPC_PATH
|
|
96
|
+
});
|
|
107
97
|
// -- tests ----------------------------------------------------------------
|
|
108
98
|
describe('role_grant_offer WS notifications (cross-process)', () => {
|
|
109
99
|
test_if(capabilities.ws, 'an offer create delivers role_grant_offer_received to the recipient WS', async () => {
|
|
@@ -187,22 +177,41 @@ export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
|
187
177
|
await recipient_ws.close();
|
|
188
178
|
}
|
|
189
179
|
});
|
|
190
|
-
test_if(capabilities.ws,
|
|
180
|
+
test_if(capabilities.ws, revoke_closes_the_socket
|
|
181
|
+
? 'revoke closes the revokee WS gated on the revoked role'
|
|
182
|
+
: 'revoke delivers a flat role_grant_revoke to the revokee WS', async () => {
|
|
191
183
|
const fixture = await setup_test();
|
|
192
184
|
const revokee = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
193
|
-
const role_grant_id = await create_active_role_grant(fixture, revokee
|
|
185
|
+
const role_grant_id = await create_active_role_grant(fixture, revokee, {
|
|
186
|
+
rpc_path: RPC_PATH
|
|
187
|
+
});
|
|
194
188
|
const revokee_ws = await open_ws(revokee.create_session_headers().cookie);
|
|
195
189
|
try {
|
|
196
190
|
const revoked = await rpc(fixture, 'role_grant_revoke', { actor_id: revokee.actor.id, role_grant_id, reason: 'cleanup' }, fixture.create_session_headers());
|
|
197
191
|
assert.isTrue(revoked.ok, `revoke failed: ${JSON.stringify(revoked)}`);
|
|
198
|
-
const
|
|
192
|
+
const is_revoke_frame = is_notification_with(ROLE_GRANT_REVOKE_NOTIFICATION_METHOD, (p) => p.role_grant_id === role_grant_id);
|
|
199
193
|
// Flat params — guards the no-`offer`-wrapper, `revoked_by`-omitted
|
|
200
194
|
// shape against drift.
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
195
|
+
const assert_flat_revoke = (frame) => {
|
|
196
|
+
const params = RoleGrantRevokeParams.parse(frame.params);
|
|
197
|
+
assert.strictEqual(params.role_grant_id, role_grant_id);
|
|
198
|
+
assert.strictEqual(params.role, ROLE_ADMIN);
|
|
199
|
+
assert.strictEqual(params.reason, 'cleanup');
|
|
200
|
+
assert.notProperty(frame.params, 'revoked_by');
|
|
201
|
+
};
|
|
202
|
+
if (revoke_closes_the_socket) {
|
|
203
|
+
// the socket lost the role its endpoint requires; the revoke
|
|
204
|
+
// notification may land just ahead of the close or not at all,
|
|
205
|
+
// and is held to the same shape when it does
|
|
206
|
+
const closed = await revokee_ws.wait_for_close(5000);
|
|
207
|
+
assert.ok(closed, 'the gated socket did not close after the revoke');
|
|
208
|
+
assert.strictEqual(revokee_ws.close_code, WS_CLOSE_PERMISSION_REVOKED);
|
|
209
|
+
const early = revokee_ws.messages.find(is_revoke_frame);
|
|
210
|
+
if (early)
|
|
211
|
+
assert_flat_revoke(early);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
assert_flat_revoke(await revokee_ws.wait_for(is_revoke_frame, 5000));
|
|
206
215
|
}
|
|
207
216
|
finally {
|
|
208
217
|
await revokee_ws.close();
|
|
@@ -240,7 +249,9 @@ export const describe_role_grant_offer_notification_ws_tests = (options) => {
|
|
|
240
249
|
const fixture = await setup_test();
|
|
241
250
|
const grantor = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
242
251
|
const revokee = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
243
|
-
const role_grant_id = await create_active_role_grant(fixture, revokee
|
|
252
|
+
const role_grant_id = await create_active_role_grant(fixture, revokee, {
|
|
253
|
+
rpc_path: RPC_PATH
|
|
254
|
+
});
|
|
244
255
|
// A *pending* sibling offer for the same (account, role) — this is what
|
|
245
256
|
// the revoke cascade supersedes.
|
|
246
257
|
const sibling_offer = await create_pending_offer(fixture, revokee.account.id, grantor.create_session_headers());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0C9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAYzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,6CAA6C,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;
|
|
1
|
+
{"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0C9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAYzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,6CAA6C,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAuBnG,kGAAkG;AAClG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,sDACG,CAAC;AAErD,MAAM,WAAW,6BAA6B;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAmDF,CAAC"}
|
|
@@ -61,9 +61,10 @@ export const RUST_SPINE_STUB_EXPECTED_SCHEMA_PATH_ENV = 'FUZ_RUST_SPINE_STUB_EXP
|
|
|
61
61
|
* plus `sse` (the stub serves `GET /api/admin/audit/stream` over the spine
|
|
62
62
|
* `fuz_realtime::SseRegistry` + audit listener), `ready` (it live-mounts
|
|
63
63
|
* `/ready` over the env-supplied fixture path), `ws_account_actions` (it
|
|
64
|
-
* serves one action registry on `/api/rpc` and `/api/ws`),
|
|
65
|
-
* `
|
|
66
|
-
*
|
|
64
|
+
* serves one action registry on `/api/rpc` and `/api/ws`), `cell_gated_create`
|
|
65
|
+
* (it mounts the `TestCellGatedCreateAuthorize` policy on its cell layer), and
|
|
66
|
+
* `ws_role_gated` (it mounts the role-gated WS endpoint at
|
|
67
|
+
* `SPINE_ROLE_GATED_WS_PATH`, sharing its connection registry). Named
|
|
67
68
|
* (not inline) so every spine preset is greppable, mirroring
|
|
68
69
|
* `ts_spine_capabilities`.
|
|
69
70
|
*/
|
|
@@ -72,7 +73,8 @@ const rust_spine_stub_capabilities = Object.freeze({
|
|
|
72
73
|
sse: true,
|
|
73
74
|
ready: true,
|
|
74
75
|
ws_account_actions: true,
|
|
75
|
-
cell_gated_create: true
|
|
76
|
+
cell_gated_create: true,
|
|
77
|
+
ws_role_gated: true
|
|
76
78
|
});
|
|
77
79
|
/** Default listening port — slots beside zzz's 1175/1176; matches the binary's `DEFAULT_PORT`. */
|
|
78
80
|
export const RUST_SPINE_STUB_DEFAULT_PORT = 1177;
|
|
@@ -51,6 +51,25 @@ export declare const SPINE_CELL_EDITOR_ROLE = "cell_editor";
|
|
|
51
51
|
* bootstrap-seed-only cell role.
|
|
52
52
|
*/
|
|
53
53
|
export declare const SPINE_PARTICIPANT_ROLE = "participant";
|
|
54
|
+
/**
|
|
55
|
+
* Mount path of the spine binaries' second, role-gated WebSocket endpoint —
|
|
56
|
+
* the same actions as the ungated `/api/ws`, behind
|
|
57
|
+
* `SPINE_ROLE_GATED_WS_ROLES`. Both spines mount it (the TS binary sharing
|
|
58
|
+
* `/api/ws`'s transport, the Rust stub its connection registry), so one
|
|
59
|
+
* account can hold a gated and an ungated socket side by side. Driven by
|
|
60
|
+
* `describe_ws_role_gated_cross_tests`, behind `capabilities.ws_role_gated`.
|
|
61
|
+
* Must match the Rust `testing_spine_stub`'s gated mount — a cross-language
|
|
62
|
+
* test contract.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SPINE_ROLE_GATED_WS_PATH = "/api/gated/ws";
|
|
65
|
+
/**
|
|
66
|
+
* The roles `SPINE_ROLE_GATED_WS_PATH` requires, any-of: `admin`, the gate a
|
|
67
|
+
* privileged-broadcast socket uses, and `participant`, a role that can be
|
|
68
|
+
* granted scoped — so the suite can revoke a scoped grant of a listed role
|
|
69
|
+
* and see nothing close. Must match the Rust `testing_spine_stub`'s gated
|
|
70
|
+
* mount.
|
|
71
|
+
*/
|
|
72
|
+
export declare const SPINE_ROLE_GATED_WS_ROLES: ReadonlyArray<string>;
|
|
54
73
|
/**
|
|
55
74
|
* Committed expected-schema fixture for the spine `/ready` deploy gate — the
|
|
56
75
|
* column map a fresh full spine bootstrap (auth + cell + cell_history + fact)
|
|
@@ -1 +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;;;;;;;;;;;;;;;;;;;;;GAqBG;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,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,EAAE,GAAwD,CAAC;AAEjG;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,kCAAkC,CAAC;AAE7E;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,uCAAuC,CAAC;AAEvF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,6BAA6B,CAAC"}
|
|
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;;;;;;;;;;;;;;;;;;;;;GAqBG;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,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,EAAE,aAAa,CAAC,MAAM,CAG1D,CAAC;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,EAAE,GAAwD,CAAC;AAEjG;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,kCAAkC,CAAC;AAE7E;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,uCAAuC,CAAC;AAEvF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,6BAA6B,CAAC"}
|
|
@@ -51,6 +51,28 @@ export const SPINE_CELL_EDITOR_ROLE = 'cell_editor';
|
|
|
51
51
|
* bootstrap-seed-only cell role.
|
|
52
52
|
*/
|
|
53
53
|
export const SPINE_PARTICIPANT_ROLE = 'participant';
|
|
54
|
+
/**
|
|
55
|
+
* Mount path of the spine binaries' second, role-gated WebSocket endpoint —
|
|
56
|
+
* the same actions as the ungated `/api/ws`, behind
|
|
57
|
+
* `SPINE_ROLE_GATED_WS_ROLES`. Both spines mount it (the TS binary sharing
|
|
58
|
+
* `/api/ws`'s transport, the Rust stub its connection registry), so one
|
|
59
|
+
* account can hold a gated and an ungated socket side by side. Driven by
|
|
60
|
+
* `describe_ws_role_gated_cross_tests`, behind `capabilities.ws_role_gated`.
|
|
61
|
+
* Must match the Rust `testing_spine_stub`'s gated mount — a cross-language
|
|
62
|
+
* test contract.
|
|
63
|
+
*/
|
|
64
|
+
export const SPINE_ROLE_GATED_WS_PATH = '/api/gated/ws';
|
|
65
|
+
/**
|
|
66
|
+
* The roles `SPINE_ROLE_GATED_WS_PATH` requires, any-of: `admin`, the gate a
|
|
67
|
+
* privileged-broadcast socket uses, and `participant`, a role that can be
|
|
68
|
+
* granted scoped — so the suite can revoke a scoped grant of a listed role
|
|
69
|
+
* and see nothing close. Must match the Rust `testing_spine_stub`'s gated
|
|
70
|
+
* mount.
|
|
71
|
+
*/
|
|
72
|
+
export const SPINE_ROLE_GATED_WS_ROLES = Object.freeze([
|
|
73
|
+
'admin',
|
|
74
|
+
SPINE_PARTICIPANT_ROLE
|
|
75
|
+
]);
|
|
54
76
|
/**
|
|
55
77
|
* Committed expected-schema fixture for the spine `/ready` deploy gate — the
|
|
56
78
|
* column map a fresh full spine bootstrap (auth + cell + cell_history + fact)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4E9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AAyFxE,kEAAkE;AAClE,MAAM,WAAW,+BAAgC,SAAQ,wBAAwB;IAChF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE;QACb,2EAA2E;QAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,+DAA+D;QAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,sDAAsD;QACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACF;AAED,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAyEF,CAAC"}
|
|
@@ -5,12 +5,15 @@ import '../assert_dev_env.js';
|
|
|
5
5
|
*
|
|
6
6
|
* `_testing_reset` / `_testing_mint_session` / `_testing_put_fact` /
|
|
7
7
|
* `_testing_flood_notifications` / `_testing_schema_snapshot` /
|
|
8
|
-
* `_testing_migration_tracker` / `_testing_action_manifest`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* `_testing_migration_tracker` / `_testing_action_manifest` /
|
|
9
|
+
* `_testing_origin_check` / `_testing_shorten_credential_expiry` are
|
|
10
|
+
* privileged test-binary actions the production
|
|
11
|
+
* wire never exposes — direct DB writes (full auth wipe, forged session row,
|
|
12
|
+
* raw fact insert), a flood of the caller's own connection, introspection
|
|
13
|
+
* reads (the live schema, the `schema_version` migration tracker, and the live
|
|
14
|
+
* RPC registry — the highest info-leak of the set were the gate to break), a
|
|
15
|
+
* probe of the origin allowlist parser, and a rewrite of an account's
|
|
16
|
+
* credential expiries. Their only structural fence
|
|
14
17
|
* is the **daemon-token** credential gate on
|
|
15
18
|
* each spec's `auth` axis. A test binary live-mounts them on its RPC
|
|
16
19
|
* endpoint (and, on fuz_app's spine binary and the Rust stub, its WS
|
|
@@ -92,7 +95,17 @@ const backdoor_methods = [
|
|
|
92
95
|
{ method: '_testing_migration_tracker', params: {} },
|
|
93
96
|
// The RPC-registry-dump read — input is an empty strict object, so `{}` is
|
|
94
97
|
// valid and clears the 400 phase, reaching the credential gate like the rest.
|
|
95
|
-
{ method: '_testing_action_manifest', params: {} }
|
|
98
|
+
{ method: '_testing_action_manifest', params: {} },
|
|
99
|
+
// The origin conformance probe — a valid `{patterns, origin}` pair, so it
|
|
100
|
+
// clears the 400 phase like the rest.
|
|
101
|
+
{ method: '_testing_origin_check', params: { patterns: 'http://localhost:*', origin: null } },
|
|
102
|
+
// The credential-expiry rewrite — a valid `{account_id, seconds}` pair (a
|
|
103
|
+
// syntactically valid UUID; no row needs to match), so it clears the 400
|
|
104
|
+
// phase like the rest.
|
|
105
|
+
{
|
|
106
|
+
method: '_testing_shorten_credential_expiry',
|
|
107
|
+
params: { account_id: '00000000-0000-4000-8000-000000000000', seconds: 30 }
|
|
108
|
+
}
|
|
96
109
|
];
|
|
97
110
|
const principals = [
|
|
98
111
|
{
|