@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
|
@@ -47,8 +47,8 @@ import { test } from 'vitest';
|
|
|
47
47
|
* `create_ws_transport`, so the in-process driver never runs it — the
|
|
48
48
|
* transport itself supports server-initiated requests),
|
|
49
49
|
* `ws_handshake_pipelining` (raw bytes on a real socket),
|
|
50
|
-
* `ws_account_actions` (a real socket's reply-then-close ordering),
|
|
51
|
-
* `cell_gated_create`. Cross-process consumers declare each flag explicitly
|
|
50
|
+
* `ws_account_actions` (a real socket's reply-then-close ordering),
|
|
51
|
+
* `cell_gated_create`, and `ws_role_gated`. Cross-process consumers declare each flag explicitly
|
|
52
52
|
* per backend.
|
|
53
53
|
*/
|
|
54
54
|
export const in_process_capabilities = Object.freeze({
|
|
@@ -71,7 +71,13 @@ export const in_process_capabilities = Object.freeze({
|
|
|
71
71
|
ws_self_revocation_reply: true,
|
|
72
72
|
// Cross-process-only: the test `CellCreateAuthorize` policy is mounted on the
|
|
73
73
|
// spine binaries' full mount + the Rust stub, not the in-process default app.
|
|
74
|
-
cell_gated_create: false
|
|
74
|
+
cell_gated_create: false,
|
|
75
|
+
// Cross-process-only: the gated endpoint is a spine-binary mount, and the
|
|
76
|
+
// case reads a real socket's close.
|
|
77
|
+
ws_role_gated: false,
|
|
78
|
+
// Read only by the front-door table's WS rows, which skip in-process (no
|
|
79
|
+
// socket to upgrade); Hono's own refusals carry their bodies.
|
|
80
|
+
ws_refusal_body: true
|
|
75
81
|
});
|
|
76
82
|
/**
|
|
77
83
|
* Shape notes for the in-process Hono transport — every wiring fact present
|
|
@@ -66,6 +66,11 @@ import { z } from 'zod';
|
|
|
66
66
|
* be replayed from a browser — wire-indistinguishable from sending no
|
|
67
67
|
* credential (the `token` principal is the honored counterpart: it suppresses
|
|
68
68
|
* `Origin`, so the same token authenticates).
|
|
69
|
+
* - `malformed_bearer` — the keeper's bearer api-token with its last character
|
|
70
|
+
* dropped, non-browser context, fresh jar. A value that cannot be a token
|
|
71
|
+
* fails each spine's shape precheck and soft-fails before any database work,
|
|
72
|
+
* so the request arrives anonymous — wire-indistinguishable from sending no
|
|
73
|
+
* credential.
|
|
69
74
|
* - `scoped_token` — the keeper authenticated via a bearer api-token minted
|
|
70
75
|
* with a **narrowed** `TokenScope` (`{kind: 'methods', methods:
|
|
71
76
|
* [SCOPED_TOKEN_ADMITTED_METHOD]}`), non-browser context. Minted through the
|
|
@@ -92,12 +97,13 @@ import { z } from 'zod';
|
|
|
92
97
|
export declare const ConformancePrincipal: z.ZodEnum<{
|
|
93
98
|
keeper: "keeper";
|
|
94
99
|
token: "token";
|
|
95
|
-
anonymous: "anonymous";
|
|
96
100
|
daemon: "daemon";
|
|
97
101
|
invalid_daemon: "invalid_daemon";
|
|
98
102
|
daemon_browser: "daemon_browser";
|
|
99
103
|
bearer_browser: "bearer_browser";
|
|
104
|
+
malformed_bearer: "malformed_bearer";
|
|
100
105
|
scoped_token: "scoped_token";
|
|
106
|
+
anonymous: "anonymous";
|
|
101
107
|
fresh_non_admin: "fresh_non_admin";
|
|
102
108
|
role_holder: "role_holder";
|
|
103
109
|
wrong_role: "wrong_role";
|
|
@@ -122,12 +128,13 @@ export declare const ConformanceCaseRequest: z.ZodObject<{
|
|
|
122
128
|
as: z.ZodEnum<{
|
|
123
129
|
keeper: "keeper";
|
|
124
130
|
token: "token";
|
|
125
|
-
anonymous: "anonymous";
|
|
126
131
|
daemon: "daemon";
|
|
127
132
|
invalid_daemon: "invalid_daemon";
|
|
128
133
|
daemon_browser: "daemon_browser";
|
|
129
134
|
bearer_browser: "bearer_browser";
|
|
135
|
+
malformed_bearer: "malformed_bearer";
|
|
130
136
|
scoped_token: "scoped_token";
|
|
137
|
+
anonymous: "anonymous";
|
|
131
138
|
fresh_non_admin: "fresh_non_admin";
|
|
132
139
|
role_holder: "role_holder";
|
|
133
140
|
wrong_role: "wrong_role";
|
|
@@ -179,12 +186,13 @@ export declare const ConformanceCase: z.ZodObject<{
|
|
|
179
186
|
as: z.ZodEnum<{
|
|
180
187
|
keeper: "keeper";
|
|
181
188
|
token: "token";
|
|
182
|
-
anonymous: "anonymous";
|
|
183
189
|
daemon: "daemon";
|
|
184
190
|
invalid_daemon: "invalid_daemon";
|
|
185
191
|
daemon_browser: "daemon_browser";
|
|
186
192
|
bearer_browser: "bearer_browser";
|
|
193
|
+
malformed_bearer: "malformed_bearer";
|
|
187
194
|
scoped_token: "scoped_token";
|
|
195
|
+
anonymous: "anonymous";
|
|
188
196
|
fresh_non_admin: "fresh_non_admin";
|
|
189
197
|
role_holder: "role_holder";
|
|
190
198
|
wrong_role: "wrong_role";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conformance_case.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_case.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB
|
|
1
|
+
{"version":3,"file":"conformance_case.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_case.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;EAc/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B,mBAAmB,CAAC;AAE7D,6CAA6C;AAC7C,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;kBAgBjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,8DAA8D;AAC9D,eAAO,MAAM,0BAA0B;;;;;;;kBA+DrC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB;;;kBAK/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAS1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC"}
|
|
@@ -66,6 +66,11 @@ import { z } from 'zod';
|
|
|
66
66
|
* be replayed from a browser — wire-indistinguishable from sending no
|
|
67
67
|
* credential (the `token` principal is the honored counterpart: it suppresses
|
|
68
68
|
* `Origin`, so the same token authenticates).
|
|
69
|
+
* - `malformed_bearer` — the keeper's bearer api-token with its last character
|
|
70
|
+
* dropped, non-browser context, fresh jar. A value that cannot be a token
|
|
71
|
+
* fails each spine's shape precheck and soft-fails before any database work,
|
|
72
|
+
* so the request arrives anonymous — wire-indistinguishable from sending no
|
|
73
|
+
* credential.
|
|
69
74
|
* - `scoped_token` — the keeper authenticated via a bearer api-token minted
|
|
70
75
|
* with a **narrowed** `TokenScope` (`{kind: 'methods', methods:
|
|
71
76
|
* [SCOPED_TOKEN_ADMITTED_METHOD]}`), non-browser context. Minted through the
|
|
@@ -96,6 +101,7 @@ export const ConformancePrincipal = z.enum([
|
|
|
96
101
|
'daemon_browser',
|
|
97
102
|
'token',
|
|
98
103
|
'bearer_browser',
|
|
104
|
+
'malformed_bearer',
|
|
99
105
|
'scoped_token',
|
|
100
106
|
'anonymous',
|
|
101
107
|
'fresh_non_admin',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conformance_table.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_table.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8B9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAOnE,OAAO,
|
|
1
|
+
{"version":3,"file":"conformance_table.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_table.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8B9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAOnE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAEN,KAAK,eAAe,EAEpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAIzD;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,sDAAsD;AACtD,MAAM,WAAW,uBAAuB;IACvC,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC/C,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,kEAAkE;IAClE,QAAQ,CAAC,aAAa,EAAE,uBAAuB,CAAC;IAChD,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACjD,iEAAiE;IACjE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAmPD;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,CAAC,MAAM,CAIrD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,MAAM,KACX,IAQF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EACvC,OAAO,MAAM,KACX,IAYF,CAAC;AAwKF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,uBAAuB,KAAG,IAkEnF,CAAC"}
|
|
@@ -27,9 +27,10 @@ import '../assert_dev_env.js';
|
|
|
27
27
|
import { assert, describe, test } from 'vitest';
|
|
28
28
|
import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
|
|
29
29
|
import { find_auth_route, rest_auth_route_suffixes } from '../integration_helpers.js';
|
|
30
|
-
import { find_rpc_action, headers_to_record, rpc_call,
|
|
30
|
+
import { find_rpc_action, headers_to_record, rpc_call, resolve_rpc_endpoints_for_setup } from '../rpc_helpers.js';
|
|
31
31
|
import { account_token_create_action_spec } from '../../auth/account_action_specs.js';
|
|
32
32
|
import { SCOPED_TOKEN_ADMITTED_METHOD } from './conformance_case.js';
|
|
33
|
+
import { mint_narrowed_api_token } from './narrowed_api_token.js';
|
|
33
34
|
import { xfail_until } from './xfail.js';
|
|
34
35
|
/**
|
|
35
36
|
* A deliberately malformed `X-Daemon-Token` value for the `invalid_daemon`
|
|
@@ -61,26 +62,13 @@ const mint_scoped_token = async (fixture, resolved_rpc_endpoints) => {
|
|
|
61
62
|
`on the surface to mint its narrowed token, but the method is not registered on ` +
|
|
62
63
|
`rpc_endpoints.`);
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
-
// `TokenCreateInput` (so dropping `scope` is a compile error, not a runtime
|
|
66
|
-
// 400 in every test at once), and `result.token` comes back typed and
|
|
67
|
-
// output-validated — no cast, no hand-rolled presence check.
|
|
68
|
-
const res = await rpc_call_for_spec({
|
|
65
|
+
return mint_narrowed_api_token({
|
|
69
66
|
app: fixture.transport,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
scope: { kind: 'methods', methods: [SCOPED_TOKEN_ADMITTED_METHOD] },
|
|
75
|
-
lifetime: { kind: 'eternal' }
|
|
76
|
-
},
|
|
77
|
-
headers: fixture.create_session_headers()
|
|
67
|
+
rpc_path: found.path,
|
|
68
|
+
session_headers: fixture.create_session_headers(),
|
|
69
|
+
methods: [SCOPED_TOKEN_ADMITTED_METHOD],
|
|
70
|
+
name: 'conformance scoped token'
|
|
78
71
|
});
|
|
79
|
-
if (!res.ok) {
|
|
80
|
-
throw new Error(`conformance: minting the 'scoped_token' principal failed with ${res.status}: ` +
|
|
81
|
-
JSON.stringify(res.error));
|
|
82
|
-
}
|
|
83
|
-
return res.result.token;
|
|
84
72
|
};
|
|
85
73
|
/**
|
|
86
74
|
* Map a `ConformancePrincipal` onto the transport + headers it
|
|
@@ -159,6 +147,19 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
|
|
|
159
147
|
transport: fixture.fresh_transport(),
|
|
160
148
|
headers: fixture.create_bearer_headers()
|
|
161
149
|
};
|
|
150
|
+
case 'malformed_bearer': {
|
|
151
|
+
// The keeper's valid token minus its last character — prefixed, one char
|
|
152
|
+
// short — over the same no-Origin, empty-jar transport as `token`, so the
|
|
153
|
+
// shape precheck (not the browser-context discard) is what refuses it.
|
|
154
|
+
const valid = fixture.create_bearer_headers().authorization;
|
|
155
|
+
if (!valid)
|
|
156
|
+
throw new Error(`conformance: principal 'malformed_bearer' found no bearer header`);
|
|
157
|
+
return {
|
|
158
|
+
transport: fixture.fresh_transport({ origin: null }),
|
|
159
|
+
headers: { authorization: valid.slice(0, -1) },
|
|
160
|
+
suppress_default_origin: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
162
163
|
case 'scoped_token': {
|
|
163
164
|
// A narrowed bearer, otherwise identical to `token` — empty jar + no
|
|
164
165
|
// Origin, so the credential is honored rather than browser-discarded
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import { type BackendCapabilities } from './capabilities.js';
|
|
3
|
+
import type { SetupTest } from './setup.js';
|
|
4
|
+
/** Configuration for `describe_credential_deadline_cross_tests`. */
|
|
5
|
+
export interface CredentialDeadlineCrossTestOptions {
|
|
6
|
+
/** Per-test fixture producer (`default_cross_process_setup(handle)`). */
|
|
7
|
+
readonly setup_test: SetupTest;
|
|
8
|
+
/** Backend capability flags — the WebSocket cases gate on `ws`, the stream case on `sse`. */
|
|
9
|
+
readonly capabilities: BackendCapabilities;
|
|
10
|
+
/** Base URL the backend is reachable at. */
|
|
11
|
+
readonly base_url: string;
|
|
12
|
+
/** The WebSocket endpoint path (e.g. `/api/ws`). */
|
|
13
|
+
readonly ws_path: string;
|
|
14
|
+
/** The audit-log stream path. Default `/api/admin/audit/stream`. */
|
|
15
|
+
readonly sse_path?: string;
|
|
16
|
+
/** JSON-RPC endpoint path. Default `SPINE_RPC_PATH`. */
|
|
17
|
+
readonly rpc_path?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Register the credential-deadline suite: a WebSocket on a session or an API
|
|
21
|
+
* token closes with 4001 "Credential expired" at its credential's expiry, an
|
|
22
|
+
* audit stream on a session ends there, and the expiry seam never extends.
|
|
23
|
+
*/
|
|
24
|
+
export declare const describe_credential_deadline_cross_tests: (options: CredentialDeadlineCrossTestOptions) => void;
|
|
25
|
+
//# sourceMappingURL=credential_deadline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential_deadline.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/credential_deadline.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmD9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAmC,MAAM,YAAY,CAAC;AAsB7E,oEAAoE;AACpE,MAAM,WAAW,kCAAkC;IAClD,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,6FAA6F;IAC7F,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,eAAO,MAAM,wCAAwC,GACpD,SAAS,kCAAkC,KACzC,IAoKF,CAAC"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-process **credential deadline** suite — a long-lived connection
|
|
4
|
+
* closes when the credential it was opened with expires.
|
|
5
|
+
*
|
|
6
|
+
* A WebSocket or an audit stream is authorized once, at open, and closed on
|
|
7
|
+
* revocation by identity — but expiry writes nothing a revocation listener
|
|
8
|
+
* could hear. So each spine carries the credential's deadline onto the
|
|
9
|
+
* connection (`ResolvedAuth.credential_deadline`: the earlier of a session's
|
|
10
|
+
* cookie and row expiry, an API token's row expiry) and closes it there: a
|
|
11
|
+
* socket with `WS_CLOSE_SESSION_REVOKED` and
|
|
12
|
+
* `WS_CLOSE_CREDENTIAL_EXPIRED_REASON`, a stream by ending it as a revoked one
|
|
13
|
+
* ends.
|
|
14
|
+
*
|
|
15
|
+
* Each case pulls a fresh account's credential expiries in to
|
|
16
|
+
* `expiry_seconds` from now with the daemon-token
|
|
17
|
+
* `_testing_shorten_credential_expiry` seam, **before** opening the
|
|
18
|
+
* connection (the deadline is read when the credential resolves), then checks
|
|
19
|
+
* the connection is open short of the deadline and closed at it:
|
|
20
|
+
*
|
|
21
|
+
* 1. WebSocket × session — 4001, "Credential expired".
|
|
22
|
+
* 2. WebSocket × API token — the same.
|
|
23
|
+
* 3. Audit stream × session — the stream ends.
|
|
24
|
+
* 4. The seam only shortens: a second call asking for a later expiry moves
|
|
25
|
+
* nothing, and the socket still closes at the first deadline.
|
|
26
|
+
*
|
|
27
|
+
* The fourth reachable combination, API token × audit stream, is not here:
|
|
28
|
+
* the stock audit stream admits sessions only on both spines. Each spine
|
|
29
|
+
* covers it below the wire — the Rust stream route on a widened credential
|
|
30
|
+
* gate, the TS deadline wait shared by both transports.
|
|
31
|
+
*
|
|
32
|
+
* Gated on `capabilities.ws` (cases 1, 2, 4) and `capabilities.sse` (case 3);
|
|
33
|
+
* cross-process only — the seam is mounted on the spine test binaries.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
import { assert, describe } from 'vitest';
|
|
38
|
+
import { heartbeat_action_spec } from '../../actions/heartbeat.js';
|
|
39
|
+
import { WS_CLOSE_CREDENTIAL_EXPIRED_REASON, WS_CLOSE_SESSION_REVOKED } from '../../actions/transports.js';
|
|
40
|
+
import { ROLE_ADMIN } from '../../auth/role_schema.js';
|
|
41
|
+
import { SSE_CONNECTED_COMMENT } from '../../realtime/sse_constants.js';
|
|
42
|
+
import { rpc_call_non_browser } from '../rpc_helpers.js';
|
|
43
|
+
import { create_sse_transport } from '../transports/sse_transport.js';
|
|
44
|
+
import { create_admitted_ws_transport } from '../transports/ws_transport.js';
|
|
45
|
+
import { test_if } from './capabilities.js';
|
|
46
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
47
|
+
/** Default audit-log stream path. */
|
|
48
|
+
const DEFAULT_SSE_PATH = '/api/admin/audit/stream';
|
|
49
|
+
/** The shortened lifetime each case gives its credentials, in seconds. */
|
|
50
|
+
const EXPIRY_SECONDS = 2;
|
|
51
|
+
/**
|
|
52
|
+
* How long past the deadline a close may take before the case fails — wide
|
|
53
|
+
* for a loaded machine, and bounded so a missing close fails rather than
|
|
54
|
+
* hangs.
|
|
55
|
+
*/
|
|
56
|
+
const CLOSE_GRACE_MS = 4000;
|
|
57
|
+
/**
|
|
58
|
+
* The deadline counts from the seam's call, and the connection opens after
|
|
59
|
+
* it; a close sooner than this after the call came before the deadline.
|
|
60
|
+
*/
|
|
61
|
+
const EARLIEST_CLOSE_MS = EXPIRY_SECONDS * 1000 - 300;
|
|
62
|
+
/**
|
|
63
|
+
* Register the credential-deadline suite: a WebSocket on a session or an API
|
|
64
|
+
* token closes with 4001 "Credential expired" at its credential's expiry, an
|
|
65
|
+
* audit stream on a session ends there, and the expiry seam never extends.
|
|
66
|
+
*/
|
|
67
|
+
export const describe_credential_deadline_cross_tests = (options) => {
|
|
68
|
+
const { setup_test, capabilities, base_url, ws_path, sse_path = DEFAULT_SSE_PATH, rpc_path = SPINE_RPC_PATH } = options;
|
|
69
|
+
/**
|
|
70
|
+
* Pull `account`'s credential expiries in to `seconds` from now through the
|
|
71
|
+
* daemon-token seam, returning when it was called.
|
|
72
|
+
*/
|
|
73
|
+
const shorten = async (fixture, account, seconds) => {
|
|
74
|
+
const called_at = performance.now();
|
|
75
|
+
const result = await rpc_call_non_browser({
|
|
76
|
+
app: fixture.fresh_transport({ origin: null }),
|
|
77
|
+
path: rpc_path,
|
|
78
|
+
method: '_testing_shorten_credential_expiry',
|
|
79
|
+
params: { account_id: account.account.id, seconds },
|
|
80
|
+
headers: fixture.create_daemon_token_headers()
|
|
81
|
+
});
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
assert.fail(`_testing_shorten_credential_expiry failed: ${JSON.stringify(result)}`);
|
|
84
|
+
}
|
|
85
|
+
const counts = result.result;
|
|
86
|
+
return { called_at, ...counts };
|
|
87
|
+
};
|
|
88
|
+
const cookie_of = (account) => {
|
|
89
|
+
const { cookie } = account.create_session_headers();
|
|
90
|
+
assert.ok(cookie, 'expected a session cookie');
|
|
91
|
+
return cookie;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Assert `client` is open short of the deadline, then closes with 4001
|
|
95
|
+
* "Credential expired" at it.
|
|
96
|
+
*/
|
|
97
|
+
const assert_closes_at_deadline = async (client, called_at) => {
|
|
98
|
+
// open and dispatching before the deadline
|
|
99
|
+
assert.deepStrictEqual(await client.request(1, heartbeat_action_spec.method, {}), {});
|
|
100
|
+
assert.ok(await client.wait_for_close(EXPIRY_SECONDS * 1000 + CLOSE_GRACE_MS), 'the socket did not close at its credential deadline');
|
|
101
|
+
const elapsed = performance.now() - called_at;
|
|
102
|
+
assert.ok(elapsed >= EARLIEST_CLOSE_MS, `closed ${Math.round(elapsed)}ms in, before the deadline`);
|
|
103
|
+
assert.strictEqual(client.close_code, WS_CLOSE_SESSION_REVOKED);
|
|
104
|
+
assert.strictEqual(client.close_reason, WS_CLOSE_CREDENTIAL_EXPIRED_REASON);
|
|
105
|
+
};
|
|
106
|
+
describe('credential deadline (cross-process)', () => {
|
|
107
|
+
test_if(capabilities.ws, 'a session socket closes with 4001 "Credential expired" at the deadline', async () => {
|
|
108
|
+
const fixture = await setup_test();
|
|
109
|
+
const account = await fixture.create_account();
|
|
110
|
+
const { called_at, sessions } = await shorten(fixture, account, EXPIRY_SECONDS);
|
|
111
|
+
assert.ok(sessions >= 1, 'the seam shortened the account session');
|
|
112
|
+
const client = await create_admitted_ws_transport({
|
|
113
|
+
base_url,
|
|
114
|
+
ws_path,
|
|
115
|
+
cookies: [cookie_of(account)],
|
|
116
|
+
origin: base_url
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
await assert_closes_at_deadline(client, called_at);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await client.close();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
test_if(capabilities.ws, 'an API token socket closes with 4001 "Credential expired" at the deadline', async () => {
|
|
126
|
+
const fixture = await setup_test();
|
|
127
|
+
const account = await fixture.create_account();
|
|
128
|
+
const { called_at, api_tokens } = await shorten(fixture, account, EXPIRY_SECONDS);
|
|
129
|
+
assert.ok(api_tokens >= 1, 'the seam shortened the account API token');
|
|
130
|
+
// a bearer outside a browser context: no Origin, no cookie
|
|
131
|
+
const client = await create_admitted_ws_transport({
|
|
132
|
+
base_url,
|
|
133
|
+
ws_path,
|
|
134
|
+
cookies: [],
|
|
135
|
+
origin: null,
|
|
136
|
+
headers: { Authorization: `Bearer ${account.api_token}` }
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
await assert_closes_at_deadline(client, called_at);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await client.close();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
test_if(capabilities.sse, 'a session audit stream ends at the deadline', async () => {
|
|
146
|
+
const fixture = await setup_test();
|
|
147
|
+
const account = await fixture.create_account({ roles: [ROLE_ADMIN] });
|
|
148
|
+
const { called_at } = await shorten(fixture, account, EXPIRY_SECONDS);
|
|
149
|
+
const sse = await create_sse_transport({
|
|
150
|
+
base_url,
|
|
151
|
+
sse_path,
|
|
152
|
+
cookies: [cookie_of(account)],
|
|
153
|
+
origin: base_url
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
assert.strictEqual((await sse.read_frame()) + '\n\n', SSE_CONNECTED_COMMENT, 'the stream opened');
|
|
157
|
+
assert.ok(await sse.wait_for_close(EXPIRY_SECONDS * 1000 + CLOSE_GRACE_MS), 'the stream did not end at its credential deadline');
|
|
158
|
+
const elapsed = performance.now() - called_at;
|
|
159
|
+
assert.ok(elapsed >= EARLIEST_CLOSE_MS, `ended ${Math.round(elapsed)}ms in, before the deadline`);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await sse.close();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
test_if(capabilities.ws, 'the expiry seam only shortens: a later expiry moves nothing', async () => {
|
|
166
|
+
const fixture = await setup_test();
|
|
167
|
+
const account = await fixture.create_account();
|
|
168
|
+
const { called_at } = await shorten(fixture, account, EXPIRY_SECONDS);
|
|
169
|
+
// asks for a minute; the credentials stay due at the first deadline
|
|
170
|
+
await shorten(fixture, account, 60);
|
|
171
|
+
const client = await create_admitted_ws_transport({
|
|
172
|
+
base_url,
|
|
173
|
+
ws_path,
|
|
174
|
+
cookies: [cookie_of(account)],
|
|
175
|
+
origin: base_url
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
await assert_closes_at_deadline(client, called_at);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await client.close();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAYhG,2CAA2C;AAC3C,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,yBAAyB,KAChC,MAAM,CAAC,MAAM,EAAE,MAAM,CAKtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,
|
|
1
|
+
{"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"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"}
|
|
@@ -53,7 +53,13 @@ export const ts_default_capabilities = Object.freeze({
|
|
|
53
53
|
// Off by default — the `cell_gated_create` test policy is a fuz_app test
|
|
54
54
|
// fixture, mounted only on the spine binaries (`ts_spine_*`), not generic
|
|
55
55
|
// consumers.
|
|
56
|
-
cell_gated_create: false
|
|
56
|
+
cell_gated_create: false,
|
|
57
|
+
// Off by default — the second, role-gated WS endpoint is a spine-binary
|
|
58
|
+
// mount (`SPINE_ROLE_GATED_WS_PATH`); fuz_app's spine configs opt in.
|
|
59
|
+
ws_role_gated: false,
|
|
60
|
+
// Off by default: `@hono/node-ws` drops a refused upgrade's body, so a
|
|
61
|
+
// Node-served consumer sends none. A Deno- or Bun-served one may opt in.
|
|
62
|
+
ws_refusal_body: false
|
|
57
63
|
});
|
|
58
64
|
/**
|
|
59
65
|
* Capabilities for the Rust family. The non-gating wiring facts live in
|
|
@@ -88,7 +94,12 @@ export const rust_default_capabilities = Object.freeze({
|
|
|
88
94
|
ws_self_revocation_reply: false,
|
|
89
95
|
// Off by default — the `testing_spine_stub` preset opts in (it mounts the
|
|
90
96
|
// `TestCellGatedCreateAuthorize` policy); a generic Rust consumer doesn't.
|
|
91
|
-
cell_gated_create: false
|
|
97
|
+
cell_gated_create: false,
|
|
98
|
+
// Off by default — the `testing_spine_stub` preset opts in (it mounts the
|
|
99
|
+
// role-gated WS endpoint at `SPINE_ROLE_GATED_WS_PATH`).
|
|
100
|
+
ws_role_gated: false,
|
|
101
|
+
// axum sends a refused upgrade's response as built, body included.
|
|
102
|
+
ws_refusal_body: true
|
|
92
103
|
});
|
|
93
104
|
/**
|
|
94
105
|
* Shape notes for TS-family backends — wiring facts, not gating flags.
|
|
@@ -14,6 +14,9 @@ import '../assert_dev_env.js';
|
|
|
14
14
|
* - the `_testing_*` daemon-token backdoors (`create_testing_actions`),
|
|
15
15
|
* - the full cell verb set (CRUD + grant + field + item + audit),
|
|
16
16
|
* - the opt-in `actor_lookup` / `actor_search` resolvers,
|
|
17
|
+
* - the `_testing_origin_check` probe the origin conformance table drives,
|
|
18
|
+
* - the `_testing_shorten_credential_expiry` seam the credential-deadline
|
|
19
|
+
* suite drives,
|
|
17
20
|
* - the `_testing_action_manifest` backdoor, appended last (it dumps the live
|
|
18
21
|
* method set for the cross-impl manifest-parity gate, so it must enumerate
|
|
19
22
|
* every method above it).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"full_spine_mount.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/full_spine_mount.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B
|
|
1
|
+
{"version":3,"file":"full_spine_mount.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/full_spine_mount.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAG5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8CAA8C,CAAC;AAcvF,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CA0CjB,CAAC;AAEF,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,cAUF,CAAC"}
|
|
@@ -5,7 +5,7 @@ import { create_standard_rpc_actions } from '../../auth/standard_rpc_actions.js'
|
|
|
5
5
|
import { create_all_cell_actions } from '../../auth/all_cell_actions.js';
|
|
6
6
|
import { create_actor_lookup_actions } from '../../auth/actor_lookup_actions.js';
|
|
7
7
|
import { create_actor_search_actions } from '../../auth/actor_search_actions.js';
|
|
8
|
-
import { create_testing_action_manifest_action, create_testing_actions } from './testing_reset_actions.js';
|
|
8
|
+
import { create_testing_action_manifest_action, create_testing_actions, create_testing_origin_check_action, create_testing_shorten_credential_expiry_action } from './testing_reset_actions.js';
|
|
9
9
|
import { test_cell_gated_create_authorize } from './test_cell_gated_create_authorize.js';
|
|
10
10
|
import { spine_roles, spine_session_options } from './default_spine_surface.js';
|
|
11
11
|
/**
|
|
@@ -39,6 +39,12 @@ export const build_full_spine_rpc_actions = (deps, options) => {
|
|
|
39
39
|
...create_all_cell_actions({ ...deps, authorize_create: test_cell_gated_create_authorize }, { roles: spine_roles }),
|
|
40
40
|
...create_actor_lookup_actions(deps),
|
|
41
41
|
...create_actor_search_actions(deps),
|
|
42
|
+
// the origin conformance table's probe — kept out of
|
|
43
|
+
// `create_testing_actions` (see its spec doc)
|
|
44
|
+
create_testing_origin_check_action(),
|
|
45
|
+
// the credential-deadline suite's shorten-only seam — kept out of
|
|
46
|
+
// `create_testing_actions` for the same reason
|
|
47
|
+
create_testing_shorten_credential_expiry_action(),
|
|
42
48
|
// `peer/ping` is mounted on the HTTP RPC endpoint too (not just WS) so an
|
|
43
49
|
// HTTP invocation reaches the handler and refuses with `peer_no_transport`
|
|
44
50
|
// rather than `method_not_found`. It's a protocol action, so it's filtered
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import { type RpcCallApp } from '../rpc_helpers.js';
|
|
3
|
+
/** Options for `mint_narrowed_api_token`. */
|
|
4
|
+
export interface MintNarrowedApiTokenOptions {
|
|
5
|
+
/** Transport the mint goes over — it must carry an allowed `Origin` (the default `rpc_call` one does). */
|
|
6
|
+
readonly app: RpcCallApp;
|
|
7
|
+
/** RPC endpoint path `account_token_create` is mounted on. */
|
|
8
|
+
readonly rpc_path: string;
|
|
9
|
+
/** Session credential headers of the account that owns the token — the mint is session-only. */
|
|
10
|
+
readonly session_headers: Record<string, string>;
|
|
11
|
+
/** The RPC methods the token admits; every other method and every non-RPC surface refuses it. */
|
|
12
|
+
readonly methods: ReadonlyArray<string>;
|
|
13
|
+
/** Token label. Default `'narrowed test token'`. */
|
|
14
|
+
readonly name?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Mint a narrowed api token and return its raw bearer value.
|
|
18
|
+
*
|
|
19
|
+
* @returns the raw token, for an `Authorization: Bearer` header
|
|
20
|
+
* @throws Error when the mint is refused, naming the status and error
|
|
21
|
+
*/
|
|
22
|
+
export declare const mint_narrowed_api_token: (options: MintNarrowedApiTokenOptions) => Promise<string>;
|
|
23
|
+
//# sourceMappingURL=narrowed_api_token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"narrowed_api_token.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/narrowed_api_token.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiB9B,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEvE,6CAA6C;AAC7C,MAAM,WAAW,2BAA2B;IAC3C,0GAA0G;IAC1G,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gGAAgG;IAChG,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjD,iGAAiG;IACjG,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC,oDAAoD;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,SAAS,2BAA2B,KAClC,OAAO,CAAC,MAAM,CAmBhB,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mint a narrowed (method-scoped) api token through the production
|
|
4
|
+
* `account_token_create` path, for suites that probe what a narrowed token is
|
|
5
|
+
* refused.
|
|
6
|
+
*
|
|
7
|
+
* Minted rather than seeded: the token comes out of the same handler a real
|
|
8
|
+
* minter uses, on whichever account owns the session the caller passes — so a
|
|
9
|
+
* suite can put a narrowed token on an account that lacks a gating role, and
|
|
10
|
+
* the mint itself asserts that a narrowed scope is accepted at the mint
|
|
11
|
+
* boundary, not merely stored.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { account_token_create_action_spec } from '../../auth/account_action_specs.js';
|
|
16
|
+
import { rpc_call_for_spec } from '../rpc_helpers.js';
|
|
17
|
+
/**
|
|
18
|
+
* Mint a narrowed api token and return its raw bearer value.
|
|
19
|
+
*
|
|
20
|
+
* @returns the raw token, for an `Authorization: Bearer` header
|
|
21
|
+
* @throws Error when the mint is refused, naming the status and error
|
|
22
|
+
*/
|
|
23
|
+
export const mint_narrowed_api_token = async (options) => {
|
|
24
|
+
const { app, rpc_path, session_headers, methods, name = 'narrowed test token' } = options;
|
|
25
|
+
const res = await rpc_call_for_spec({
|
|
26
|
+
app,
|
|
27
|
+
path: rpc_path,
|
|
28
|
+
spec: account_token_create_action_spec,
|
|
29
|
+
params: {
|
|
30
|
+
name,
|
|
31
|
+
scope: { kind: 'methods', methods: [...methods] },
|
|
32
|
+
lifetime: { kind: 'eternal' }
|
|
33
|
+
},
|
|
34
|
+
headers: session_headers
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
throw new Error(`minting a narrowed api token failed with ${res.status}: ${JSON.stringify(res.error)}`);
|
|
38
|
+
}
|
|
39
|
+
return res.result.token;
|
|
40
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"origin.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/origin.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"origin.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/origin.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6B9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAM9D,eAAO,MAAM,2BAA2B,GAAI,SAAS,sBAAsB,KAAG,IAqB7E,CAAC"}
|
|
@@ -3,18 +3,17 @@ import '../assert_dev_env.js';
|
|
|
3
3
|
* Cross-backend parity suite for Origin verification.
|
|
4
4
|
*
|
|
5
5
|
* Origin checking is middleware that runs *before* the RPC dispatcher and
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* imperative suite
|
|
9
|
-
* in-process origin tests were already hand-rolled. Two cases:
|
|
6
|
+
* refuses only an `Origin` it doesn't allow. The case here needs a transport
|
|
7
|
+
* that sends no `Origin` at all (`fresh_transport({origin: null})`), so this
|
|
8
|
+
* is an imperative suite rather than a conformance-table row. The case:
|
|
10
9
|
*
|
|
11
|
-
* - **disallowed `Origin` → 403** `forbidden_origin`, refused before any
|
|
12
|
-
* handler runs (the allowlist rejects the cross-origin request even with
|
|
13
|
-
* a valid session cookie attached).
|
|
14
10
|
* - **absent `Origin` → request passes** — non-browser / direct-access
|
|
15
11
|
* clients (curl, CLI, server-to-server) carry no `Origin` and must not be
|
|
16
12
|
* blocked; token auth is the control for those callers.
|
|
17
13
|
*
|
|
14
|
+
* A disallowed `Origin` → 403 `forbidden_origin`, on every transport, is
|
|
15
|
+
* pinned by the front-door table (`transport_composition.ts`).
|
|
16
|
+
*
|
|
18
17
|
* Runs both legs via the shared `setup_test` protocol: the
|
|
19
18
|
* in-process leg (`auth/origin_parity.db.test.ts`, plain `gro test`) and the
|
|
20
19
|
* cross-process leg (`cross_backend/origin.cross.test.ts`, the TS spine
|
|
@@ -25,7 +24,6 @@ import '../assert_dev_env.js';
|
|
|
25
24
|
*/
|
|
26
25
|
import { describe, test, assert } from 'vitest';
|
|
27
26
|
import { account_verify_action_spec } from '../../auth/account_action_specs.js';
|
|
28
|
-
import { ERROR_FORBIDDEN_ORIGIN } from '../../http/error_schemas.js';
|
|
29
27
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
30
28
|
/** Build the JSON-RPC envelope body for a nullary `account_verify` call. */
|
|
31
29
|
const verify_envelope = (id) => JSON.stringify({ jsonrpc: '2.0', method: account_verify_action_spec.method, id });
|
|
@@ -33,24 +31,6 @@ export const describe_origin_cross_tests = (options) => {
|
|
|
33
31
|
const { setup_test } = options;
|
|
34
32
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
35
33
|
describe('origin verification parity', () => {
|
|
36
|
-
test('disallowed Origin → 403 forbidden_origin (refused before dispatch)', async () => {
|
|
37
|
-
const fixture = await setup_test();
|
|
38
|
-
// Keeper session cookie attached + a rogue Origin header (overrides
|
|
39
|
-
// the transport's default allowed Origin). The allowlist must reject
|
|
40
|
-
// before the dispatcher, returning a flat REST error body.
|
|
41
|
-
const res = await fixture.transport(rpc_path, {
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: {
|
|
44
|
-
...fixture.create_session_headers(),
|
|
45
|
-
origin: 'http://evil.com',
|
|
46
|
-
'content-type': 'application/json'
|
|
47
|
-
},
|
|
48
|
-
body: verify_envelope('evil-origin')
|
|
49
|
-
});
|
|
50
|
-
assert.strictEqual(res.status, 403, 'disallowed Origin must be rejected with 403');
|
|
51
|
-
const body = (await res.json().catch(() => undefined));
|
|
52
|
-
assert.strictEqual(body?.error, ERROR_FORBIDDEN_ORIGIN);
|
|
53
|
-
});
|
|
54
34
|
test('absent Origin → request passes (non-browser direct access)', async () => {
|
|
55
35
|
const fixture = await setup_test();
|
|
56
36
|
// `origin: null` so no Origin header is sent at all (a header omission
|