@fuzdev/fuz_app 0.140.0 → 0.142.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 (311) hide show
  1. package/dist/actions/CLAUDE.md +72 -22
  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 +44 -4
  6. package/dist/actions/connection_closer.d.ts.map +1 -1
  7. package/dist/actions/connection_closer.js +33 -3
  8. package/dist/actions/perform_action.d.ts +10 -0
  9. package/dist/actions/perform_action.d.ts.map +1 -1
  10. package/dist/actions/perform_action.js +23 -9
  11. package/dist/actions/register_action_ws.d.ts +42 -6
  12. package/dist/actions/register_action_ws.d.ts.map +1 -1
  13. package/dist/actions/register_action_ws.js +87 -19
  14. package/dist/actions/register_ws_endpoint.d.ts +15 -6
  15. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  16. package/dist/actions/register_ws_endpoint.js +10 -4
  17. package/dist/actions/socket.svelte.d.ts +19 -4
  18. package/dist/actions/socket.svelte.d.ts.map +1 -1
  19. package/dist/actions/socket.svelte.js +31 -4
  20. package/dist/actions/transports.d.ts +30 -1
  21. package/dist/actions/transports.d.ts.map +1 -1
  22. package/dist/actions/transports.js +30 -1
  23. package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
  24. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  25. package/dist/actions/transports_ws_auth_guard.js +24 -10
  26. package/dist/actions/transports_ws_backend.d.ts +51 -5
  27. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  28. package/dist/actions/transports_ws_backend.js +62 -9
  29. package/dist/actions/ws_endpoint_spec.d.ts +11 -10
  30. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  31. package/dist/auth/CLAUDE.md +44 -22
  32. package/dist/auth/account_actions.d.ts.map +1 -1
  33. package/dist/auth/account_actions.js +12 -1
  34. package/dist/auth/account_route_schema.d.ts +1 -0
  35. package/dist/auth/account_route_schema.d.ts.map +1 -1
  36. package/dist/auth/account_route_schema.js +12 -0
  37. package/dist/auth/account_routes.d.ts +7 -2
  38. package/dist/auth/account_routes.d.ts.map +1 -1
  39. package/dist/auth/account_routes.js +240 -187
  40. package/dist/auth/admin_action_specs.d.ts +6 -1
  41. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  42. package/dist/auth/admin_action_specs.js +6 -1
  43. package/dist/auth/admin_actions.d.ts.map +1 -1
  44. package/dist/auth/admin_actions.js +14 -2
  45. package/dist/auth/api_token.d.ts +20 -0
  46. package/dist/auth/api_token.d.ts.map +1 -1
  47. package/dist/auth/api_token.js +21 -1
  48. package/dist/auth/api_token_queries.d.ts +22 -4
  49. package/dist/auth/api_token_queries.d.ts.map +1 -1
  50. package/dist/auth/api_token_queries.js +11 -6
  51. package/dist/auth/audit_log_routes.d.ts +10 -0
  52. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  53. package/dist/auth/audit_log_routes.js +20 -0
  54. package/dist/auth/audit_log_schema.d.ts +30 -5
  55. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  56. package/dist/auth/audit_log_schema.js +30 -0
  57. package/dist/auth/bearer_auth.d.ts +4 -2
  58. package/dist/auth/bearer_auth.d.ts.map +1 -1
  59. package/dist/auth/bearer_auth.js +19 -8
  60. package/dist/auth/bootstrap_route_schema.d.ts +1 -0
  61. package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
  62. package/dist/auth/bootstrap_route_schema.js +1 -0
  63. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  64. package/dist/auth/bootstrap_routes.js +85 -76
  65. package/dist/auth/cell_action_specs.d.ts +7 -3
  66. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  67. package/dist/auth/cell_action_specs.js +9 -5
  68. package/dist/auth/cell_actions.d.ts +3 -3
  69. package/dist/auth/cell_actions.d.ts.map +1 -1
  70. package/dist/auth/cell_actions.js +67 -38
  71. package/dist/auth/cell_authorize.d.ts +69 -8
  72. package/dist/auth/cell_authorize.d.ts.map +1 -1
  73. package/dist/auth/cell_authorize.js +89 -9
  74. package/dist/auth/cell_field_action_specs.d.ts +5 -4
  75. package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
  76. package/dist/auth/cell_field_action_specs.js +7 -7
  77. package/dist/auth/cell_field_actions.d.ts +7 -5
  78. package/dist/auth/cell_field_actions.d.ts.map +1 -1
  79. package/dist/auth/cell_field_actions.js +24 -22
  80. package/dist/auth/cell_grant_action_specs.d.ts +6 -0
  81. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
  82. package/dist/auth/cell_grant_action_specs.js +6 -0
  83. package/dist/auth/cell_grant_actions.d.ts +3 -1
  84. package/dist/auth/cell_grant_actions.d.ts.map +1 -1
  85. package/dist/auth/cell_grant_actions.js +16 -2
  86. package/dist/auth/cell_item_action_specs.d.ts +5 -4
  87. package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
  88. package/dist/auth/cell_item_action_specs.js +7 -7
  89. package/dist/auth/cell_item_actions.d.ts +6 -5
  90. package/dist/auth/cell_item_actions.d.ts.map +1 -1
  91. package/dist/auth/cell_item_actions.js +23 -22
  92. package/dist/auth/cell_relation_list_limit.d.ts +20 -0
  93. package/dist/auth/cell_relation_list_limit.d.ts.map +1 -0
  94. package/dist/auth/cell_relation_list_limit.js +27 -0
  95. package/dist/auth/cell_relation_visibility.d.ts +4 -3
  96. package/dist/auth/cell_relation_visibility.d.ts.map +1 -1
  97. package/dist/auth/cell_relation_visibility.js +6 -5
  98. package/dist/auth/cell_steward.d.ts +45 -0
  99. package/dist/auth/cell_steward.d.ts.map +1 -0
  100. package/dist/auth/cell_steward.js +53 -0
  101. package/dist/auth/cleanup.d.ts +9 -9
  102. package/dist/auth/cleanup.js +9 -9
  103. package/dist/auth/credential_deadline.d.ts +95 -0
  104. package/dist/auth/credential_deadline.d.ts.map +1 -0
  105. package/dist/auth/credential_deadline.js +139 -0
  106. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  107. package/dist/auth/daemon_token_middleware.js +4 -1
  108. package/dist/auth/middleware.d.ts +90 -12
  109. package/dist/auth/middleware.d.ts.map +1 -1
  110. package/dist/auth/middleware.js +115 -16
  111. package/dist/auth/request_context.d.ts +8 -2
  112. package/dist/auth/request_context.d.ts.map +1 -1
  113. package/dist/auth/request_context.js +20 -4
  114. package/dist/auth/resolved_auth.d.ts +13 -2
  115. package/dist/auth/resolved_auth.d.ts.map +1 -1
  116. package/dist/auth/resolved_auth.js +5 -4
  117. package/dist/auth/role_grant_offer_action_specs.d.ts +5 -0
  118. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  119. package/dist/auth/role_grant_offer_action_specs.js +6 -1
  120. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  121. package/dist/auth/role_grant_offer_actions.js +23 -16
  122. package/dist/auth/role_grant_offer_queries.d.ts +8 -6
  123. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  124. package/dist/auth/role_grant_offer_queries.js +16 -8
  125. package/dist/auth/session_cookie.d.ts +4 -0
  126. package/dist/auth/session_cookie.d.ts.map +1 -1
  127. package/dist/auth/session_cookie.js +3 -2
  128. package/dist/auth/session_middleware.d.ts +1 -1
  129. package/dist/auth/session_middleware.d.ts.map +1 -1
  130. package/dist/auth/session_middleware.js +4 -1
  131. package/dist/auth/session_queries.d.ts +19 -2
  132. package/dist/auth/session_queries.d.ts.map +1 -1
  133. package/dist/auth/session_queries.js +8 -2
  134. package/dist/auth/signup_route_schema.d.ts.map +1 -1
  135. package/dist/auth/signup_route_schema.js +1 -0
  136. package/dist/auth/signup_routes.d.ts +4 -3
  137. package/dist/auth/signup_routes.d.ts.map +1 -1
  138. package/dist/auth/signup_routes.js +157 -134
  139. package/dist/auth/token_scope.d.ts +7 -1
  140. package/dist/auth/token_scope.d.ts.map +1 -1
  141. package/dist/auth/token_scope.js +8 -2
  142. package/dist/db/CLAUDE.md +32 -19
  143. package/dist/db/cell_ddl.d.ts +1 -2
  144. package/dist/db/cell_ddl.d.ts.map +1 -1
  145. package/dist/db/cell_ddl.js +1 -2
  146. package/dist/db/cell_field_queries.d.ts +20 -18
  147. package/dist/db/cell_field_queries.d.ts.map +1 -1
  148. package/dist/db/cell_field_queries.js +28 -22
  149. package/dist/db/cell_item_queries.d.ts +20 -14
  150. package/dist/db/cell_item_queries.d.ts.map +1 -1
  151. package/dist/db/cell_item_queries.js +28 -18
  152. package/dist/db/cell_moderation.d.ts +67 -0
  153. package/dist/db/cell_moderation.d.ts.map +1 -0
  154. package/dist/db/cell_moderation.js +66 -0
  155. package/dist/db/cell_queries.d.ts +44 -14
  156. package/dist/db/cell_queries.d.ts.map +1 -1
  157. package/dist/db/cell_queries.js +68 -45
  158. package/dist/db/cell_view_sql.d.ts +102 -0
  159. package/dist/db/cell_view_sql.d.ts.map +1 -0
  160. package/dist/db/cell_view_sql.js +112 -0
  161. package/dist/db/db_pg.d.ts +1 -1
  162. package/dist/db/db_pg.js +1 -1
  163. package/dist/hono_context.d.ts +23 -0
  164. package/dist/hono_context.d.ts.map +1 -1
  165. package/dist/hono_context.js +19 -0
  166. package/dist/http/CLAUDE.md +41 -15
  167. package/dist/http/auth_shape.d.ts +5 -2
  168. package/dist/http/auth_shape.d.ts.map +1 -1
  169. package/dist/http/auth_shape.js +5 -2
  170. package/dist/http/ip_canonical.d.ts +30 -3
  171. package/dist/http/ip_canonical.d.ts.map +1 -1
  172. package/dist/http/ip_canonical.js +50 -3
  173. package/dist/http/middleware_spec.d.ts +16 -0
  174. package/dist/http/middleware_spec.d.ts.map +1 -1
  175. package/dist/http/origin.d.ts +113 -18
  176. package/dist/http/origin.d.ts.map +1 -1
  177. package/dist/http/origin.js +247 -127
  178. package/dist/http/route_spec.d.ts +21 -0
  179. package/dist/http/route_spec.d.ts.map +1 -1
  180. package/dist/http/schema_helpers.d.ts +22 -0
  181. package/dist/http/schema_helpers.d.ts.map +1 -1
  182. package/dist/http/schema_helpers.js +24 -0
  183. package/dist/http/surface.d.ts +6 -1
  184. package/dist/http/surface.d.ts.map +1 -1
  185. package/dist/http/surface.js +12 -5
  186. package/dist/primitive_schemas.d.ts +10 -1
  187. package/dist/primitive_schemas.d.ts.map +1 -1
  188. package/dist/primitive_schemas.js +11 -2
  189. package/dist/rate_limiter.d.ts +179 -12
  190. package/dist/rate_limiter.d.ts.map +1 -1
  191. package/dist/rate_limiter.js +352 -42
  192. package/dist/realtime/sse_auth_guard.d.ts +22 -6
  193. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  194. package/dist/realtime/sse_auth_guard.js +31 -12
  195. package/dist/realtime/subscriber_registry.d.ts +10 -1
  196. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  197. package/dist/realtime/subscriber_registry.js +10 -1
  198. package/dist/server/app_server.d.ts +20 -7
  199. package/dist/server/app_server.d.ts.map +1 -1
  200. package/dist/server/app_server.js +8 -3
  201. package/dist/server/env.d.ts +2 -1
  202. package/dist/server/env.d.ts.map +1 -1
  203. package/dist/server/env.js +7 -17
  204. package/dist/server/serve_fact_route.d.ts +8 -4
  205. package/dist/server/serve_fact_route.d.ts.map +1 -1
  206. package/dist/server/serve_fact_route.js +16 -6
  207. package/dist/testing/CLAUDE.md +164 -25
  208. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  209. package/dist/testing/adversarial_headers.js +13 -6
  210. package/dist/testing/connection_closer_helpers.d.ts +6 -1
  211. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  212. package/dist/testing/connection_closer_helpers.js +4 -0
  213. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  214. package/dist/testing/cross_backend/account_lifecycle.js +37 -2
  215. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  216. package/dist/testing/cross_backend/body_size.js +14 -65
  217. package/dist/testing/cross_backend/capabilities.d.ts +26 -2
  218. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  219. package/dist/testing/cross_backend/capabilities.js +9 -3
  220. package/dist/testing/cross_backend/cell_gated_create.d.ts +38 -0
  221. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  222. package/dist/testing/cross_backend/cell_gated_create.js +401 -5
  223. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  224. package/dist/testing/cross_backend/cell_relations.js +145 -2
  225. package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
  226. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  227. package/dist/testing/cross_backend/conformance_case.js +6 -0
  228. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  229. package/dist/testing/cross_backend/conformance_table.js +20 -19
  230. package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
  231. package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
  232. package/dist/testing/cross_backend/credential_deadline.js +185 -0
  233. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  234. package/dist/testing/cross_backend/default_backend_configs.js +13 -2
  235. package/dist/testing/cross_backend/expected_schema.json +0 -1
  236. package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
  237. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  238. package/dist/testing/cross_backend/full_spine_mount.js +7 -1
  239. package/dist/testing/cross_backend/login_security.d.ts +16 -0
  240. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  241. package/dist/testing/cross_backend/login_security.js +154 -0
  242. package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
  243. package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
  244. package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
  245. package/dist/testing/cross_backend/origin.d.ts.map +1 -1
  246. package/dist/testing/cross_backend/origin.js +6 -26
  247. package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
  248. package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
  249. package/dist/testing/cross_backend/origin_conformance.js +136 -0
  250. package/dist/testing/cross_backend/origin_conformance.json +1679 -0
  251. package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
  252. package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
  253. package/dist/testing/cross_backend/origin_scope.js +140 -0
  254. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
  255. package/dist/testing/cross_backend/query_shape.js +24 -2
  256. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
  257. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
  258. package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
  259. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
  260. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  261. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
  262. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
  263. package/dist/testing/cross_backend/role_grant_participation.js +64 -0
  264. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  265. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
  266. package/dist/testing/cross_backend/spine_surface_constants.d.ts +20 -1
  267. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  268. package/dist/testing/cross_backend/spine_surface_constants.js +23 -1
  269. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  270. package/dist/testing/cross_backend/testing_backdoor.js +20 -7
  271. package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
  272. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  273. package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
  274. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  275. package/dist/testing/cross_backend/token_scope_surface.js +25 -21
  276. package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
  277. package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
  278. package/dist/testing/cross_backend/transport_composition.js +364 -0
  279. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  280. package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
  281. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
  282. package/dist/testing/cross_backend/ws_action_rate_limit.js +21 -1
  283. package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
  284. package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
  285. package/dist/testing/cross_backend/ws_role_gated.js +142 -0
  286. package/dist/testing/entities.d.ts +23 -0
  287. package/dist/testing/entities.d.ts.map +1 -1
  288. package/dist/testing/entities.js +37 -0
  289. package/dist/testing/integration.d.ts.map +1 -1
  290. package/dist/testing/integration.js +3 -1
  291. package/dist/testing/middleware.d.ts +10 -0
  292. package/dist/testing/middleware.d.ts.map +1 -1
  293. package/dist/testing/middleware.js +31 -7
  294. package/dist/testing/schema_introspect.js +1 -1
  295. package/dist/testing/schema_parity.d.ts +4 -3
  296. package/dist/testing/schema_parity.d.ts.map +1 -1
  297. package/dist/testing/schema_parity.js +4 -3
  298. package/dist/testing/schema_ready_fixture.d.ts +2 -4
  299. package/dist/testing/schema_ready_fixture.d.ts.map +1 -1
  300. package/dist/testing/schema_ready_fixture.js +2 -4
  301. package/dist/testing/stubs.d.ts +16 -3
  302. package/dist/testing/stubs.d.ts.map +1 -1
  303. package/dist/testing/stubs.js +20 -7
  304. package/dist/testing/surface_invariants.js +2 -2
  305. package/dist/ui/auth_state.svelte.d.ts +10 -0
  306. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  307. package/dist/ui/auth_state.svelte.js +22 -4
  308. package/package.json +1 -1
  309. package/dist/db/cell_history_ddl.d.ts +0 -38
  310. package/dist/db/cell_history_ddl.d.ts.map +0 -1
  311. package/dist/db/cell_history_ddl.js +0 -61
@@ -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
@@ -6,6 +6,22 @@ export interface LoginSecurityCrossTestOptions {
6
6
  readonly setup_test: SetupTest;
7
7
  /** REST login route path. Default `/api/account/login` (the spine convention). */
8
8
  readonly login_path?: string;
9
+ /** REST password-change route path. Default `/api/account/password`. */
10
+ readonly password_path?: string;
11
+ /**
12
+ * Whether the backend serves concurrent logins — each its own database
13
+ * transaction — at once, for the burst-of-correct-logins case. Default
14
+ * `false`: a backend on one in-process connection (pglet-wasm, PGlite)
15
+ * refuses a second top-level transaction while one runs, so concurrent
16
+ * successful logins answer `500` there whatever the limiter does.
17
+ */
18
+ readonly concurrent_transactions?: boolean;
19
+ /**
20
+ * The backend's base URL, for the repeated-`X-Forwarded-For` case, which
21
+ * needs a raw socket to send two header lines. `null` (the default) skips
22
+ * that case.
23
+ */
24
+ readonly base_url?: string | null;
9
25
  }
10
26
  export declare const describe_login_security_cross_tests: (options: LoginSecurityCrossTestOptions) => void;
11
27
  //# sourceMappingURL=login_security.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"login_security.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/login_security.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2D9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,mDAAmD;AACnD,MAAM,WAAW,6BAA6B;IAC7C,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AA0BD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IA2KF,CAAC"}
1
+ {"version":3,"file":"login_security.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/login_security.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAgF9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,mDAAmD;AACnD,MAAM,WAAW,6BAA6B;IAC7C,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAgDD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IA4UF,CAAC"}
@@ -24,6 +24,26 @@ import '../assert_dev_env.js';
24
24
  * not `429`). A backend that ignored XFF and keyed on the (loopback) TCP
25
25
  * peer would 429 the fresh-IP request too — so the `401` proves the limiter
26
26
  * keys on the resolved `X-Forwarded-For` client IP.
27
+ * - **IPv6 clients share a bucket per `/64`** — after one forwarded IPv6
28
+ * address exhausts its bucket, another address in the same `/64` is `429`
29
+ * and one in the next `/64` is not; the login audit rows keep each full
30
+ * address, never the bucket key. A password change, which shares the
31
+ * per-IP login limiter, from an address in an exhausted `/64` is `429` too.
32
+ * - **a concurrent burst gets at most the cap past the limiter** — `2 ×
33
+ * ip_limit` wrong logins sent at once from one fresh forwarded IP answer at
34
+ * most `ip_limit` `401`s and `429` for the rest. A limiter checked before
35
+ * the lookup and recorded after the verify lets the whole burst through; each
36
+ * attempt is reserved instead, counted in flight from the check on.
37
+ * - **a burst of correct logins past the cap waits, then succeeds** — attempts
38
+ * count while in flight whatever their outcome, so `2 × ip_limit` correct
39
+ * logins at once from one address meet the cap; the ones past it wait for a
40
+ * slot (at most the cap of them) and all answer `200`. Runs on a backend
41
+ * that serves concurrent transactions (`concurrent_transactions`).
42
+ * - **every `X-Forwarded-For` line is read** — a client that sends its own
43
+ * line ahead of the one the trusted proxy appended can't choose its bucket:
44
+ * after exhausting one forwarded IP, a raw request carrying a spoofed line
45
+ * first and that IP last still `429`s. Raw socket only (`fetch` merges
46
+ * repeated headers into one line), so it runs when `base_url` is given.
27
47
  * - **a success does not refund the per-IP budget** — the distributed-spray
28
48
  * backstop. Failed guesses against *distinct* usernames from one forwarded
29
49
  * IP, interleaved with the attacker's own successful logins, must still 429
@@ -50,7 +70,11 @@ import '../assert_dev_env.js';
50
70
  */
51
71
  import { describe, test, assert } from 'vitest';
52
72
  import { ERROR_RATE_LIMIT_EXCEEDED } from '../../http/error_schemas.js';
73
+ import { raw_http_exchange, raw_http_parse_response } from '../transports/raw_http.js';
53
74
  import { default_login_account_rate_limit, default_login_ip_rate_limit } from '../../rate_limiter.js';
75
+ import { AuditLogListOutput } from '../../auth/admin_action_specs.js';
76
+ import { cross_rpc_call, expect_output } from './cell_cross_helpers.js';
77
+ import { SPINE_RPC_PATH } from './spine_surface_constants.js';
54
78
  /**
55
79
  * RFC 5737 TEST-NET-3 (`203.0.113.0/24`) documentation addresses — guaranteed
56
80
  * non-routable, so they never collide with a real client IP. One per bucket the
@@ -63,11 +87,35 @@ const XFF_IP_LIMIT = '203.0.113.1';
63
87
  const XFF_SEGREGATION_EXHAUST = '203.0.113.2';
64
88
  const XFF_SEGREGATION_FRESH = '203.0.113.3';
65
89
  const XFF_SPRAY = '203.0.113.4';
90
+ const XFF_BURST = '203.0.113.5';
91
+ const XFF_CORRECT_BURST = '203.0.113.7';
92
+ const XFF_REPEATED_LINES = '203.0.113.6';
93
+ /** RFC 5737 TEST-NET-2 — the address a client writes into its own first line. */
94
+ const XFF_SPOOFED_LINE = '198.51.100.77';
95
+ /**
96
+ * RFC 3849 documentation (`2001:db8::/32`) addresses: two in one `/64` — the
97
+ * second differs past bit 64 only — and one in the next `/64`.
98
+ */
99
+ const XFF_V6_EXHAUST = '2001:db8:e3:1::1';
100
+ const XFF_V6_SAME_64 = '2001:db8:e3:1:8000::2';
101
+ const XFF_V6_OTHER_64 = '2001:db8:e3:2::1';
102
+ /**
103
+ * Two addresses in one more `/64`: login exhausts the first, and a password
104
+ * change — which reserves on the same per-IP login limiter — comes from the
105
+ * second.
106
+ */
107
+ const XFF_V6_PASSWORD_EXHAUST = '2001:db8:e3:5::1';
108
+ const XFF_V6_PASSWORD_SAME_64 = '2001:db8:e3:5:8000::2';
109
+ /** How long a raw exchange waits for the login's answer. */
110
+ const RAW_READ_TIMEOUT_MS = 5000;
66
111
  /** A non-routable, non-existent identifier — login resolves to the not-found path (records the bucket, returns 401). */
67
112
  const PROBE_PASSWORD = 'login_security_wrong_password';
68
113
  export const describe_login_security_cross_tests = (options) => {
69
114
  const { setup_test } = options;
70
115
  const login_path = options.login_path ?? '/api/account/login';
116
+ const password_path = options.password_path ?? '/api/account/password';
117
+ const concurrent_transactions = options.concurrent_transactions ?? false;
118
+ const base_url = options.base_url ?? null;
71
119
  /** Per-IP cap shared by both impls (TS `default_login_ip_rate_limit`, Rust `DEFAULT_LOGIN_IP_RATE_LIMIT`). */
72
120
  const ip_limit = default_login_ip_rate_limit.max_attempts;
73
121
  // The XFF-segregation case enables both limiters (one flag drives both), so
@@ -145,6 +193,112 @@ export const describe_login_security_cross_tests = (options) => {
145
193
  const fresh = await attempt(fixture, XFF_SEGREGATION_FRESH, 'probe_xff');
146
194
  assert.strictEqual(fresh.status, 401, "a fresh forwarded IP must not inherit another IP's limit");
147
195
  });
196
+ test('IPv6 clients share a bucket per /64, and the audit row keeps the full address', async () => {
197
+ const fixture = await setup_test();
198
+ for (let i = 0; i < ip_limit; i++) {
199
+ const outcome = await attempt(fixture, XFF_V6_EXHAUST, 'probe_v6');
200
+ assert.strictEqual(outcome.status, 401, `attempt ${i + 1} from ${XFF_V6_EXHAUST}`);
201
+ }
202
+ // another address in the same /64 draws the same budget
203
+ const same = await attempt(fixture, XFF_V6_SAME_64, 'probe_v6');
204
+ assert.strictEqual(same.status, 429, 'an address in the same /64 must be rate-limited');
205
+ // the next /64 has its own
206
+ const other = await attempt(fixture, XFF_V6_OTHER_64, 'probe_v6');
207
+ assert.strictEqual(other.status, 401, 'the next /64 must keep its own budget');
208
+ // The limiter keys the /64; the audit trail keeps the address. Drain
209
+ // the fire-and-forget audit writes, then read this test's login rows
210
+ // (`_testing_reset` wiped the log at setup).
211
+ const drained = await cross_rpc_call(fixture.fresh_transport({ origin: null }), SPINE_RPC_PATH, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
212
+ assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
213
+ const listed = expect_output(await cross_rpc_call(fixture.transport, SPINE_RPC_PATH, 'audit_log_list', { event_type: 'login', outcome: 'failure' }, fixture.create_session_headers()), AuditLogListOutput);
214
+ const ips = listed.events.map((e) => e.ip);
215
+ assert.ok(ips.includes(XFF_V6_EXHAUST), `audit ips: ${JSON.stringify(ips)}`);
216
+ assert.ok(ips.includes(XFF_V6_OTHER_64), `audit ips: ${JSON.stringify(ips)}`);
217
+ assert.ok(ips.every((ip) => ip === null || !ip.includes('/')), `no audit row may carry a rate-limit key: ${JSON.stringify(ips)}`);
218
+ });
219
+ test('a password change from the /64 an exhausted IPv6 login shares is rate-limited', async () => {
220
+ const fixture = await setup_test();
221
+ for (let i = 0; i < ip_limit; i++) {
222
+ const outcome = await attempt(fixture, XFF_V6_PASSWORD_EXHAUST, 'probe_v6_password');
223
+ assert.strictEqual(outcome.status, 401, `attempt ${i + 1} from ${XFF_V6_PASSWORD_EXHAUST}`);
224
+ }
225
+ // The keeper's session, from another address in the same /64: the
226
+ // password route reserves on the per-IP login limiter before it
227
+ // reads the credential or the body, so the /64's spent budget
228
+ // answers. Keyed on the full address instead, the wrong
229
+ // `current_password` would answer `401`.
230
+ const res = await fixture.fresh_transport()(password_path, {
231
+ method: 'POST',
232
+ headers: fixture.create_session_headers({
233
+ 'content-type': 'application/json',
234
+ 'x-forwarded-for': XFF_V6_PASSWORD_SAME_64
235
+ }),
236
+ body: JSON.stringify({
237
+ current_password: PROBE_PASSWORD,
238
+ new_password: 'login_security_new_password'
239
+ })
240
+ });
241
+ const body = (await res.json().catch(() => undefined));
242
+ assert.strictEqual(res.status, 429, `password change answered ${JSON.stringify(body)}`);
243
+ assert.strictEqual(body?.error, ERROR_RATE_LIMIT_EXCEEDED);
244
+ });
245
+ test(`a concurrent burst of ${ip_limit * 2} wrong logins gets at most ${ip_limit} past the per-IP limit`, async () => {
246
+ const fixture = await setup_test();
247
+ // All sent at once, so every request reaches the limiter before any
248
+ // has been answered. Its own username keeps the per-account bucket
249
+ // (cap above the IP cap, asserted above) out of the count.
250
+ const outcomes = await Promise.all(Array.from({ length: ip_limit * 2 }, () => attempt(fixture, XFF_BURST, 'probe_burst')));
251
+ const statuses = outcomes.map((o) => o.status);
252
+ const refused = statuses.filter((status) => status === 401).length;
253
+ const limited = statuses.filter((status) => status === 429).length;
254
+ // Exactly the cap: every admitted attempt fails and records, and the
255
+ // ones past it — waiting for a slot — are refused as those failures
256
+ // record, so the count is the same however the burst interleaves.
257
+ assert.strictEqual(refused, ip_limit, `exactly ${ip_limit} may reach the verify (${statuses})`);
258
+ assert.strictEqual(refused + limited, ip_limit * 2, `the rest must be 429 (${statuses})`);
259
+ // And the window is spent: the burst can't have left budget behind.
260
+ const after = await attempt(fixture, XFF_BURST, 'probe_burst');
261
+ assert.strictEqual(after.status, 429, 'the burst must leave the per-IP bucket full');
262
+ });
263
+ test.skipIf(!concurrent_transactions)(`a concurrent burst of ${ip_limit * 2} correct logins all succeed past the per-IP cap`, async () => {
264
+ const fixture = await setup_test();
265
+ const account = await fixture.create_account({
266
+ username: 'burst_correct',
267
+ password_value: 'burst_correct_password'
268
+ });
269
+ const statuses = await Promise.all(Array.from({ length: ip_limit * 2 }, () => succeed(fixture, XFF_CORRECT_BURST, account.account.username, 'burst_correct_password')));
270
+ assert.deepStrictEqual(statuses, Array(ip_limit * 2).fill(200), 'the logins past the cap wait for a slot rather than being refused');
271
+ });
272
+ test.skipIf(base_url === null)('every X-Forwarded-For line is read (a spoofed first line picks no bucket)', async () => {
273
+ assert.ok(base_url !== null);
274
+ const fixture = await setup_test();
275
+ for (let i = 0; i < ip_limit; i++) {
276
+ await attempt(fixture, XFF_REPEATED_LINES, 'probe_xff_lines');
277
+ }
278
+ const exhausted = await attempt(fixture, XFF_REPEATED_LINES, 'probe_xff_lines');
279
+ assert.strictEqual(exhausted.status, 429, 'the forwarded IP must be exhausted first');
280
+ // The client's own line first, the address the proxy appended last.
281
+ // A backend that read only the first line would key this on the
282
+ // fresh spoofed address and answer 401.
283
+ const url = new URL(base_url);
284
+ const body = JSON.stringify({ username: 'probe_xff_lines', password: PROBE_PASSWORD });
285
+ const request = `POST ${login_path} HTTP/1.1\r\n` +
286
+ `host: ${url.host}\r\n` +
287
+ `content-type: application/json\r\n` +
288
+ `x-forwarded-for: ${XFF_SPOOFED_LINE}\r\n` +
289
+ `x-forwarded-for: ${XFF_REPEATED_LINES}\r\n` +
290
+ `content-length: ${body.length}\r\n` +
291
+ `connection: close\r\n` +
292
+ `\r\n` +
293
+ body;
294
+ const { received } = await raw_http_exchange(base_url, request, {
295
+ read_timeout_ms: RAW_READ_TIMEOUT_MS,
296
+ until: 'response'
297
+ });
298
+ const response = raw_http_parse_response(received);
299
+ assert.ok(response, `no whole response; received: ${received.slice(0, 200)}`);
300
+ assert.strictEqual(response.status, 429, `the request must key on the proxy-appended line (body: ${response.body})`);
301
+ });
148
302
  test('a success does not refund the per-IP budget (distributed-spray backstop)', async () => {
149
303
  const fixture = await setup_test();
150
304
  // The attacker's own account. Minting rides the setup path from
@@ -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
@@ -0,0 +1,152 @@
1
+ import '../assert_dev_env.js';
2
+ import { z } from 'zod';
3
+ import type { RpcPathCrossSuiteOptions } from './setup.js';
4
+ import { TestingOriginCheckOutput, type TestingOriginCheckInput } from './testing_reset_actions.js';
5
+ /** One expected refusal of a parse row. */
6
+ export declare const OriginConformanceError: z.ZodObject<{
7
+ index: z.ZodOptional<z.ZodNumber>;
8
+ kind: z.ZodEnum<{
9
+ empty_entry: "empty_entry";
10
+ not_printable_ascii: "not_printable_ascii";
11
+ missing_scheme: "missing_scheme";
12
+ unsupported_scheme: "unsupported_scheme";
13
+ has_path: "has_path";
14
+ has_userinfo: "has_userinfo";
15
+ missing_host: "missing_host";
16
+ invalid_host_char: "invalid_host_char";
17
+ unbracketed_ipv6: "unbracketed_ipv6";
18
+ partial_wildcard: "partial_wildcard";
19
+ non_canonical_ipv4: "non_canonical_ipv4";
20
+ unclosed_ipv6: "unclosed_ipv6";
21
+ invalid_ipv6: "invalid_ipv6";
22
+ invalid_after_ipv6: "invalid_after_ipv6";
23
+ invalid_port: "invalid_port";
24
+ default_port: "default_port";
25
+ no_origins: "no_origins";
26
+ }>;
27
+ }, z.core.$strict>;
28
+ export type OriginConformanceError = z.infer<typeof OriginConformanceError>;
29
+ /** A parse row: the value parses (`ok: true`), or is refused with `errors`. */
30
+ export declare const OriginConformanceParseRow: z.ZodUnion<readonly [z.ZodObject<{
31
+ raw: z.ZodString;
32
+ ok: z.ZodLiteral<true>;
33
+ note: z.ZodOptional<z.ZodString>;
34
+ }, z.core.$strict>, z.ZodObject<{
35
+ raw: z.ZodString;
36
+ errors: z.ZodArray<z.ZodObject<{
37
+ index: z.ZodOptional<z.ZodNumber>;
38
+ kind: z.ZodEnum<{
39
+ empty_entry: "empty_entry";
40
+ not_printable_ascii: "not_printable_ascii";
41
+ missing_scheme: "missing_scheme";
42
+ unsupported_scheme: "unsupported_scheme";
43
+ has_path: "has_path";
44
+ has_userinfo: "has_userinfo";
45
+ missing_host: "missing_host";
46
+ invalid_host_char: "invalid_host_char";
47
+ unbracketed_ipv6: "unbracketed_ipv6";
48
+ partial_wildcard: "partial_wildcard";
49
+ non_canonical_ipv4: "non_canonical_ipv4";
50
+ unclosed_ipv6: "unclosed_ipv6";
51
+ invalid_ipv6: "invalid_ipv6";
52
+ invalid_after_ipv6: "invalid_after_ipv6";
53
+ invalid_port: "invalid_port";
54
+ default_port: "default_port";
55
+ no_origins: "no_origins";
56
+ }>;
57
+ }, z.core.$strict>>;
58
+ note: z.ZodOptional<z.ZodString>;
59
+ }, z.core.$strict>]>;
60
+ export type OriginConformanceParseRow = z.infer<typeof OriginConformanceParseRow>;
61
+ /** A match row: a valid allowlist, one `Origin` value or `null`, and the answer. */
62
+ export declare const OriginConformanceMatchRow: z.ZodObject<{
63
+ patterns: z.ZodString;
64
+ origin: z.ZodNullable<z.ZodString>;
65
+ allowed: z.ZodBoolean;
66
+ note: z.ZodOptional<z.ZodString>;
67
+ }, z.core.$strict>;
68
+ export type OriginConformanceMatchRow = z.infer<typeof OriginConformanceMatchRow>;
69
+ /** The conformance table. */
70
+ export declare const OriginConformance: z.ZodObject<{
71
+ parse: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
72
+ raw: z.ZodString;
73
+ ok: z.ZodLiteral<true>;
74
+ note: z.ZodOptional<z.ZodString>;
75
+ }, z.core.$strict>, z.ZodObject<{
76
+ raw: z.ZodString;
77
+ errors: z.ZodArray<z.ZodObject<{
78
+ index: z.ZodOptional<z.ZodNumber>;
79
+ kind: z.ZodEnum<{
80
+ empty_entry: "empty_entry";
81
+ not_printable_ascii: "not_printable_ascii";
82
+ missing_scheme: "missing_scheme";
83
+ unsupported_scheme: "unsupported_scheme";
84
+ has_path: "has_path";
85
+ has_userinfo: "has_userinfo";
86
+ missing_host: "missing_host";
87
+ invalid_host_char: "invalid_host_char";
88
+ unbracketed_ipv6: "unbracketed_ipv6";
89
+ partial_wildcard: "partial_wildcard";
90
+ non_canonical_ipv4: "non_canonical_ipv4";
91
+ unclosed_ipv6: "unclosed_ipv6";
92
+ invalid_ipv6: "invalid_ipv6";
93
+ invalid_after_ipv6: "invalid_after_ipv6";
94
+ invalid_port: "invalid_port";
95
+ default_port: "default_port";
96
+ no_origins: "no_origins";
97
+ }>;
98
+ }, z.core.$strict>>;
99
+ note: z.ZodOptional<z.ZodString>;
100
+ }, z.core.$strict>]>>;
101
+ match: z.ZodArray<z.ZodObject<{
102
+ patterns: z.ZodString;
103
+ origin: z.ZodNullable<z.ZodString>;
104
+ allowed: z.ZodBoolean;
105
+ note: z.ZodOptional<z.ZodString>;
106
+ }, z.core.$strict>>;
107
+ }, z.core.$strict>;
108
+ export type OriginConformance = z.infer<typeof OriginConformance>;
109
+ /** The committed table, resolved relative to this module. */
110
+ export declare const ORIGIN_CONFORMANCE_URL: URL;
111
+ /**
112
+ * Read and validate the committed conformance table.
113
+ *
114
+ * @returns the parsed table
115
+ * @throws Error when the file doesn't match `OriginConformance`
116
+ */
117
+ export declare const load_origin_conformance: () => OriginConformance;
118
+ /** One table row as the action input that checks it. */
119
+ export interface OriginConformanceCheck {
120
+ /** A short label naming the row, for a failure message. */
121
+ readonly label: string;
122
+ readonly input: TestingOriginCheckInput;
123
+ readonly expected: TestingOriginCheckOutput;
124
+ }
125
+ /**
126
+ * Flatten the table into action inputs and the answers they must get. A parse
127
+ * row sends a `null` origin, so a value that parses answers
128
+ * `{ok: true, allowed: true}`.
129
+ *
130
+ * @param table - the conformance table
131
+ * @returns one check per row, parse rows first
132
+ */
133
+ export declare const to_origin_conformance_checks: (table: OriginConformance) => Array<OriginConformanceCheck>;
134
+ /**
135
+ * Compare each check's answer with its expectation and fail once, listing
136
+ * every row that disagrees.
137
+ *
138
+ * @param checks - the table's checks
139
+ * @param answer - the answer each check got, in order
140
+ * @param backend - the name the failure message gives the answerer
141
+ */
142
+ export declare const assert_origin_conformance: (checks: ReadonlyArray<OriginConformanceCheck>, answer: ReadonlyArray<unknown>, backend: string) => void;
143
+ /** Options for `describe_origin_conformance_cross_tests`. */
144
+ export type OriginConformanceCrossTestOptions = RpcPathCrossSuiteOptions;
145
+ /**
146
+ * Register the origin conformance suite: every row of the committed table,
147
+ * sent to the backend's `_testing_origin_check` action over the keeper's
148
+ * daemon-token channel, must get the answer the table names. The action is on
149
+ * every spine test binary, so the suite is ungated.
150
+ */
151
+ export declare const describe_origin_conformance_cross_tests: (options: OriginConformanceCrossTestOptions) => void;
152
+ //# sourceMappingURL=origin_conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"origin_conformance.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/origin_conformance.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0B9B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE3D,OAAO,EACN,wBAAwB,EAExB,KAAK,uBAAuB,EAC5B,MAAM,4BAA4B,CAAC;AAEpC,2CAA2C;AAC3C,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;kBAGjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAOpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,oFAAoF;AACpF,eAAO,MAAM,yBAAyB;;;;;kBAKpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAElF,6BAA6B;AAC7B,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAG5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,6DAA6D;AAC7D,eAAO,MAAM,sBAAsB,EAAE,GAA2D,CAAC;AAEjG;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,iBACuC,CAAC;AAEnF,wDAAwD;AACxD,MAAM,WAAW,sBAAsB;IACtC,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,uBAAuB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;CAC5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,GACxC,OAAO,iBAAiB,KACtB,KAAK,CAAC,sBAAsB,CAc9B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,GACrC,QAAQ,aAAa,CAAC,sBAAsB,CAAC,EAC7C,QAAQ,aAAa,CAAC,OAAO,CAAC,EAC9B,SAAS,MAAM,KACb,IAiBF,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,iCAAiC,GAAG,wBAAwB,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,uCAAuC,GACnD,SAAS,iCAAiC,KACxC,IA4BF,CAAC"}