@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.
Files changed (182) hide show
  1. package/dist/actions/CLAUDE.md +62 -17
  2. package/dist/actions/action_rpc.d.ts +5 -1
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +9 -1
  5. package/dist/actions/connection_closer.d.ts +25 -2
  6. package/dist/actions/connection_closer.d.ts.map +1 -1
  7. package/dist/actions/connection_closer.js +8 -1
  8. package/dist/actions/register_action_ws.d.ts +42 -6
  9. package/dist/actions/register_action_ws.d.ts.map +1 -1
  10. package/dist/actions/register_action_ws.js +87 -19
  11. package/dist/actions/register_ws_endpoint.d.ts +15 -6
  12. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  13. package/dist/actions/register_ws_endpoint.js +5 -2
  14. package/dist/actions/socket.svelte.d.ts +19 -4
  15. package/dist/actions/socket.svelte.d.ts.map +1 -1
  16. package/dist/actions/socket.svelte.js +31 -4
  17. package/dist/actions/transports.d.ts +30 -1
  18. package/dist/actions/transports.d.ts.map +1 -1
  19. package/dist/actions/transports.js +30 -1
  20. package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
  21. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  22. package/dist/actions/transports_ws_auth_guard.js +24 -10
  23. package/dist/actions/transports_ws_backend.d.ts +51 -5
  24. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  25. package/dist/actions/transports_ws_backend.js +62 -9
  26. package/dist/actions/ws_endpoint_spec.d.ts +11 -10
  27. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  28. package/dist/auth/CLAUDE.md +11 -9
  29. package/dist/auth/account_route_schema.d.ts +1 -0
  30. package/dist/auth/account_route_schema.d.ts.map +1 -1
  31. package/dist/auth/account_route_schema.js +4 -0
  32. package/dist/auth/api_token.d.ts +20 -0
  33. package/dist/auth/api_token.d.ts.map +1 -1
  34. package/dist/auth/api_token.js +21 -1
  35. package/dist/auth/api_token_queries.d.ts +22 -4
  36. package/dist/auth/api_token_queries.d.ts.map +1 -1
  37. package/dist/auth/api_token_queries.js +11 -6
  38. package/dist/auth/audit_log_routes.d.ts +10 -0
  39. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  40. package/dist/auth/audit_log_routes.js +20 -0
  41. package/dist/auth/audit_log_schema.d.ts +30 -5
  42. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  43. package/dist/auth/audit_log_schema.js +30 -0
  44. package/dist/auth/bearer_auth.d.ts +4 -2
  45. package/dist/auth/bearer_auth.d.ts.map +1 -1
  46. package/dist/auth/bearer_auth.js +19 -8
  47. package/dist/auth/bootstrap_route_schema.d.ts +1 -0
  48. package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
  49. package/dist/auth/bootstrap_route_schema.js +1 -0
  50. package/dist/auth/cleanup.d.ts +9 -9
  51. package/dist/auth/cleanup.js +9 -9
  52. package/dist/auth/credential_deadline.d.ts +95 -0
  53. package/dist/auth/credential_deadline.d.ts.map +1 -0
  54. package/dist/auth/credential_deadline.js +139 -0
  55. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  56. package/dist/auth/daemon_token_middleware.js +4 -1
  57. package/dist/auth/middleware.d.ts +90 -12
  58. package/dist/auth/middleware.d.ts.map +1 -1
  59. package/dist/auth/middleware.js +115 -16
  60. package/dist/auth/request_context.d.ts +4 -2
  61. package/dist/auth/request_context.d.ts.map +1 -1
  62. package/dist/auth/request_context.js +14 -3
  63. package/dist/auth/resolved_auth.d.ts +13 -2
  64. package/dist/auth/resolved_auth.d.ts.map +1 -1
  65. package/dist/auth/resolved_auth.js +5 -4
  66. package/dist/auth/session_cookie.d.ts +4 -0
  67. package/dist/auth/session_cookie.d.ts.map +1 -1
  68. package/dist/auth/session_cookie.js +3 -2
  69. package/dist/auth/session_middleware.d.ts +1 -1
  70. package/dist/auth/session_middleware.d.ts.map +1 -1
  71. package/dist/auth/session_middleware.js +4 -1
  72. package/dist/auth/session_queries.d.ts +19 -2
  73. package/dist/auth/session_queries.d.ts.map +1 -1
  74. package/dist/auth/session_queries.js +8 -2
  75. package/dist/auth/signup_route_schema.d.ts.map +1 -1
  76. package/dist/auth/signup_route_schema.js +1 -0
  77. package/dist/hono_context.d.ts +23 -0
  78. package/dist/hono_context.d.ts.map +1 -1
  79. package/dist/hono_context.js +19 -0
  80. package/dist/http/CLAUDE.md +41 -15
  81. package/dist/http/middleware_spec.d.ts +16 -0
  82. package/dist/http/middleware_spec.d.ts.map +1 -1
  83. package/dist/http/origin.d.ts +113 -18
  84. package/dist/http/origin.d.ts.map +1 -1
  85. package/dist/http/origin.js +247 -127
  86. package/dist/http/route_spec.d.ts +21 -0
  87. package/dist/http/route_spec.d.ts.map +1 -1
  88. package/dist/http/schema_helpers.d.ts +22 -0
  89. package/dist/http/schema_helpers.d.ts.map +1 -1
  90. package/dist/http/schema_helpers.js +24 -0
  91. package/dist/http/surface.d.ts +6 -1
  92. package/dist/http/surface.d.ts.map +1 -1
  93. package/dist/http/surface.js +12 -5
  94. package/dist/realtime/sse_auth_guard.d.ts +22 -6
  95. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  96. package/dist/realtime/sse_auth_guard.js +31 -12
  97. package/dist/realtime/subscriber_registry.d.ts +10 -1
  98. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  99. package/dist/realtime/subscriber_registry.js +10 -1
  100. package/dist/server/app_server.d.ts +20 -7
  101. package/dist/server/app_server.d.ts.map +1 -1
  102. package/dist/server/app_server.js +8 -3
  103. package/dist/server/env.d.ts +2 -1
  104. package/dist/server/env.d.ts.map +1 -1
  105. package/dist/server/env.js +7 -17
  106. package/dist/server/serve_fact_route.d.ts.map +1 -1
  107. package/dist/server/serve_fact_route.js +1 -0
  108. package/dist/testing/CLAUDE.md +154 -22
  109. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  110. package/dist/testing/adversarial_headers.js +13 -6
  111. package/dist/testing/connection_closer_helpers.d.ts +6 -1
  112. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  113. package/dist/testing/connection_closer_helpers.js +4 -0
  114. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  115. package/dist/testing/cross_backend/body_size.js +14 -65
  116. package/dist/testing/cross_backend/capabilities.d.ts +26 -2
  117. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  118. package/dist/testing/cross_backend/capabilities.js +9 -3
  119. package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
  120. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  121. package/dist/testing/cross_backend/conformance_case.js +6 -0
  122. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  123. package/dist/testing/cross_backend/conformance_table.js +20 -19
  124. package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
  125. package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
  126. package/dist/testing/cross_backend/credential_deadline.js +185 -0
  127. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/default_backend_configs.js +13 -2
  129. package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
  130. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  131. package/dist/testing/cross_backend/full_spine_mount.js +7 -1
  132. package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
  133. package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
  135. package/dist/testing/cross_backend/origin.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/origin.js +6 -26
  137. package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
  138. package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
  139. package/dist/testing/cross_backend/origin_conformance.js +136 -0
  140. package/dist/testing/cross_backend/origin_conformance.json +1679 -0
  141. package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
  142. package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/origin_scope.js +140 -0
  144. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
  145. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
  147. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
  148. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
  150. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
  152. package/dist/testing/cross_backend/spine_surface_constants.d.ts +19 -0
  153. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  154. package/dist/testing/cross_backend/spine_surface_constants.js +22 -0
  155. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  156. package/dist/testing/cross_backend/testing_backdoor.js +20 -7
  157. package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
  158. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  159. package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
  160. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  161. package/dist/testing/cross_backend/token_scope_surface.js +25 -21
  162. package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
  163. package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
  164. package/dist/testing/cross_backend/transport_composition.js +364 -0
  165. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  166. package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
  167. package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
  168. package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
  169. package/dist/testing/cross_backend/ws_role_gated.js +142 -0
  170. package/dist/testing/entities.d.ts +23 -0
  171. package/dist/testing/entities.d.ts.map +1 -1
  172. package/dist/testing/entities.js +37 -0
  173. package/dist/testing/integration.d.ts.map +1 -1
  174. package/dist/testing/integration.js +3 -1
  175. package/dist/testing/middleware.d.ts +10 -0
  176. package/dist/testing/middleware.d.ts.map +1 -1
  177. package/dist/testing/middleware.js +31 -7
  178. package/dist/testing/stubs.d.ts +16 -3
  179. package/dist/testing/stubs.d.ts.map +1 -1
  180. package/dist/testing/stubs.js +20 -7
  181. package/dist/testing/surface_invariants.js +2 -2
  182. 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;AAqE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAmC,MAAM,YAAY,CAAC;AAK7E,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;CACzB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,+CAA+C,GAC3D,SAAS,uCAAuC,KAC9C,IAuWF,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
- * Create a fresh pending `ROLE_ADMIN` offer from `grantor_headers` (defaults
86
- * to the keeper) to `to_account_id`, returning the created offer.
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, 'revoke delivers a flat role_grant_revoke to the revokee WS', async () => {
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 frame = await revokee_ws.wait_for(is_notification_with(ROLE_GRANT_REVOKE_NOTIFICATION_METHOD, (p) => p.role_grant_id === role_grant_id), 5000);
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 params = RoleGrantRevokeParams.parse(frame.params);
202
- assert.strictEqual(params.role_grant_id, role_grant_id);
203
- assert.strictEqual(params.role, ROLE_ADMIN);
204
- assert.strictEqual(params.reason, 'cleanup');
205
- assert.notProperty(frame.params, 'revoked_by');
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;AAqBnG,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"}
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`), and
65
- * `cell_gated_create` (it
66
- * mounts the `TestCellGatedCreateAuthorize` policy on its cell layer). Named
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;AAyE9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AA+ExE,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"}
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` are privileged
9
- * test-binary actions the production wire never exposes — direct DB writes
10
- * (full auth wipe, forged session row, raw fact insert), a flood of the
11
- * caller's own connection, and introspection reads (the live schema, the
12
- * `schema_version` migration tracker, and the live RPC registry — the highest
13
- * info-leak of the set were the gate to break). Their only structural fence
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
  {