@fuzdev/fuz_app 0.97.0 → 0.98.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 +57 -23
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_codegen.d.ts +1 -1
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +1 -1
- package/dist/actions/{action_peer.d.ts → action_dispatcher.d.ts} +12 -12
- package/dist/actions/action_dispatcher.d.ts.map +1 -0
- package/dist/actions/{action_peer.js → action_dispatcher.js} +2 -2
- package/dist/actions/action_event.d.ts.map +1 -1
- package/dist/actions/action_rpc.d.ts +9 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/broadcast_api.d.ts +2 -2
- package/dist/actions/broadcast_api.d.ts.map +1 -1
- package/dist/actions/compile_action_registry.d.ts.map +1 -1
- package/dist/actions/frontend_rpc_client.d.ts +3 -3
- package/dist/actions/frontend_rpc_client.d.ts.map +1 -1
- package/dist/actions/frontend_rpc_client.js +3 -3
- package/dist/actions/peer_ping.d.ts +147 -0
- package/dist/actions/peer_ping.d.ts.map +1 -0
- package/dist/actions/peer_ping.js +189 -0
- package/dist/actions/peer_request.d.ts +158 -0
- package/dist/actions/peer_request.d.ts.map +1 -0
- package/dist/actions/peer_request.js +167 -0
- package/dist/actions/perform_action.d.ts +8 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +2 -1
- package/dist/actions/protocol.d.ts +3 -3
- package/dist/actions/protocol.d.ts.map +1 -1
- package/dist/actions/protocol.js +9 -4
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +28 -1
- package/dist/actions/rpc_client.d.ts +4 -4
- package/dist/actions/rpc_client.d.ts.map +1 -1
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/transports.d.ts +1 -1
- package/dist/actions/transports_http.d.ts.map +1 -1
- package/dist/actions/transports_ws.d.ts +12 -5
- package/dist/actions/transports_ws.d.ts.map +1 -1
- package/dist/actions/transports_ws.js +36 -6
- package/dist/actions/transports_ws_backend.d.ts +34 -1
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +62 -5
- package/dist/auth/CLAUDE.md +12 -12
- package/dist/auth/actor_lookup_action_specs.d.ts.map +1 -1
- package/dist/auth/actor_search_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.d.ts +31 -12
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +36 -12
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/db/create_db.d.ts.map +1 -1
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/status.d.ts.map +1 -1
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/jsonrpc_helpers.d.ts +1 -1
- package/dist/http/jsonrpc_helpers.js +1 -1
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +11 -0
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +7 -4
- package/dist/testing/CLAUDE.md +18 -15
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +3 -1
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest.d.ts +7 -7
- package/dist/testing/cross_backend/action_manifest.js +7 -7
- package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +15 -12
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +4 -4
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.d.ts +2 -0
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +13 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +32 -3
- package/dist/testing/cross_backend/cookie_attributes.d.ts +20 -0
- package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -0
- package/dist/testing/cross_backend/cookie_attributes.js +158 -0
- package/dist/testing/cross_backend/credential_header_robustness.d.ts +12 -0
- package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -0
- package/dist/testing/cross_backend/credential_header_robustness.js +179 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +5 -3
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +7 -0
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/method_coverage.d.ts +3 -7
- package/dist/testing/cross_backend/method_coverage.d.ts.map +1 -1
- package/dist/testing/cross_backend/method_coverage.js +1 -2
- package/dist/testing/cross_backend/peer_ping_ws.js +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts +26 -0
- package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_offer_enumeration.js +108 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +2 -4
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +0 -13
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +12 -4
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +9 -3
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db_entities.d.ts +15 -0
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +16 -1
- package/dist/testing/error_coverage.d.ts.map +1 -1
- package/dist/testing/error_coverage.js +1 -2
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +3 -1
- package/dist/testing/schema_parity.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/transports/bootstrap.d.ts.map +1 -1
- package/dist/testing/transports/ws_client.d.ts +1 -1
- package/dist/testing/transports/ws_transport.d.ts +1 -1
- package/dist/testing/ws_round_trip.d.ts +1 -1
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +3 -3
- package/dist/ui/CLAUDE.md +6 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/form_state.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.d.ts +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +1 -1
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/actions/action_peer.d.ts.map +0 -1
|
@@ -142,18 +142,21 @@ export interface BackendCapabilities {
|
|
|
142
142
|
/**
|
|
143
143
|
* The backend can **initiate** a JSON-RPC request to a connected client
|
|
144
144
|
* and await its typed reply (the server→client request/response direction
|
|
145
|
-
*
|
|
145
|
+
* ActionPeer adds). Gates `describe_peer_ping_ws_tests` — the on-demand
|
|
146
146
|
* `peer/ping` round-trip plus its security negatives (unsolicited-response
|
|
147
147
|
* rejection, per-connection id isolation, never-replying `Timeout`,
|
|
148
148
|
* wrong-shape reply rejection).
|
|
149
149
|
*
|
|
150
|
-
* `true` for the Rust spine (server-initiated requests landed
|
|
151
|
-
* canonical)
|
|
152
|
-
* `BackendWebsocketTransport.
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
150
|
+
* `true` for both the Rust spine (server-initiated requests landed
|
|
151
|
+
* Rust-first canonical) and the TS spine
|
|
152
|
+
* (`BackendWebsocketTransport.request_connection` drives the round-trip,
|
|
153
|
+
* correlated by `register_action_ws`). The conservative
|
|
154
|
+
* `ts_default_capabilities` keeps it `false` like sse/ready — a TS backend
|
|
155
|
+
* opts in once it mounts `peer/ping` on a WS endpoint **and** the HTTP RPC
|
|
156
|
+
* endpoint (the no-transport refusal). Like ws/sse/cells, `peer/ping` stays
|
|
157
|
+
* off the standard declared surface (it's a protocol action,
|
|
158
|
+
* manifest-excluded), so this flag is the only opt-in into the peer parity
|
|
159
|
+
* coverage.
|
|
157
160
|
*/
|
|
158
161
|
readonly peer_request: boolean;
|
|
159
162
|
}
|
|
@@ -161,10 +164,10 @@ export interface BackendCapabilities {
|
|
|
161
164
|
* Capability declarations for the in-process Hono transport. Nearly every
|
|
162
165
|
* flag is `true` because in-process testing exercises the full backend
|
|
163
166
|
* with no missing optional behaviors. The one exception is `peer_request`:
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
167
|
+
* `describe_peer_ping_ws_tests` is cross-process-only (it needs a real bound
|
|
168
|
+
* socket with an `on_request` responder via `create_ws_transport`), so the
|
|
169
|
+
* in-process driver never runs it — the transport itself supports
|
|
170
|
+
* server-initiated requests. Cross-process consumers declare each flag
|
|
168
171
|
* explicitly per backend.
|
|
169
172
|
*/
|
|
170
173
|
export declare const in_process_capabilities: BackendCapabilities;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwC9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;IACrD
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwC9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;IACrD;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAWpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
|
|
@@ -39,10 +39,10 @@ import { test } from 'vitest';
|
|
|
39
39
|
* Capability declarations for the in-process Hono transport. Nearly every
|
|
40
40
|
* flag is `true` because in-process testing exercises the full backend
|
|
41
41
|
* with no missing optional behaviors. The one exception is `peer_request`:
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* `describe_peer_ping_ws_tests` is cross-process-only (it needs a real bound
|
|
43
|
+
* socket with an `on_request` responder via `create_ws_transport`), so the
|
|
44
|
+
* in-process driver never runs it — the transport itself supports
|
|
45
|
+
* server-initiated requests. Cross-process consumers declare each flag
|
|
46
46
|
* explicitly per backend.
|
|
47
47
|
*/
|
|
48
48
|
export const in_process_capabilities = Object.freeze({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAG3B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,kCAAkC,CAAC;AAErE,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC;CAC5F;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAC1B,WAAW,cAAc,EACzB,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,QAAQ,OAAO,EACf,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7B,OAAO,CAAC,SAAS,CAMnB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,YAAY,GAAI,GAAG,SAAS,KAAG,OAG/B,CAAC;AAEd;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,GAAG,SAAS,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAG,
|
|
1
|
+
{"version":3,"file":"cell_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAG3B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,kCAAkC,CAAC;AAErE,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC;CAC5F;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAC1B,WAAW,cAAc,EACzB,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,QAAQ,OAAO,EACf,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7B,OAAO,CAAC,SAAS,CAMnB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,YAAY,GAAI,GAAG,SAAS,KAAG,OAG/B,CAAC;AAEd;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,GAAG,SAAS,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAG,CAUrE,CAAC"}
|
|
@@ -122,6 +122,7 @@ export declare const ConformanceCaseExpectation: z.ZodObject<{
|
|
|
122
122
|
status: z.ZodNumber;
|
|
123
123
|
error_reason: z.ZodOptional<z.ZodString>;
|
|
124
124
|
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
125
|
+
absent_fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
125
126
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
126
127
|
equivalence_group: z.ZodOptional<z.ZodString>;
|
|
127
128
|
}, z.core.$strict>;
|
|
@@ -175,6 +176,7 @@ export declare const ConformanceCase: z.ZodObject<{
|
|
|
175
176
|
status: z.ZodNumber;
|
|
176
177
|
error_reason: z.ZodOptional<z.ZodString>;
|
|
177
178
|
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
179
|
+
absent_fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
178
180
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
|
|
179
181
|
equivalence_group: z.ZodOptional<z.ZodString>;
|
|
180
182
|
}, z.core.$strict>;
|
|
@@ -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,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAY/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,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
|
|
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,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAY/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,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"}
|
|
@@ -130,6 +130,19 @@ export const ConformanceCaseExpectation = z.strictObject({
|
|
|
130
130
|
description: 'Specific field-value assertions on the success `result` (2xx) or the error ' +
|
|
131
131
|
'`error.data` (non-2xx). Each key must deep-equal the corresponding response field.',
|
|
132
132
|
}),
|
|
133
|
+
absent_fields: z
|
|
134
|
+
.array(z.string())
|
|
135
|
+
.optional()
|
|
136
|
+
.meta({
|
|
137
|
+
description: 'Sensitive field names that must NOT appear ANYWHERE (at any depth) in the ' +
|
|
138
|
+
'normalized response body — the negative-space twin of `fields`. Each name is ' +
|
|
139
|
+
'searched recursively through the success `result` / error envelope, so a ' +
|
|
140
|
+
'secret column (`password_hash`, `token_hash`) that leaks at any nesting level ' +
|
|
141
|
+
'(top-level, inside a list element) fails the case. Pins "this secret never ' +
|
|
142
|
+
'serializes" on BOTH spines — a serialization regression would otherwise leak ' +
|
|
143
|
+
'identically and silently on each. Non-vacuous only when the body is known ' +
|
|
144
|
+
'non-empty (e.g. a list that always contains the caller); note that in the case.',
|
|
145
|
+
}),
|
|
133
146
|
headers: z
|
|
134
147
|
.record(z.string(), z.string().nullable())
|
|
135
148
|
.optional()
|
|
@@ -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,EAAC,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAOjE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAC,KAAK,eAAe,EAA4B,MAAM,uBAAuB,CAAC;AACtF,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAc,MAAM,YAAY,CAAC;AAGvD;;;;;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;
|
|
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,EAAC,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAOjE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAC,KAAK,eAAe,EAA4B,MAAM,uBAAuB,CAAC;AACtF,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAc,MAAM,YAAY,CAAC;AAGvD;;;;;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;AAuLD;;;;;;;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;AAuKF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,uBAAuB,KAAG,IAkEnF,CAAC"}
|
|
@@ -155,6 +155,29 @@ const assert_fields = (actual, fields, label) => {
|
|
|
155
155
|
assert.deepEqual(record[key], expected, `${label}: field '${key}'`);
|
|
156
156
|
}
|
|
157
157
|
};
|
|
158
|
+
/**
|
|
159
|
+
* Whether `key` is present as an own property of `value` or any object nested
|
|
160
|
+
* within it (arrays and objects walked recursively). The shape-agnostic test
|
|
161
|
+
* behind `expect.absent_fields` — a secret column leaking at any depth (inside
|
|
162
|
+
* a list element, a nested record) is caught, not just a top-level field.
|
|
163
|
+
*/
|
|
164
|
+
const has_key_anywhere = (value, key) => {
|
|
165
|
+
if (Array.isArray(value))
|
|
166
|
+
return value.some((v) => has_key_anywhere(v, key));
|
|
167
|
+
if (value !== null && typeof value === 'object') {
|
|
168
|
+
if (Object.hasOwn(value, key))
|
|
169
|
+
return true;
|
|
170
|
+
return Object.values(value).some((v) => has_key_anywhere(v, key));
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
};
|
|
174
|
+
/** Assert none of the sensitive field names appear anywhere in the response body. */
|
|
175
|
+
const assert_absent_fields = (body, absent, label) => {
|
|
176
|
+
for (const key of absent) {
|
|
177
|
+
assert.ok(!has_key_anywhere(body, key), `${label}: response must not serialize the sensitive field '${key}' anywhere in its ` +
|
|
178
|
+
`body — a secret column leaked into the wire shape.`);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
158
181
|
/**
|
|
159
182
|
* Response headers that fingerprint the backend implementation or framework.
|
|
160
183
|
* Neither spine emits these; the runner asserts they stay absent on EVERY
|
|
@@ -215,10 +238,16 @@ const is_success_status = (status) => status >= 200 && status < 300;
|
|
|
215
238
|
const run_case = async (c, options, resolved_rpc_endpoints) => {
|
|
216
239
|
const fixture = await options.setup_test();
|
|
217
240
|
const { transport, headers, suppress_default_origin } = await resolve_principal(fixture, c.request.as, options.principals);
|
|
218
|
-
|
|
219
|
-
|
|
241
|
+
const normalized = c.request.method.startsWith('/')
|
|
242
|
+
? await run_rest_case(c, options, transport, headers)
|
|
243
|
+
: await run_rpc_case(c, transport, headers, suppress_default_origin, resolved_rpc_endpoints);
|
|
244
|
+
// Negative-space field check, uniform across REST + RPC and success + error
|
|
245
|
+
// bodies: a sensitive column (`password_hash`, `token_hash`) must not appear
|
|
246
|
+
// anywhere in what the prober sees.
|
|
247
|
+
if (c.expect.absent_fields) {
|
|
248
|
+
assert_absent_fields(normalized.body, c.expect.absent_fields, c.name);
|
|
220
249
|
}
|
|
221
|
-
return
|
|
250
|
+
return normalized;
|
|
222
251
|
};
|
|
223
252
|
/** Dispatch + assert a case targeting an RPC method. */
|
|
224
253
|
const run_rpc_case = async (c, transport, headers, suppress_default_origin, resolved_rpc_endpoints) => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { SetupTest } from './setup.js';
|
|
3
|
+
/** Options for the session-cookie-attribute parity suite. */
|
|
4
|
+
export interface CookieAttributesCrossTestOptions {
|
|
5
|
+
/** Per-test fixture producer (cross-process only — see the module doc). */
|
|
6
|
+
readonly setup_test: SetupTest;
|
|
7
|
+
/**
|
|
8
|
+
* The spine's session cookie name (`handle.config.cookie_name`, e.g.
|
|
9
|
+
* `fuz_session`). The `Set-Cookie` under test is matched by this name so a
|
|
10
|
+
* future cookie-name change surfaces here as "no session cookie set" rather
|
|
11
|
+
* than a silent miss.
|
|
12
|
+
*/
|
|
13
|
+
readonly cookie_name: string;
|
|
14
|
+
/** REST login route path. Default `/api/account/login`. */
|
|
15
|
+
readonly login_path?: string;
|
|
16
|
+
/** REST logout route path. Default `/api/account/logout`. */
|
|
17
|
+
readonly logout_path?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const describe_cookie_attributes_cross_tests: (options: CookieAttributesCrossTestOptions) => void;
|
|
20
|
+
//# sourceMappingURL=cookie_attributes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie_attributes.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cookie_attributes.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA+C9B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAE1C,6DAA6D;AAC7D,MAAM,WAAW,gCAAgC;IAChD,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoED,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAmFF,CAAC"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for **session cookie attributes** over real HTTP.
|
|
4
|
+
*
|
|
5
|
+
* The session cookie's `Set-Cookie` attributes are the load-bearing browser
|
|
6
|
+
* security boundary: `HttpOnly` keeps the token out of JS (XSS can't read it),
|
|
7
|
+
* `Secure` keeps it off plaintext HTTP, `SameSite=Strict` is the primary CSRF
|
|
8
|
+
* defense (the Origin allowlist is only defense-in-depth), and `Path=/` scopes
|
|
9
|
+
* it to the whole app. A regression dropping any one of these is a real
|
|
10
|
+
* downgrade — and it's wire-observable, so both spines must emit the same
|
|
11
|
+
* hardened set. The cross-backend conformance table deliberately keeps
|
|
12
|
+
* `Set-Cookie` *out* of its byte-identity comparison (the signed value
|
|
13
|
+
* legitimately differs per request and per impl), so the attribute contract
|
|
14
|
+
* had no cross-backend pin: the TS spine's attributes were covered by
|
|
15
|
+
* `session_cookie.ts`'s own tests, but the Rust spine's `sign_session_cookie`
|
|
16
|
+
* / `clear_session_cookie` strings were asserted nowhere. This suite closes
|
|
17
|
+
* that gap on both impls by parsing the raw `Set-Cookie` and asserting the
|
|
18
|
+
* attributes directly. Three properties:
|
|
19
|
+
*
|
|
20
|
+
* - **successful login sets a hardened cookie** — `HttpOnly; Secure;
|
|
21
|
+
* SameSite=Strict; Path=/` plus a positive integer `Max-Age` (the session
|
|
22
|
+
* lifetime). The signed value is opaque here; only the attributes are pinned.
|
|
23
|
+
* - **a failed login sets no session cookie** — a denial mints no credential,
|
|
24
|
+
* so there is no `Set-Cookie` for the session name at all. A spine that
|
|
25
|
+
* leaked a (signed-but-unauthenticated) cookie on the 401 would fail here.
|
|
26
|
+
* - **logout clears the cookie with `Max-Age=0` and the same hardened flags** —
|
|
27
|
+
* the clear must not silently drop `Secure` / `HttpOnly` / `SameSite=Strict`
|
|
28
|
+
* (a cleared-but-unhardened `Set-Cookie` is a downgrade window).
|
|
29
|
+
*
|
|
30
|
+
* Both surfaces are flat REST (`POST /api/account/{login,logout}`) on every
|
|
31
|
+
* spine, so this is an imperative suite (not a `conformance_table` row) — the
|
|
32
|
+
* sibling of `origin.ts` / `login_security.ts`. Cross-process only: reading the
|
|
33
|
+
* raw `Set-Cookie` attributes needs the wire response (the in-process
|
|
34
|
+
* `parse_session` / `session_cookie.rs` unit tests cover each impl's cookie
|
|
35
|
+
* codec directly). Cited property: `docs/security.md` §"Session Security"
|
|
36
|
+
* (the "Cookie attributes" bullet — `HttpOnly; Secure; SameSite=Strict; Path=/`).
|
|
37
|
+
*
|
|
38
|
+
* `$lib`-free by contract (relative specifiers only), like the sibling
|
|
39
|
+
* cross-backend suites.
|
|
40
|
+
*
|
|
41
|
+
* @module
|
|
42
|
+
*/
|
|
43
|
+
import { describe, test, assert } from 'vitest';
|
|
44
|
+
import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
|
|
45
|
+
/**
|
|
46
|
+
* Parse a single `Set-Cookie` header value into `{name, value, attributes}`.
|
|
47
|
+
* Returns `null` for a malformed head (no `=`, empty name). Unlike the
|
|
48
|
+
* transport jar's `parse_set_cookie` (which drops everything after the first
|
|
49
|
+
* `;`), this retains the attributes — they are exactly what the suite asserts.
|
|
50
|
+
*/
|
|
51
|
+
const parse_set_cookie = (raw) => {
|
|
52
|
+
const parts = raw.split(';');
|
|
53
|
+
const head = (parts[0] ?? '').trim();
|
|
54
|
+
const eq = head.indexOf('=');
|
|
55
|
+
if (eq <= 0)
|
|
56
|
+
return null;
|
|
57
|
+
const name = head.slice(0, eq).trim();
|
|
58
|
+
if (!name)
|
|
59
|
+
return null;
|
|
60
|
+
const value = head.slice(eq + 1);
|
|
61
|
+
const attributes = new Map();
|
|
62
|
+
for (let i = 1; i < parts.length; i++) {
|
|
63
|
+
const attr = parts[i].trim();
|
|
64
|
+
if (!attr)
|
|
65
|
+
continue;
|
|
66
|
+
const attr_eq = attr.indexOf('=');
|
|
67
|
+
if (attr_eq === -1) {
|
|
68
|
+
attributes.set(attr.toLowerCase(), '');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
attributes.set(attr.slice(0, attr_eq).trim().toLowerCase(), attr.slice(attr_eq + 1).trim());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { name, value, attributes };
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Find the `Set-Cookie` for the session cookie among a response's headers.
|
|
78
|
+
* `getSetCookie()` returns each `Set-Cookie` as its own string (the only way
|
|
79
|
+
* to read attributes — `Headers.get()` collapses multiple cookies to one
|
|
80
|
+
* comma-joined string). Returns `undefined` when no session cookie was set.
|
|
81
|
+
*/
|
|
82
|
+
const find_session_cookie = (res, cookie_name) => {
|
|
83
|
+
for (const raw of res.headers.getSetCookie()) {
|
|
84
|
+
const parsed = parse_set_cookie(raw);
|
|
85
|
+
if (parsed && parsed.name === cookie_name)
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
};
|
|
90
|
+
/** Assert the hardened attribute set shared by the set + cleared cookies. */
|
|
91
|
+
const assert_hardened_flags = (cookie, label) => {
|
|
92
|
+
assert.ok(cookie.attributes.has('httponly'), `${label}: cookie must be HttpOnly (no JS read)`);
|
|
93
|
+
assert.ok(cookie.attributes.has('secure'), `${label}: cookie must be Secure (HTTPS-only)`);
|
|
94
|
+
assert.strictEqual(cookie.attributes.get('samesite')?.toLowerCase(), 'strict', `${label}: cookie must be SameSite=Strict (CSRF backstop)`);
|
|
95
|
+
assert.strictEqual(cookie.attributes.get('path'), '/', `${label}: cookie Path must be '/'`);
|
|
96
|
+
};
|
|
97
|
+
export const describe_cookie_attributes_cross_tests = (options) => {
|
|
98
|
+
const { setup_test, cookie_name } = options;
|
|
99
|
+
const login_path = options.login_path ?? '/api/account/login';
|
|
100
|
+
const logout_path = options.logout_path ?? '/api/account/logout';
|
|
101
|
+
// Fresh-keeper-per-test wipes the DB between tests, so a literal username
|
|
102
|
+
// never collides (see `setup.ts`). Each test mints it fresh.
|
|
103
|
+
const username = 'cookie_attr_user';
|
|
104
|
+
describe('session cookie attribute parity', () => {
|
|
105
|
+
test('successful login sets a hardened session cookie (HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age>0)', async () => {
|
|
106
|
+
const fixture = await setup_test();
|
|
107
|
+
await fixture.create_account({ username, password_value: DEFAULT_TEST_PASSWORD });
|
|
108
|
+
// Fresh transport so no pre-existing cookie rides along; the default
|
|
109
|
+
// Origin (= base_url) is allowlisted, so the login clears the Origin gate.
|
|
110
|
+
const res = await fixture.fresh_transport()(login_path, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'content-type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ username, password: DEFAULT_TEST_PASSWORD }),
|
|
114
|
+
});
|
|
115
|
+
assert.strictEqual(res.status, 200, 'valid credentials must log in');
|
|
116
|
+
const cookie = find_session_cookie(res, cookie_name);
|
|
117
|
+
assert.ok(cookie, `login must Set-Cookie the '${cookie_name}' session cookie`);
|
|
118
|
+
assert.ok(cookie.value.length > 0, 'the session cookie must carry a signed value');
|
|
119
|
+
assert_hardened_flags(cookie, 'login Set-Cookie');
|
|
120
|
+
const max_age = Number(cookie.attributes.get('max-age'));
|
|
121
|
+
assert.ok(Number.isInteger(max_age) && max_age > 0, `login cookie must carry a positive integer Max-Age (got ${cookie.attributes.get('max-age')})`);
|
|
122
|
+
});
|
|
123
|
+
test('failed login (wrong password) sets no session cookie', async () => {
|
|
124
|
+
const fixture = await setup_test();
|
|
125
|
+
await fixture.create_account({ username, password_value: DEFAULT_TEST_PASSWORD });
|
|
126
|
+
const res = await fixture.fresh_transport()(login_path, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'content-type': 'application/json' },
|
|
129
|
+
body: JSON.stringify({ username, password: `wrong-${DEFAULT_TEST_PASSWORD}` }),
|
|
130
|
+
});
|
|
131
|
+
assert.strictEqual(res.status, 401, 'wrong password must be rejected');
|
|
132
|
+
assert.strictEqual(find_session_cookie(res, cookie_name), undefined, 'a failed login must not Set-Cookie a session — no credential is minted on denial');
|
|
133
|
+
});
|
|
134
|
+
test('logout clears the session cookie with Max-Age=0 and hardened flags', async () => {
|
|
135
|
+
const fixture = await setup_test();
|
|
136
|
+
await fixture.create_account({ username, password_value: DEFAULT_TEST_PASSWORD });
|
|
137
|
+
// Log in on a fresh transport so its jar carries the session cookie into
|
|
138
|
+
// the logout request (logout is session-authenticated).
|
|
139
|
+
const transport = fixture.fresh_transport();
|
|
140
|
+
const login_res = await transport(login_path, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'content-type': 'application/json' },
|
|
143
|
+
body: JSON.stringify({ username, password: DEFAULT_TEST_PASSWORD }),
|
|
144
|
+
});
|
|
145
|
+
assert.strictEqual(login_res.status, 200, 'precondition: login must succeed');
|
|
146
|
+
const res = await transport(logout_path, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'content-type': 'application/json' },
|
|
149
|
+
body: JSON.stringify({}),
|
|
150
|
+
});
|
|
151
|
+
assert.strictEqual(res.status, 200, 'logout must succeed for an authenticated session');
|
|
152
|
+
const cookie = find_session_cookie(res, cookie_name);
|
|
153
|
+
assert.ok(cookie, `logout must Set-Cookie a cleared '${cookie_name}'`);
|
|
154
|
+
assert.strictEqual(cookie.attributes.get('max-age'), '0', 'logout must clear the cookie with Max-Age=0');
|
|
155
|
+
assert_hardened_flags(cookie, 'logout Set-Cookie');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/** Options for the credential-header robustness probe — needs the raw URL + a valid daemon token. */
|
|
3
|
+
export interface CredentialHeaderRobustnessCrossTestOptions {
|
|
4
|
+
/** Base URL the spawned backend is reachable at (e.g. `http://localhost:1178`). */
|
|
5
|
+
readonly base_url: string;
|
|
6
|
+
/** RPC endpoint path to target. Default `/api/rpc`. */
|
|
7
|
+
readonly rpc_path?: string;
|
|
8
|
+
/** A valid daemon token (keeper) — `handle.daemon_token`. */
|
|
9
|
+
readonly daemon_token: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const describe_credential_header_robustness_cross_tests: (options: CredentialHeaderRobustnessCrossTestOptions) => void;
|
|
12
|
+
//# sourceMappingURL=credential_header_robustness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential_header_robustness.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/credential_header_robustness.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2D9B,qGAAqG;AACrG,MAAM,WAAW,0CAA0C;IAC1D,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAwFD,eAAO,MAAM,iDAAiD,GAC7D,SAAS,0CAA0C,KACjD,IAkHF,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend **credential-header robustness** probe — the auth sibling of
|
|
4
|
+
* `body_size_smuggling.ts`.
|
|
5
|
+
*
|
|
6
|
+
* The auth middleware reads a single credential per header (`Authorization`,
|
|
7
|
+
* `X-Daemon-Token`). What a *duplicated*, *oversized*, or *control-char-injected*
|
|
8
|
+
* credential header does is framework-territory — Hono reads its headers via the
|
|
9
|
+
* Web `Headers` API, axum via `http::HeaderMap` — and the two could resolve a
|
|
10
|
+
* duplicate differently (first-vs-last), cap header size differently, or parse a
|
|
11
|
+
* malformed value differently. None of that is exercised over `fetch` (the
|
|
12
|
+
* `FetchTransport` can't emit a duplicate or malformed header), so it had no
|
|
13
|
+
* cross-impl pin. This raw-socket suite sends hand-framed requests and asserts
|
|
14
|
+
* the **security invariants that must hold regardless of how each framework
|
|
15
|
+
* resolves the ambiguity** — so it pins a real property without blessing one
|
|
16
|
+
* resolution over the other:
|
|
17
|
+
*
|
|
18
|
+
* - **a duplicated credential header never escalates** — a request carrying a
|
|
19
|
+
* valid keeper `X-Daemon-Token` *and* a second, conflicting one must not
|
|
20
|
+
* perform a keeper operation. The target omits `confirm`, so the documented
|
|
21
|
+
* confirm guard (`purge_not_confirmed`) makes the honored-token branch a
|
|
22
|
+
* guaranteed non-2xx with **zero side effect**, and the discarded-token
|
|
23
|
+
* branch is a non-2xx auth/credential refusal — so whichever copy the
|
|
24
|
+
* framework picks, the outcome is non-2xx. The same shape with two garbage
|
|
25
|
+
* `Authorization` headers pins the bearer path can't be escalated either.
|
|
26
|
+
* - **a control-char-injected credential header can't smuggle or authenticate**
|
|
27
|
+
* — embedded `CRLF` / bare `CR` in the value never frames a second request
|
|
28
|
+
* (no smuggle) and never authenticates (the truncated / rejected value is no
|
|
29
|
+
* token).
|
|
30
|
+
* - **no response desync** — each crafted request yields exactly one HTTP
|
|
31
|
+
* response (a duplicate header that reframed the request would surface as a
|
|
32
|
+
* second status line, like the body-size smuggling probe).
|
|
33
|
+
* - **an oversized credential header can't wedge the server** — after a request
|
|
34
|
+
* with a 64 KiB `X-Daemon-Token` (well past both frameworks' header caps), a
|
|
35
|
+
* subsequent normal request on a fresh connection still gets a response. The
|
|
36
|
+
* oversized request's own outcome (4xx / 431 / connection close) is
|
|
37
|
+
* don't-care; surviving it is the property.
|
|
38
|
+
*
|
|
39
|
+
* Raw-socket by necessity (`fetch` can't emit duplicate or malformed headers),
|
|
40
|
+
* so — like `body_size_smuggling.ts` — this is **cross-process only** and
|
|
41
|
+
* fixture-free: it needs only the base URL, the RPC path, and a valid daemon
|
|
42
|
+
* token (`handle.daemon_token`, kept current for the run by the same rotation
|
|
43
|
+
* the `_testing_reset` channel relies on). Cited property: `docs/security.md`
|
|
44
|
+
* §"Credential Type Hierarchy" (a leaked / conflicting credential header can't
|
|
45
|
+
* escalate the credential ceiling) + §"API Token Security" (bearer handling).
|
|
46
|
+
*
|
|
47
|
+
* `$lib`-free by contract (relative + `node:` specifiers only).
|
|
48
|
+
*
|
|
49
|
+
* @module
|
|
50
|
+
*/
|
|
51
|
+
import { connect } from 'node:net';
|
|
52
|
+
import { describe, test, assert } from 'vitest';
|
|
53
|
+
import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
|
|
54
|
+
import { SPINE_RPC_PATH } from './default_spine_surface.js';
|
|
55
|
+
/** A well-formed UUID that never names a real row. */
|
|
56
|
+
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
57
|
+
/** A value that is never a real credential — used for the conflicting / garbage copies. */
|
|
58
|
+
const GARBAGE_TOKEN = 'not-a-real-credential-value';
|
|
59
|
+
/**
|
|
60
|
+
* Open a raw TCP socket to `base_url`, write `request_bytes` once, and collect
|
|
61
|
+
* everything the server sends back until it closes or `read_timeout_ms`
|
|
62
|
+
* elapses. Write errors are swallowed (a server closing mid-write on a rejected
|
|
63
|
+
* request is expected behavior); what matters is what we read back. Mirrors
|
|
64
|
+
* `body_size_smuggling.ts`'s `send_raw`.
|
|
65
|
+
*/
|
|
66
|
+
const send_raw = (base_url, request_bytes, read_timeout_ms) => new Promise((resolve) => {
|
|
67
|
+
const url = new URL(base_url);
|
|
68
|
+
const port = Number(url.port) || (url.protocol === 'https:' ? 443 : 80);
|
|
69
|
+
const socket = connect({ host: url.hostname, port });
|
|
70
|
+
let received = '';
|
|
71
|
+
let settled = false;
|
|
72
|
+
const finish = () => {
|
|
73
|
+
if (settled)
|
|
74
|
+
return;
|
|
75
|
+
settled = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
socket.destroy();
|
|
78
|
+
resolve(received);
|
|
79
|
+
};
|
|
80
|
+
const timer = setTimeout(finish, read_timeout_ms);
|
|
81
|
+
socket.setEncoding('latin1');
|
|
82
|
+
socket.on('connect', () => socket.write(request_bytes));
|
|
83
|
+
socket.on('data', (chunk) => {
|
|
84
|
+
received += chunk;
|
|
85
|
+
});
|
|
86
|
+
socket.on('error', () => { }); // EPIPE/ECONNRESET on mid-write close is expected
|
|
87
|
+
socket.on('close', finish);
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* Count HTTP response status lines in a raw byte stream. Unanchored so a
|
|
91
|
+
* second (smuggled) response concatenated after the first body — which carries
|
|
92
|
+
* no trailing newline — is still counted; see `body_size_smuggling.ts`.
|
|
93
|
+
*/
|
|
94
|
+
const count_responses = (raw) => (raw.match(/HTTP\/1\.[01] \d{3}/g) ?? []).length;
|
|
95
|
+
/** The status code of the first HTTP response in a raw stream, or `null` if none. */
|
|
96
|
+
const first_status = (raw) => {
|
|
97
|
+
const match = /HTTP\/1\.[01] (\d{3})/.exec(raw);
|
|
98
|
+
return match ? Number(match[1]) : null;
|
|
99
|
+
};
|
|
100
|
+
const is_success_status = (status) => status >= 200 && status < 300;
|
|
101
|
+
/**
|
|
102
|
+
* Frame a raw HTTP/1.1 POST with the given extra header lines + JSON body.
|
|
103
|
+
* `Connection: close` so the server closes after one response (the response
|
|
104
|
+
* count is then unambiguous). The body is ASCII JSON, so `body.length` is the
|
|
105
|
+
* byte length — no `Buffer` needed.
|
|
106
|
+
*/
|
|
107
|
+
const rpc_post = (host, path, header_lines, body) => `POST ${path} HTTP/1.1\r\n` +
|
|
108
|
+
`Host: ${host}\r\n` +
|
|
109
|
+
`Content-Type: application/json\r\n` +
|
|
110
|
+
header_lines.map((h) => `${h}\r\n`).join('') +
|
|
111
|
+
`Content-Length: ${body.length}\r\n` +
|
|
112
|
+
`Connection: close\r\n` +
|
|
113
|
+
`\r\n` +
|
|
114
|
+
body;
|
|
115
|
+
/**
|
|
116
|
+
* A keeper-gated `account_purge` body with `confirm` OMITTED — the confirm
|
|
117
|
+
* guard (`purge_not_confirmed`, before any deletion) guarantees the
|
|
118
|
+
* valid-credential branch is a non-2xx with zero side effect, so the
|
|
119
|
+
* no-escalation assertion holds regardless of which duplicate header wins.
|
|
120
|
+
*/
|
|
121
|
+
const purge_body = (id) => JSON.stringify({ jsonrpc: '2.0', method: 'account_purge', id, params: { account_id: NIL_UUID } });
|
|
122
|
+
export const describe_credential_header_robustness_cross_tests = (options) => {
|
|
123
|
+
const { base_url, daemon_token } = options;
|
|
124
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
125
|
+
const host = new URL(base_url).host;
|
|
126
|
+
describe('credential-header robustness', () => {
|
|
127
|
+
test('duplicate X-Daemon-Token (valid + conflicting) never escalates → one non-2xx response', async () => {
|
|
128
|
+
const req = rpc_post(host, rpc_path, [`${DAEMON_TOKEN_HEADER}: ${daemon_token}`, `${DAEMON_TOKEN_HEADER}: ${GARBAGE_TOKEN}`], purge_body('dup-daemon'));
|
|
129
|
+
const raw = await send_raw(base_url, req, 2000);
|
|
130
|
+
assert.strictEqual(count_responses(raw), 1, `a duplicated X-Daemon-Token must yield exactly one response (no desync). Raw head: ${raw.slice(0, 120)}`);
|
|
131
|
+
const status = first_status(raw);
|
|
132
|
+
assert.ok(status !== null, `expected an HTTP response. Raw head: ${raw.slice(0, 120)}`);
|
|
133
|
+
assert.ok(!is_success_status(status), `a duplicated daemon token must not perform a keeper operation; whichever copy wins, ` +
|
|
134
|
+
`the result must be non-2xx (got ${status})`);
|
|
135
|
+
});
|
|
136
|
+
test('duplicate Authorization bearer headers (both garbage) do not authenticate → one non-2xx response', async () => {
|
|
137
|
+
const req = rpc_post(host, rpc_path, [`Authorization: Bearer ${GARBAGE_TOKEN}`, `Authorization: Bearer ${GARBAGE_TOKEN}-2`], purge_body('dup-bearer'));
|
|
138
|
+
const raw = await send_raw(base_url, req, 2000);
|
|
139
|
+
assert.strictEqual(count_responses(raw), 1, `duplicate Authorization headers must yield exactly one response (no desync). Raw head: ${raw.slice(0, 120)}`);
|
|
140
|
+
const status = first_status(raw);
|
|
141
|
+
assert.ok(status !== null, `expected an HTTP response. Raw head: ${raw.slice(0, 120)}`);
|
|
142
|
+
assert.ok(!is_success_status(status), `garbage bearer tokens (even duplicated) must not authenticate (got ${status})`);
|
|
143
|
+
});
|
|
144
|
+
test('control-char-injected credential header (CR/LF) cannot smuggle or authenticate', async () => {
|
|
145
|
+
// Embedded CRLF and a bare CR (header-splitting attempts) in a garbage
|
|
146
|
+
// `X-Daemon-Token` value. A strict HTTP parser rejects the request
|
|
147
|
+
// (400 / connection close); a lenient one truncates the value to a
|
|
148
|
+
// non-token or treats the injected line as an extra (harmless) header.
|
|
149
|
+
// Either way the framework-agnostic invariants hold: the injected bytes
|
|
150
|
+
// never frame a SECOND request (no smuggle), and the garbage credential
|
|
151
|
+
// never authenticates (no 2xx). `<= 1` (not `=== 1`) because a strict
|
|
152
|
+
// parser may close without a readable response — that's also "no smuggle".
|
|
153
|
+
const malformed = [
|
|
154
|
+
{ label: 'CRLF header-split', value: `${GARBAGE_TOKEN}\r\nX-Injected: 1` },
|
|
155
|
+
{ label: 'bare CR', value: `${GARBAGE_TOKEN}\rX-Injected: 1` },
|
|
156
|
+
];
|
|
157
|
+
for (const { label, value } of malformed) {
|
|
158
|
+
const req = rpc_post(host, rpc_path, [`${DAEMON_TOKEN_HEADER}: ${value}`], purge_body('ctrl-char'));
|
|
159
|
+
const raw = await send_raw(base_url, req, 2000);
|
|
160
|
+
assert.ok(count_responses(raw) <= 1, `${label}: a control-char-injected credential header must not frame a second ` +
|
|
161
|
+
`request (no smuggle). Raw head: ${raw.slice(0, 120)}`);
|
|
162
|
+
const status = first_status(raw);
|
|
163
|
+
assert.ok(status === null || !is_success_status(status), `${label}: a malformed credential header must never authenticate (got ${status})`);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
test('oversized credential header does not crash or wedge the server', async () => {
|
|
167
|
+
// 64 KiB — well past both frameworks' header-size caps, so the request is
|
|
168
|
+
// rejected (4xx / 431 / connection close). Its own outcome is don't-care.
|
|
169
|
+
const oversized = 'a'.repeat(64 * 1024);
|
|
170
|
+
const req = rpc_post(host, rpc_path, [`${DAEMON_TOKEN_HEADER}: ${oversized}`], purge_body('oversized'));
|
|
171
|
+
await send_raw(base_url, req, 2000);
|
|
172
|
+
// The property: the server survives it. A normal request on a fresh
|
|
173
|
+
// connection must still get a response (no crash, no wedged accept loop).
|
|
174
|
+
const probe = `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\nConnection: close\r\n\r\n`;
|
|
175
|
+
const raw = await send_raw(base_url, probe, 2000);
|
|
176
|
+
assert.ok(first_status(raw) !== null, `server must still respond after an oversized-header request (no crash / wedge). Raw head: ${raw.slice(0, 120)}`);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
};
|
|
@@ -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,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;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":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAuBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAmBtC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBAIrC,CAAC;AAeH,MAAM,WAAW,iCAAiC;IACjD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAoCF,CAAC;AAEF,MAAM,WAAW,mCAAmC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA4CF,CAAC"}
|
|
@@ -35,9 +35,11 @@ export const ts_default_capabilities = Object.freeze({
|
|
|
35
35
|
// posture). A Bun-served consumer overrides to `false` (see the bun spine
|
|
36
36
|
// config) — fail-loud rather than silently skipping the smuggle detector.
|
|
37
37
|
oversized_reject_closes_connection: true,
|
|
38
|
-
//
|
|
39
|
-
// `BackendWebsocketTransport.
|
|
40
|
-
//
|
|
38
|
+
// Off by default like `sse`/`ready`: the WS backend transport *can* initiate
|
|
39
|
+
// server→client requests (`BackendWebsocketTransport.request_connection`),
|
|
40
|
+
// but driving the full `peer/ping` suite also needs `peer/ping` mounted on
|
|
41
|
+
// the HTTP RPC endpoint (the no-transport case). The TS spine config opts in;
|
|
42
|
+
// a generic consumer enables it once it wires that mount.
|
|
41
43
|
peer_request: false,
|
|
42
44
|
});
|
|
43
45
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"full_spine_mount.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/full_spine_mount.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"full_spine_mount.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/full_spine_mount.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAE3D,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,8CAA8C,CAAC;AAKrF,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oCAAoC,CAAC;AAOzE,2FAA2F;AAC3F,MAAM,WAAW,qBAAqB;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CA0BjB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,GACpC,KAAK,gBAAgB,EACrB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,eAAe,CAEvB,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
|
+
import { peer_ping_action } from '../../actions/peer_ping.js';
|
|
2
3
|
import { create_standard_rpc_actions } from '../../auth/standard_rpc_actions.js';
|
|
3
4
|
import { create_all_cell_actions } from '../../auth/all_cell_actions.js';
|
|
4
5
|
import { create_actor_lookup_actions } from '../../auth/actor_lookup_actions.js';
|
|
@@ -29,6 +30,12 @@ export const build_full_spine_rpc_actions = (deps, options) => {
|
|
|
29
30
|
...create_all_cell_actions(deps, { roles: spine_roles }),
|
|
30
31
|
...create_actor_lookup_actions(deps),
|
|
31
32
|
...create_actor_search_actions(deps),
|
|
33
|
+
// `peer/ping` is mounted on the HTTP RPC endpoint too (not just WS) so an
|
|
34
|
+
// HTTP invocation reaches the handler and refuses with `peer_no_transport`
|
|
35
|
+
// rather than `method_not_found`. It's a protocol action, so it's filtered
|
|
36
|
+
// out of the action manifest (`create_testing_action_manifest_action`) —
|
|
37
|
+
// the WS endpoint registers it via the `protocol_actions` spread.
|
|
38
|
+
peer_ping_action,
|
|
32
39
|
];
|
|
33
40
|
// Append the `_testing_action_manifest` backdoor last — it closes over the
|
|
34
41
|
// complete `actions` list (plus its own spec) to dump the live method set
|