@fuzdev/fuz_app 0.140.0 → 0.141.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/dist/actions/CLAUDE.md +62 -17
  2. package/dist/actions/action_rpc.d.ts +5 -1
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +9 -1
  5. package/dist/actions/connection_closer.d.ts +25 -2
  6. package/dist/actions/connection_closer.d.ts.map +1 -1
  7. package/dist/actions/connection_closer.js +8 -1
  8. package/dist/actions/register_action_ws.d.ts +42 -6
  9. package/dist/actions/register_action_ws.d.ts.map +1 -1
  10. package/dist/actions/register_action_ws.js +87 -19
  11. package/dist/actions/register_ws_endpoint.d.ts +15 -6
  12. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  13. package/dist/actions/register_ws_endpoint.js +5 -2
  14. package/dist/actions/socket.svelte.d.ts +19 -4
  15. package/dist/actions/socket.svelte.d.ts.map +1 -1
  16. package/dist/actions/socket.svelte.js +31 -4
  17. package/dist/actions/transports.d.ts +30 -1
  18. package/dist/actions/transports.d.ts.map +1 -1
  19. package/dist/actions/transports.js +30 -1
  20. package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
  21. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  22. package/dist/actions/transports_ws_auth_guard.js +24 -10
  23. package/dist/actions/transports_ws_backend.d.ts +51 -5
  24. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  25. package/dist/actions/transports_ws_backend.js +62 -9
  26. package/dist/actions/ws_endpoint_spec.d.ts +11 -10
  27. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  28. package/dist/auth/CLAUDE.md +11 -9
  29. package/dist/auth/account_route_schema.d.ts +1 -0
  30. package/dist/auth/account_route_schema.d.ts.map +1 -1
  31. package/dist/auth/account_route_schema.js +4 -0
  32. package/dist/auth/api_token.d.ts +20 -0
  33. package/dist/auth/api_token.d.ts.map +1 -1
  34. package/dist/auth/api_token.js +21 -1
  35. package/dist/auth/api_token_queries.d.ts +22 -4
  36. package/dist/auth/api_token_queries.d.ts.map +1 -1
  37. package/dist/auth/api_token_queries.js +11 -6
  38. package/dist/auth/audit_log_routes.d.ts +10 -0
  39. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  40. package/dist/auth/audit_log_routes.js +20 -0
  41. package/dist/auth/audit_log_schema.d.ts +30 -5
  42. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  43. package/dist/auth/audit_log_schema.js +30 -0
  44. package/dist/auth/bearer_auth.d.ts +4 -2
  45. package/dist/auth/bearer_auth.d.ts.map +1 -1
  46. package/dist/auth/bearer_auth.js +19 -8
  47. package/dist/auth/bootstrap_route_schema.d.ts +1 -0
  48. package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
  49. package/dist/auth/bootstrap_route_schema.js +1 -0
  50. package/dist/auth/cleanup.d.ts +9 -9
  51. package/dist/auth/cleanup.js +9 -9
  52. package/dist/auth/credential_deadline.d.ts +95 -0
  53. package/dist/auth/credential_deadline.d.ts.map +1 -0
  54. package/dist/auth/credential_deadline.js +139 -0
  55. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  56. package/dist/auth/daemon_token_middleware.js +4 -1
  57. package/dist/auth/middleware.d.ts +90 -12
  58. package/dist/auth/middleware.d.ts.map +1 -1
  59. package/dist/auth/middleware.js +115 -16
  60. package/dist/auth/request_context.d.ts +4 -2
  61. package/dist/auth/request_context.d.ts.map +1 -1
  62. package/dist/auth/request_context.js +14 -3
  63. package/dist/auth/resolved_auth.d.ts +13 -2
  64. package/dist/auth/resolved_auth.d.ts.map +1 -1
  65. package/dist/auth/resolved_auth.js +5 -4
  66. package/dist/auth/session_cookie.d.ts +4 -0
  67. package/dist/auth/session_cookie.d.ts.map +1 -1
  68. package/dist/auth/session_cookie.js +3 -2
  69. package/dist/auth/session_middleware.d.ts +1 -1
  70. package/dist/auth/session_middleware.d.ts.map +1 -1
  71. package/dist/auth/session_middleware.js +4 -1
  72. package/dist/auth/session_queries.d.ts +19 -2
  73. package/dist/auth/session_queries.d.ts.map +1 -1
  74. package/dist/auth/session_queries.js +8 -2
  75. package/dist/auth/signup_route_schema.d.ts.map +1 -1
  76. package/dist/auth/signup_route_schema.js +1 -0
  77. package/dist/hono_context.d.ts +23 -0
  78. package/dist/hono_context.d.ts.map +1 -1
  79. package/dist/hono_context.js +19 -0
  80. package/dist/http/CLAUDE.md +41 -15
  81. package/dist/http/middleware_spec.d.ts +16 -0
  82. package/dist/http/middleware_spec.d.ts.map +1 -1
  83. package/dist/http/origin.d.ts +113 -18
  84. package/dist/http/origin.d.ts.map +1 -1
  85. package/dist/http/origin.js +247 -127
  86. package/dist/http/route_spec.d.ts +21 -0
  87. package/dist/http/route_spec.d.ts.map +1 -1
  88. package/dist/http/schema_helpers.d.ts +22 -0
  89. package/dist/http/schema_helpers.d.ts.map +1 -1
  90. package/dist/http/schema_helpers.js +24 -0
  91. package/dist/http/surface.d.ts +6 -1
  92. package/dist/http/surface.d.ts.map +1 -1
  93. package/dist/http/surface.js +12 -5
  94. package/dist/realtime/sse_auth_guard.d.ts +22 -6
  95. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  96. package/dist/realtime/sse_auth_guard.js +31 -12
  97. package/dist/realtime/subscriber_registry.d.ts +10 -1
  98. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  99. package/dist/realtime/subscriber_registry.js +10 -1
  100. package/dist/server/app_server.d.ts +20 -7
  101. package/dist/server/app_server.d.ts.map +1 -1
  102. package/dist/server/app_server.js +8 -3
  103. package/dist/server/env.d.ts +2 -1
  104. package/dist/server/env.d.ts.map +1 -1
  105. package/dist/server/env.js +7 -17
  106. package/dist/server/serve_fact_route.d.ts.map +1 -1
  107. package/dist/server/serve_fact_route.js +1 -0
  108. package/dist/testing/CLAUDE.md +154 -22
  109. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  110. package/dist/testing/adversarial_headers.js +13 -6
  111. package/dist/testing/connection_closer_helpers.d.ts +6 -1
  112. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  113. package/dist/testing/connection_closer_helpers.js +4 -0
  114. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  115. package/dist/testing/cross_backend/body_size.js +14 -65
  116. package/dist/testing/cross_backend/capabilities.d.ts +26 -2
  117. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  118. package/dist/testing/cross_backend/capabilities.js +9 -3
  119. package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
  120. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  121. package/dist/testing/cross_backend/conformance_case.js +6 -0
  122. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  123. package/dist/testing/cross_backend/conformance_table.js +20 -19
  124. package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
  125. package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
  126. package/dist/testing/cross_backend/credential_deadline.js +185 -0
  127. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/default_backend_configs.js +13 -2
  129. package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
  130. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  131. package/dist/testing/cross_backend/full_spine_mount.js +7 -1
  132. package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
  133. package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
  135. package/dist/testing/cross_backend/origin.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/origin.js +6 -26
  137. package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
  138. package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
  139. package/dist/testing/cross_backend/origin_conformance.js +136 -0
  140. package/dist/testing/cross_backend/origin_conformance.json +1679 -0
  141. package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
  142. package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/origin_scope.js +140 -0
  144. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
  145. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
  147. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
  148. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
  150. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
  152. package/dist/testing/cross_backend/spine_surface_constants.d.ts +19 -0
  153. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  154. package/dist/testing/cross_backend/spine_surface_constants.js +22 -0
  155. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  156. package/dist/testing/cross_backend/testing_backdoor.js +20 -7
  157. package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
  158. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  159. package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
  160. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  161. package/dist/testing/cross_backend/token_scope_surface.js +25 -21
  162. package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
  163. package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
  164. package/dist/testing/cross_backend/transport_composition.js +364 -0
  165. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  166. package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
  167. package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
  168. package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
  169. package/dist/testing/cross_backend/ws_role_gated.js +142 -0
  170. package/dist/testing/entities.d.ts +23 -0
  171. package/dist/testing/entities.d.ts.map +1 -1
  172. package/dist/testing/entities.js +37 -0
  173. package/dist/testing/integration.d.ts.map +1 -1
  174. package/dist/testing/integration.js +3 -1
  175. package/dist/testing/middleware.d.ts +10 -0
  176. package/dist/testing/middleware.d.ts.map +1 -1
  177. package/dist/testing/middleware.js +31 -7
  178. package/dist/testing/stubs.d.ts +16 -3
  179. package/dist/testing/stubs.d.ts.map +1 -1
  180. package/dist/testing/stubs.js +20 -7
  181. package/dist/testing/surface_invariants.js +2 -2
  182. package/package.json +1 -1
@@ -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), and
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;EAa/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"}
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,EAMN,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;AAGzD;;;;;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;AAqPD;;;;;;;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"}
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, rpc_call_for_spec, resolve_rpc_endpoints_for_setup } from '../rpc_helpers.js';
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
- // Spec-bound rather than a loose `rpc_call`: `params` type-checks against
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
- path: found.path,
71
- spec: account_token_create_action_spec,
72
- params: {
73
- name: 'conformance scoped token',
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":"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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;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;AAYvF,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,CAoCjB,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"}
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;AA+B9B,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,IAwC7E,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
- * returns a flat REST `{error}` body — not a JSON-RPC envelope — so it
7
- * doesn't fit the envelope-shaped conformance-table runner. This dedicated
8
- * imperative suite drives raw transport calls instead, mirroring how the
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