@fuzdev/fuz_app 0.110.1 → 0.112.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 (210) hide show
  1. package/dist/actions/CLAUDE.md +15 -11
  2. package/dist/actions/action_rpc.d.ts +3 -3
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +4 -6
  5. package/dist/actions/peer_ping.d.ts +8 -0
  6. package/dist/actions/peer_ping.d.ts.map +1 -1
  7. package/dist/actions/peer_ping.js +8 -0
  8. package/dist/actions/peer_request.d.ts +1 -1
  9. package/dist/actions/peer_request.js +1 -1
  10. package/dist/actions/perform_action.d.ts +16 -9
  11. package/dist/actions/perform_action.d.ts.map +1 -1
  12. package/dist/actions/perform_action.js +30 -21
  13. package/dist/actions/register_action_ws.d.ts +1 -1
  14. package/dist/actions/register_action_ws.d.ts.map +1 -1
  15. package/dist/actions/register_action_ws.js +88 -21
  16. package/dist/auth/CLAUDE.md +59 -10
  17. package/dist/auth/account_action_specs.d.ts +0 -2
  18. package/dist/auth/account_action_specs.d.ts.map +1 -1
  19. package/dist/auth/account_actions.d.ts.map +1 -1
  20. package/dist/auth/account_actions.js +10 -1
  21. package/dist/auth/account_queries.d.ts +22 -0
  22. package/dist/auth/account_queries.d.ts.map +1 -1
  23. package/dist/auth/account_queries.js +70 -16
  24. package/dist/auth/account_schema.d.ts +27 -9
  25. package/dist/auth/account_schema.d.ts.map +1 -1
  26. package/dist/auth/account_schema.js +23 -3
  27. package/dist/auth/actor_search_actions.d.ts +3 -3
  28. package/dist/auth/actor_search_actions.d.ts.map +1 -1
  29. package/dist/auth/actor_search_actions.js +10 -6
  30. package/dist/auth/admin_action_specs.d.ts +0 -2
  31. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  32. package/dist/auth/api_token_queries.d.ts +9 -4
  33. package/dist/auth/api_token_queries.d.ts.map +1 -1
  34. package/dist/auth/api_token_queries.js +36 -8
  35. package/dist/auth/app_settings_queries.d.ts +8 -0
  36. package/dist/auth/app_settings_queries.d.ts.map +1 -1
  37. package/dist/auth/app_settings_queries.js +16 -3
  38. package/dist/auth/audit_log_queries.d.ts +21 -3
  39. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  40. package/dist/auth/audit_log_queries.js +38 -7
  41. package/dist/auth/audit_log_schema.d.ts +0 -1
  42. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  43. package/dist/auth/auth_ddl.d.ts +6 -0
  44. package/dist/auth/auth_ddl.d.ts.map +1 -1
  45. package/dist/auth/auth_ddl.js +9 -0
  46. package/dist/auth/cell_actions.d.ts.map +1 -1
  47. package/dist/auth/cell_actions.js +3 -8
  48. package/dist/auth/cell_audit_actions.d.ts.map +1 -1
  49. package/dist/auth/cell_audit_actions.js +1 -1
  50. package/dist/auth/cell_field_actions.js +1 -1
  51. package/dist/auth/cell_grant_actions.js +1 -1
  52. package/dist/auth/cell_item_actions.js +1 -1
  53. package/dist/auth/invite_queries.d.ts +10 -0
  54. package/dist/auth/invite_queries.d.ts.map +1 -1
  55. package/dist/auth/invite_queries.js +28 -7
  56. package/dist/auth/migrations.d.ts.map +1 -1
  57. package/dist/auth/migrations.js +10 -0
  58. package/dist/auth/role_grant_offer_ddl.d.ts +34 -2
  59. package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
  60. package/dist/auth/role_grant_offer_ddl.js +61 -2
  61. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  62. package/dist/auth/role_grant_offer_queries.js +48 -53
  63. package/dist/auth/role_grant_queries.d.ts +18 -0
  64. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  65. package/dist/auth/role_grant_queries.js +58 -18
  66. package/dist/auth/session_queries.d.ts +12 -3
  67. package/dist/auth/session_queries.d.ts.map +1 -1
  68. package/dist/auth/session_queries.js +26 -6
  69. package/dist/db/CLAUDE.md +34 -0
  70. package/dist/db/cell_audit_queries.d.ts.map +1 -1
  71. package/dist/db/cell_audit_queries.js +3 -1
  72. package/dist/db/cell_field_queries.d.ts +12 -3
  73. package/dist/db/cell_field_queries.d.ts.map +1 -1
  74. package/dist/db/cell_field_queries.js +25 -7
  75. package/dist/db/cell_grant_queries.d.ts +10 -2
  76. package/dist/db/cell_grant_queries.d.ts.map +1 -1
  77. package/dist/db/cell_grant_queries.js +29 -11
  78. package/dist/db/cell_item_queries.d.ts +13 -2
  79. package/dist/db/cell_item_queries.d.ts.map +1 -1
  80. package/dist/db/cell_item_queries.js +27 -7
  81. package/dist/db/cell_queries.d.ts +32 -3
  82. package/dist/db/cell_queries.d.ts.map +1 -1
  83. package/dist/db/cell_queries.js +57 -22
  84. package/dist/db/fact_queries.d.ts +7 -0
  85. package/dist/db/fact_queries.d.ts.map +1 -1
  86. package/dist/db/fact_queries.js +27 -4
  87. package/dist/db/schema_ready.d.ts +6 -1
  88. package/dist/db/schema_ready.d.ts.map +1 -1
  89. package/dist/db/schema_ready.js +6 -2
  90. package/dist/db/sql_columns.d.ts +144 -0
  91. package/dist/db/sql_columns.d.ts.map +1 -0
  92. package/dist/db/sql_columns.js +164 -0
  93. package/dist/db/sql_identifier.d.ts +6 -5
  94. package/dist/db/sql_identifier.d.ts.map +1 -1
  95. package/dist/db/sql_identifier.js +6 -5
  96. package/dist/http/CLAUDE.md +4 -2
  97. package/dist/http/jsonrpc_helpers.d.ts +13 -0
  98. package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
  99. package/dist/http/jsonrpc_helpers.js +13 -0
  100. package/dist/http/route_spec.d.ts +2 -2
  101. package/dist/server/app_server.d.ts.map +1 -1
  102. package/dist/server/app_server.js +1 -2
  103. package/dist/server/serve_fact_route.d.ts.map +1 -1
  104. package/dist/server/serve_fact_route.js +7 -2
  105. package/dist/testing/CLAUDE.md +55 -28
  106. package/dist/testing/admin_integration.d.ts +0 -3
  107. package/dist/testing/admin_integration.d.ts.map +1 -1
  108. package/dist/testing/admin_integration.js +0 -1
  109. package/dist/testing/adversarial_404.d.ts.map +1 -1
  110. package/dist/testing/adversarial_404.js +4 -1
  111. package/dist/testing/adversarial_input.d.ts.map +1 -1
  112. package/dist/testing/adversarial_input.js +5 -4
  113. package/dist/testing/app_server.d.ts +4 -3
  114. package/dist/testing/app_server.d.ts.map +1 -1
  115. package/dist/testing/app_server.js +11 -5
  116. package/dist/testing/attack_surface.d.ts +16 -0
  117. package/dist/testing/attack_surface.d.ts.map +1 -1
  118. package/dist/testing/attack_surface.js +36 -13
  119. package/dist/testing/audit_completeness.d.ts +0 -3
  120. package/dist/testing/audit_completeness.d.ts.map +1 -1
  121. package/dist/testing/audit_completeness.js +0 -1
  122. package/dist/testing/auth_apps.d.ts +9 -2
  123. package/dist/testing/auth_apps.d.ts.map +1 -1
  124. package/dist/testing/auth_apps.js +9 -2
  125. package/dist/testing/cross_backend/account_lifecycle.d.ts +5 -4
  126. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  127. package/dist/testing/cross_backend/account_lifecycle.js +7 -0
  128. package/dist/testing/cross_backend/action_manifest.d.ts +19 -3
  129. package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
  130. package/dist/testing/cross_backend/action_manifest.js +10 -4
  131. package/dist/testing/cross_backend/action_manifest_parity.d.ts +9 -3
  132. package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
  133. package/dist/testing/cross_backend/action_manifest_parity.js +6 -0
  134. package/dist/testing/cross_backend/actor_lookup.d.ts +3 -2
  135. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/body_size.d.ts +4 -2
  137. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/body_size.js +1 -1
  139. package/dist/testing/cross_backend/cell_crud.d.ts +2 -2
  140. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  141. package/dist/testing/cross_backend/cell_crud.js +10 -0
  142. package/dist/testing/cross_backend/cell_gated_create.d.ts +3 -3
  143. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  144. package/dist/testing/cross_backend/cell_grant_role.d.ts +2 -2
  145. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  146. package/dist/testing/cross_backend/cell_relations.d.ts +2 -2
  147. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  148. package/dist/testing/cross_backend/expected_schema.json +1 -1
  149. package/dist/testing/cross_backend/fact_serving.d.ts +2 -2
  150. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/make_cross_backend_project.d.ts +17 -2
  152. package/dist/testing/cross_backend/make_cross_backend_project.d.ts.map +1 -1
  153. package/dist/testing/cross_backend/make_cross_backend_project.js +23 -3
  154. package/dist/testing/cross_backend/origin.d.ts +4 -2
  155. package/dist/testing/cross_backend/origin.d.ts.map +1 -1
  156. package/dist/testing/cross_backend/origin.js +1 -1
  157. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  158. package/dist/testing/cross_backend/peer_ping_ws.js +11 -7
  159. package/dist/testing/cross_backend/query_shape.d.ts +6 -5
  160. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
  161. package/dist/testing/cross_backend/ready.d.ts +2 -2
  162. package/dist/testing/cross_backend/ready.d.ts.map +1 -1
  163. package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -1
  164. package/dist/testing/cross_backend/role_grant_offer_enumeration.js +7 -1
  165. package/dist/testing/cross_backend/setup.d.ts +26 -6
  166. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  167. package/dist/testing/cross_backend/standard.d.ts +5 -1
  168. package/dist/testing/cross_backend/standard.d.ts.map +1 -1
  169. package/dist/testing/cross_backend/standard.js +1 -5
  170. package/dist/testing/cross_backend/testing_backdoor.js +1 -1
  171. package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -0
  172. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  173. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
  174. package/dist/testing/cross_backend/token_lifetime.js +6 -0
  175. package/dist/testing/cross_backend/token_scope_surface.d.ts +2 -2
  176. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  177. package/dist/testing/cross_backend/wire_shapes.d.ts +22 -0
  178. package/dist/testing/cross_backend/wire_shapes.d.ts.map +1 -0
  179. package/dist/testing/cross_backend/wire_shapes.js +41 -0
  180. package/dist/testing/data_exposure.d.ts +0 -3
  181. package/dist/testing/data_exposure.d.ts.map +1 -1
  182. package/dist/testing/data_exposure.js +0 -1
  183. package/dist/testing/db.d.ts +62 -7
  184. package/dist/testing/db.d.ts.map +1 -1
  185. package/dist/testing/db.js +86 -28
  186. package/dist/testing/integration.d.ts.map +1 -1
  187. package/dist/testing/integration.js +15 -2
  188. package/dist/testing/round_trip.d.ts +0 -3
  189. package/dist/testing/round_trip.d.ts.map +1 -1
  190. package/dist/testing/round_trip.js +0 -4
  191. package/dist/testing/rpc_attack_surface.js +2 -2
  192. package/dist/testing/rpc_round_trip.d.ts +0 -3
  193. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  194. package/dist/testing/rpc_round_trip.js +0 -1
  195. package/dist/testing/schema_generators.d.ts +15 -0
  196. package/dist/testing/schema_generators.d.ts.map +1 -1
  197. package/dist/testing/schema_generators.js +45 -11
  198. package/dist/testing/standard.d.ts +5 -1
  199. package/dist/testing/standard.d.ts.map +1 -1
  200. package/dist/testing/standard.js +2 -7
  201. package/dist/timestamp.d.ts +58 -0
  202. package/dist/timestamp.d.ts.map +1 -0
  203. package/dist/timestamp.js +57 -0
  204. package/dist/ui/AdminOverview.svelte +5 -10
  205. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  206. package/dist/ui/RoleGrantOfferHistory.svelte +3 -1
  207. package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -1
  208. package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
  209. package/dist/ui/role_grant_offers_state.svelte.js +6 -2
  210. package/package.json +1 -1
@@ -52,8 +52,9 @@ Optional fields:
52
52
  - `rate_limit?: 'ip' | 'account' | 'both'` — opts the action into the
53
53
  dispatcher's per-action rate-limit hook. **Throttle-requests semantics**
54
54
  — every invocation records regardless of outcome (different from REST
55
- login's throttle-failures). `'account'` rejected at registration when
56
- paired with `auth.account !== 'required'`. Limiters configured via
55
+ login's throttle-failures), and the gate fires ahead of param validation
56
+ so malformed calls are charged too. `'account'` / `'both'` rejected at
57
+ registration when paired with `auth.account !== 'required'`. Limiters configured via
57
58
  `AppServerOptions.action_ip_rate_limiter` / `action_account_rate_limiter`
58
59
  and threaded into both dispatchers automatically.
59
60
 
@@ -210,10 +211,13 @@ The HTTP RPC dispatcher is a thin shim around `perform_action`
210
211
  parsing, GET vs POST split, `c.json` binding); the
211
212
  auth/validation/dispatch pipeline is shared with the WebSocket dispatcher.
212
213
 
213
- **Phase order: 401 → authz → 403 → 400 → handler.** Authorize first,
214
- validate after — a caller the authority gates refuse never learns the
215
- action's input shape, and the coarse authority facts settle before the fine
216
- ones. Matches the Rust spine, which validates handler-side.
214
+ **Phase order: 401 → authz → 403 → 429 → 400 → handler.** Authorize first,
215
+ throttle, validate after — a caller the authority gates refuse never learns
216
+ the action's input shape, the coarse authority facts settle before the fine
217
+ ones, and a malformed flood burns the budget it should. The Rust spine's
218
+ dispatcher enforces the same order: it validates the `acting` selector at
219
+ the dispatcher after the rate limit and leaves every other field to the
220
+ handlers.
217
221
 
218
222
  Shim responsibilities (per-request):
219
223
 
@@ -286,13 +290,13 @@ The transport-agnostic post-parse pipeline. Each transport assembles a
286
290
  `perform_action(input, deps)`, and binds the discriminated
287
291
  `PerformActionResult` to its wire shape.
288
292
 
289
- Pipeline (401 → authz → 403 → 400 → handler):
293
+ Pipeline (401 → authz → 403 → 429 → 400 → handler):
290
294
 
291
295
  1. Pre-authorization auth (401)
292
- 2. Authorization phase — `apply_authorization_phase` against `account_id` + `read_acting(auth, raw_params)`, which takes the selector off the raw params (malformed reads as omitted) since validation now runs later. Test escape hatch via `preset.request_context`
296
+ 2. Authorization phase — `apply_authorization_phase` against `account_id` + `read_acting(auth, raw_params)`, which takes the selector off the raw params (malformed reads as omitted) since validation runs later. Test escape hatch via `preset.request_context`
293
297
  3. Post-authorization auth (403) — credential-type gate, then token scope, then role
294
- 4. Validate params (400) — `spec.input.safeParse` with `z.void()` / `?? {}` rules
295
- 5. Rate limit (429) — throttle-requests semantics
298
+ 4. Rate limit (429) — throttle-requests semantics, ahead of validation so malformed params charge the budget and a throttled caller pays no schema work
299
+ 5. Validate params (400) — `spec.input.safeParse` with `z.void()` / `?? {}` rules
296
300
  6. Dispatch + DEV output validation + error normalization — `spec.side_effects` picks transaction vs pool. `ThrownJsonrpcError` preserves code + data; generic throws become `internal_error`
297
301
 
298
302
  `PerformActionInput` carries `account_id`, `credential_type`, `client_ip`,
@@ -529,7 +533,7 @@ dispatcher without the origin/auth front-stack.
529
533
  Per-message dispatch delegates to `perform_action` — the shared core that
530
534
  HTTP RPC also calls. `register_action_ws` owns only WS-specific concerns:
531
535
 
532
- - **Wire envelope parsing** — JSON.parse → batch rejection → notification interception (cancel, silent drop) → per-message dispatch
536
+ - **Wire envelope parsing** — JSON.parse → batch rejection → peer-response routing → envelope-version guard → notification interception (cancel, silent drop) → `JsonrpcRequest.safeParse` → per-message dispatch. The version guard is what makes `{method: 'x'}` and `{jsonrpc: '1.0', method: 'x'}` an `invalid_request` answer rather than a silently dropped notification, matching the first check the Rust `classify` runs. Peer-response routing is the twin's response shape exactly — no `method` key, a string/number `id`, exactly one of `result` / `error` — so a hybrid frame like `{method, id, result}` stays a dispatchable request instead of being demuxed and swallowed. The envelope schema is the same one the HTTP POST path runs, so `params: null`, an array `params`, or a non-string/number `id` answers `invalid_request` (-32600) on both transports rather than reaching the dispatcher as `invalid_params`. It runs **after** the notification/cancel block: notifications carry no id to echo and stay unvalidated, and `cancel` routing precedes it. The version guard echoes the id via `to_jsonrpc_envelope_id` (a scalar frame answers `id: null`), the later steps via `to_jsonrpc_message_id`.
533
537
  - **Cancel-notification interception** — `{request_id → AbortController}` map; aborts the matching pending controller before the cancel bubbles past the dispatcher
534
538
  - **Socket-scoped notify** — `(method, params) => ws.send(notification)`, threaded into `perform_action` as `notify`
535
539
  - **Composed abort signal** — `AbortSignal.any([socket_close, per_request_cancel])`, threaded as `signal`
@@ -61,8 +61,8 @@ export interface ActionContext {
61
61
  db: Db;
62
62
  /**
63
63
  * Eager fire-and-forget queue — push the in-flight `Promise<void>` for
64
- * pool writes already running (audit emits, session touch, api-token
65
- * usage tracking). Drained via `flush_pending_effects` after the
64
+ * pool writes already running (audit emits, api-token usage
65
+ * tracking). Drained via `flush_pending_effects` after the
66
66
  * handler returns.
67
67
  */
68
68
  pending_effects: Array<Promise<void>>;
@@ -85,7 +85,7 @@ export interface ActionContext {
85
85
  /**
86
86
  * Credential channel the request arrived on (`'session'` | `'api_token'` |
87
87
  * `'daemon_token'`), or `null` for anonymous requests. Same value the
88
- * dispatcher's `credential_types` gate consumed at step 4 — exposed here
88
+ * dispatcher's `credential_types` gate consumed at step 3 — exposed here
89
89
  * so handlers can record it in audit metadata (defense in depth: the
90
90
  * gate may be loosened or bypassed in a future refactor, but the audit
91
91
  * row preserves what actually authenticated the request).
@@ -1 +1 @@
1
- {"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@ import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY, TEST_CONTEXT_PRES
20
20
  import { compile_action_registry } from './compile_action_registry.js';
21
21
  import { JSONRPC_VERSION, JsonrpcRequest } from '../http/jsonrpc.js';
22
22
  import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, JSONRPC_ERROR_CODES, dev_only } from '../http/jsonrpc_errors.js';
23
+ import { to_jsonrpc_envelope_id } from '../http/jsonrpc_helpers.js';
23
24
  import { perform_action, perform_action_result_to_envelope } from './perform_action.js';
24
25
  /**
25
26
  * Pair a spec with a handler while preserving per-method input/output types
@@ -200,12 +201,9 @@ export const create_rpc_endpoint = (options) => {
200
201
  }
201
202
  const envelope = JsonrpcRequest.safeParse(body);
202
203
  if (!envelope.success) {
203
- // try to extract id even from an invalid envelope
204
- const raw_id = typeof body === 'object' && body !== null && 'id' in body
205
- ? body.id
206
- : null;
207
- const id = typeof raw_id === 'string' || typeof raw_id === 'number' ? raw_id : null;
208
- const error = jsonrpc_error_envelope(id, jsonrpc_error_messages.invalid_request(dev_only({ issues: envelope.error.issues })));
204
+ // echo the id when the body is an object carrying one — the same
205
+ // extraction the WS path runs; a scalar body answers `id: null`
206
+ const error = jsonrpc_error_envelope(to_jsonrpc_envelope_id(body), jsonrpc_error_messages.invalid_request(dev_only({ issues: envelope.error.issues })));
209
207
  return c.json(error, 400);
210
208
  }
211
209
  return dispatch(c, route, envelope.data.method, envelope.data.params, envelope.data.id, false);
@@ -87,6 +87,13 @@ export type PingResponse = z.infer<typeof PingResponse>;
87
87
  * client→server invocation drives a server→client request); `auth: public`
88
88
  * (liveness is non-sensitive; the upgrade authenticated the socket);
89
89
  * `side_effects: false` (no state change — the handler only round-trips a ping).
90
+ *
91
+ * Public is not ungoverned: `rate_limit: 'ip'` is the one throttle an
92
+ * unauthenticated method can be given (there is no account to key on). It
93
+ * bounds anonymous fan-out and keeps the class at parity with the Rust spec's
94
+ * `RateLimitClass::Ip` — there the no-transport refusal costs two pool
95
+ * checkouts per call; here the public auth shape skips the authorization
96
+ * phase, so the refusal is free and the class is carried for the bound alone.
90
97
  */
91
98
  export declare const peer_ping_action_spec: {
92
99
  method: string;
@@ -106,6 +113,7 @@ export declare const peer_ping_action_spec: {
106
113
  protocol_version: z.ZodNumber;
107
114
  }, z.core.$strict>;
108
115
  async: true;
116
+ rate_limit: "ip";
109
117
  description: string;
110
118
  };
111
119
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"peer_ping.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/peer_ping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGhE,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAE5C;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAIvC,kDAAkD;AAClD,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,iDAAiD;AACjD,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAClE,iHAAiH;AACjH,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,sEAAsE;AACtE,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AACxE,0EAA0E;AAC1E,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AACxE,qFAAqF;AACrF,eAAO,MAAM,+BAA+B,6BAA6B,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;mBAQf,CAAC;AACd,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB;;kBAE5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;GAGG;AACH,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;CAYG,CAAC;AA+BtC;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAC7B,OAAO,eAAe,EACtB,KAAK,aAAa,KAChB,OAAO,CAAC,YAAY,CAqCtB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,OAAO,KAAG,YAGrD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAG9B,CAAC"}
1
+ {"version":3,"file":"peer_ping.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/peer_ping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGhE,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAE5C;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAIvC,kDAAkD;AAClD,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,iDAAiD;AACjD,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAClE,iHAAiH;AACjH,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,sEAAsE;AACtE,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AACxE,0EAA0E;AAC1E,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AACxE,qFAAqF;AACrF,eAAO,MAAM,+BAA+B,6BAA6B,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;mBAQf,CAAC;AACd,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAE9D,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB;;kBAE5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;GAGG;AACH,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;CAaG,CAAC;AA+BtC;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAC7B,OAAO,eAAe,EACtB,KAAK,aAAa,KAChB,OAAO,CAAC,YAAY,CAqCtB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,OAAO,KAAG,YAGrD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAG9B,CAAC"}
@@ -91,6 +91,13 @@ export const PingResponse = z.strictObject({
91
91
  * client→server invocation drives a server→client request); `auth: public`
92
92
  * (liveness is non-sensitive; the upgrade authenticated the socket);
93
93
  * `side_effects: false` (no state change — the handler only round-trips a ping).
94
+ *
95
+ * Public is not ungoverned: `rate_limit: 'ip'` is the one throttle an
96
+ * unauthenticated method can be given (there is no account to key on). It
97
+ * bounds anonymous fan-out and keeps the class at parity with the Rust spec's
98
+ * `RateLimitClass::Ip` — there the no-transport refusal costs two pool
99
+ * checkouts per call; here the public auth shape skips the authorization
100
+ * phase, so the refusal is free and the class is carried for the bound alone.
94
101
  */
95
102
  export const peer_ping_action_spec = {
96
103
  method: PEER_PING_METHOD,
@@ -101,6 +108,7 @@ export const peer_ping_action_spec = {
101
108
  input: PingActionInput,
102
109
  output: PingResponse,
103
110
  async: true,
111
+ rate_limit: 'ip',
104
112
  description: 'Liveness round-trip — the handler pings the originating client back over its socket, ' +
105
113
  'validates the echo, and returns it. Refused as no-transport over HTTP RPC.'
106
114
  };
@@ -25,7 +25,7 @@ export declare const DEFAULT_PEER_REQUEST_TIMEOUT = 10000;
25
25
  /**
26
26
  * Per-connection cap on concurrent in-flight server→client requests. A caller
27
27
  * past it gets `too_many_in_flight` instead of growing the pending map
28
- * unbounded. Twin of the Rust spine's `DEFAULT_MAX_IN_FLIGHT_PER_CONN`.
28
+ * unbounded. Twin of the Rust spine's `DEFAULT_MAX_OUTBOUND_PEER_REQUESTS_PER_CONN`.
29
29
  */
30
30
  export declare const MAX_IN_FLIGHT_PEER_REQUESTS_PER_CONNECTION = 256;
31
31
  /**
@@ -23,7 +23,7 @@ export const DEFAULT_PEER_REQUEST_TIMEOUT = 10_000;
23
23
  /**
24
24
  * Per-connection cap on concurrent in-flight server→client requests. A caller
25
25
  * past it gets `too_many_in_flight` instead of growing the pending map
26
- * unbounded. Twin of the Rust spine's `DEFAULT_MAX_IN_FLIGHT_PER_CONN`.
26
+ * unbounded. Twin of the Rust spine's `DEFAULT_MAX_OUTBOUND_PEER_REQUESTS_PER_CONN`.
27
27
  */
28
28
  export const MAX_IN_FLIGHT_PEER_REQUESTS_PER_CONNECTION = 256;
29
29
  /**
@@ -18,7 +18,16 @@
18
18
  * 3. **Post-authorization auth (403)** — gates `auth.credential_types`,
19
19
  * the credential's `token_scope`, and `auth.roles` against the resolved
20
20
  * context.
21
- * 4. **Validate params (400)** — `spec.input.safeParse(raw_params)` with
21
+ * 4. **Rate limit (429)** — per-action IP / account throttling, throttle-
22
+ * requests semantics (every invocation records, regardless of outcome).
23
+ * Ahead of validation so the budget is charged for malformed params too
24
+ * — a flood of garbage bodies must cost the sender its quota, not slip
25
+ * past the limiter on a cheap 400 — and so a throttled caller never pays
26
+ * for schema work. The account key is the credential's own account
27
+ * (`request_context.account.id`), settled by authentication, so the gate
28
+ * sits behind the authority gates by choice rather than necessity: a
29
+ * caller those gates refuse is answered 403, not 429.
30
+ * 5. **Validate params (400)** — `spec.input.safeParse(raw_params)` with
22
31
  * `z.void()` / `?? {}` rules. Runs **after** the authority gates: a
23
32
  * 400 describing the action's input shape to a caller those gates
24
33
  * refuse would confirm the method exists and describe how to call it,
@@ -26,13 +35,11 @@
26
35
  * facts before fine ones, and no shape disclosure before authority is
27
36
  * settled — the same argument that puts the credential gate ahead of
28
37
  * the scope gate. The cost: a malformed request from an *authenticated*
29
- * caller now pays for the authorization phase's actor + role_grant reads
30
- * before being rejected, where validation used to turn it away first.
31
- * Not a new DoS lever — the same caller reaches that work anyway by
32
- * sending well-formed params — but it does remove a cheap-rejection path
33
- * that buggy clients used to hit.
34
- * 5. **Rate limit (429)** — per-action IP / account throttling, throttle-
35
- * requests semantics (every invocation records, regardless of outcome).
38
+ * caller pays for the authorization phase's actor + role_grant reads
39
+ * before being rejected, rather than being turned away by validation
40
+ * first. Not a new DoS lever — the same caller reaches that work anyway
41
+ * by sending well-formed params — but there is no cheap-rejection path
42
+ * for buggy clients.
36
43
  * 6. **Dispatch + DEV-only output validation + error normalization** —
37
44
  * `spec.side_effects` picks transaction (`deps.db.transaction`) vs
38
45
  * pool. Handler throws roll back the transaction; the catch sits
@@ -150,7 +157,7 @@ export type PerformActionResult = {
150
157
  * transport calls into this with pre-parsed inputs and binds the result
151
158
  * to its wire shape.
152
159
  *
153
- * Phase order: 401 → authz → 403 → 400 → handler. On the test-preset path
160
+ * Phase order: 401 → authz → 403 → 429 → 400 → handler. On the test-preset path
154
161
  * the dispatcher skips the live authorization phase and uses the supplied
155
162
  * pre-baked context for post-authorization checks; the 401 still fires when
156
163
  * the harness omits `account_id`.
@@ -1 +1 @@
1
- {"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"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"}
@@ -18,7 +18,16 @@
18
18
  * 3. **Post-authorization auth (403)** — gates `auth.credential_types`,
19
19
  * the credential's `token_scope`, and `auth.roles` against the resolved
20
20
  * context.
21
- * 4. **Validate params (400)** — `spec.input.safeParse(raw_params)` with
21
+ * 4. **Rate limit (429)** — per-action IP / account throttling, throttle-
22
+ * requests semantics (every invocation records, regardless of outcome).
23
+ * Ahead of validation so the budget is charged for malformed params too
24
+ * — a flood of garbage bodies must cost the sender its quota, not slip
25
+ * past the limiter on a cheap 400 — and so a throttled caller never pays
26
+ * for schema work. The account key is the credential's own account
27
+ * (`request_context.account.id`), settled by authentication, so the gate
28
+ * sits behind the authority gates by choice rather than necessity: a
29
+ * caller those gates refuse is answered 403, not 429.
30
+ * 5. **Validate params (400)** — `spec.input.safeParse(raw_params)` with
22
31
  * `z.void()` / `?? {}` rules. Runs **after** the authority gates: a
23
32
  * 400 describing the action's input shape to a caller those gates
24
33
  * refuse would confirm the method exists and describe how to call it,
@@ -26,13 +35,11 @@
26
35
  * facts before fine ones, and no shape disclosure before authority is
27
36
  * settled — the same argument that puts the credential gate ahead of
28
37
  * the scope gate. The cost: a malformed request from an *authenticated*
29
- * caller now pays for the authorization phase's actor + role_grant reads
30
- * before being rejected, where validation used to turn it away first.
31
- * Not a new DoS lever — the same caller reaches that work anyway by
32
- * sending well-formed params — but it does remove a cheap-rejection path
33
- * that buggy clients used to hit.
34
- * 5. **Rate limit (429)** — per-action IP / account throttling, throttle-
35
- * requests semantics (every invocation records, regardless of outcome).
38
+ * caller pays for the authorization phase's actor + role_grant reads
39
+ * before being rejected, rather than being turned away by validation
40
+ * first. Not a new DoS lever — the same caller reaches that work anyway
41
+ * by sending well-formed params — but there is no cheap-rejection path
42
+ * for buggy clients.
36
43
  * 6. **Dispatch + DEV-only output validation + error normalization** —
37
44
  * `spec.side_effects` picks transaction (`deps.db.transaction`) vs
38
45
  * pool. Handler throws roll back the transaction; the catch sits
@@ -62,7 +69,7 @@ import { token_scope_admits_method, token_scope_method_capability } from '../aut
62
69
  * transport calls into this with pre-parsed inputs and binds the result
63
70
  * to its wire shape.
64
71
  *
65
- * Phase order: 401 → authz → 403 → 400 → handler. On the test-preset path
72
+ * Phase order: 401 → authz → 403 → 429 → 400 → handler. On the test-preset path
66
73
  * the dispatcher skips the live authorization phase and uses the supplied
67
74
  * pre-baked context for post-authorization checks; the 401 still fires when
68
75
  * the harness omits `account_id`.
@@ -77,10 +84,10 @@ export const perform_action = async (input, deps) => {
77
84
  if (pre)
78
85
  return error_result(pre);
79
86
  // step 2: authorization phase. `acting` reads off the raw params rather
80
- // than a validated field, because validation now runs after the authority
87
+ // than a validated field, because validation runs after the authority
81
88
  // gates. Per registry-time invariant 2,
82
89
  // `auth.actor !== 'none' ⟺ input declares acting?: ActingActor` — so the
83
- // selector is present on exactly the specs that need it, and step 4 is
90
+ // selector is present on exactly the specs that need it, and step 5 is
84
91
  // what rejects a malformed one.
85
92
  let request_context = null;
86
93
  if (preset !== undefined) {
@@ -105,18 +112,11 @@ export const perform_action = async (input, deps) => {
105
112
  const post = check_action_auth_post_authorization(action_auth, request_context, credential_type, action.spec.method, token_scope);
106
113
  if (post)
107
114
  return error_result(post);
108
- // step 4: validate params. JSON-RPC 2.0 §4.2 forbids `params: null`;
109
- // registration sites reject `z.null()` inputs. Empty-body convention
110
- // (`raw_params ?? {}`) lets all-optional-object methods omit `params`.
111
- const params = is_void_schema(spec.input) ? raw_params : (raw_params ?? {});
112
- const parse_result = spec.input.safeParse(params);
113
- if (!parse_result.success) {
114
- return error_result(jsonrpc_error_messages.invalid_params('invalid params', dev_only({ issues: parse_result.error.issues })));
115
- }
116
- const validated_input = parse_result.data;
117
- // step 5: rate limit — throttle-requests semantics (record on every
115
+ // step 4: rate limit — throttle-requests semantics (record on every
118
116
  // invocation, no success-reset). Same limiters shared with the WS
119
117
  // dispatcher so an attacker can't switch transports to bypass the budget.
118
+ // Runs ahead of validation so a flood of malformed params burns the
119
+ // budget it should, and so a throttled caller pays no schema work.
120
120
  const rate_limit = spec.rate_limit;
121
121
  if (rate_limit) {
122
122
  const ip_check = action_ip_rate_limiter && (rate_limit === 'ip' || rate_limit === 'both');
@@ -141,6 +141,15 @@ export const perform_action = async (input, deps) => {
141
141
  action_account_rate_limiter.record(account_keyed_context.account.id);
142
142
  }
143
143
  }
144
+ // step 5: validate params. JSON-RPC 2.0 §4.2 forbids `params: null`;
145
+ // registration sites reject `z.null()` inputs. Empty-body convention
146
+ // (`raw_params ?? {}`) lets all-optional-object methods omit `params`.
147
+ const params = is_void_schema(spec.input) ? raw_params : (raw_params ?? {});
148
+ const parse_result = spec.input.safeParse(params);
149
+ if (!parse_result.success) {
150
+ return error_result(jsonrpc_error_messages.invalid_params('invalid params', dev_only({ issues: parse_result.error.issues })));
151
+ }
152
+ const validated_input = parse_result.data;
144
153
  // step 6: dispatch — transaction for mutations, pool for reads.
145
154
  const use_transaction = spec.side_effects;
146
155
  const execute = async (effective_db) => {
@@ -176,7 +176,7 @@ export interface RegisterActionWsResult {
176
176
  * `ctx.signal` before bubbling out.
177
177
  * - Per-message dispatch goes through `perform_action`: pre-authorization
178
178
  * auth (401) → authorization phase → post-authorization auth (403) →
179
- * input validation (400) → rate limit (429) → handler (with transaction
179
+ * rate limit (429) → input validation (400) → handler (with transaction
180
180
  * wrap iff `spec.side_effects: true`) → DEV output validation.
181
181
  * - Authorization phase runs **per message** — role_grant changes during a
182
182
  * connection lifetime are picked up on the next message without any
@@ -1 +1 @@
1
- {"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAE3D,OAAO,EAAU,KAAK,MAAM,IAAI,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAWnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAmBtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAIhD,OAAO,EAAE,yBAAyB,EAAE,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIhG,YAAY,EAAE,MAAM,EAAE,CAAC;AAEvB,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,qFAAqF;IACrF,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,aAAa,EAAE,IAAI,CAAC;IACpB,oDAAoD;IACpD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,wFAAwF;IACxF,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,EAAE,EAAE,SAAS,CAAC;IACd,2CAA2C;IAC3C,aAAa,EAAE,IAAI,CAAC;IACpB,kGAAkG;IAClG,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,uBAAuB;IACvC,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;;;;OAOG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;;;;OASG;IACH,EAAE,EAAE,EAAE,CAAC;IACP;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5C;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACjD;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,uBAAuB,KAAG,sBAuZrE,CAAC"}
1
+ {"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAE3D,OAAO,EAAU,KAAK,MAAM,IAAI,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAWnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAyBtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAIhD,OAAO,EAAE,yBAAyB,EAAE,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAIhG,YAAY,EAAE,MAAM,EAAE,CAAC;AAEvB,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAsCvD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,qFAAqF;IACrF,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,aAAa,EAAE,IAAI,CAAC;IACpB,oDAAoD;IACpD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,wFAAwF;IACxF,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,EAAE,EAAE,SAAS,CAAC;IACd,2CAA2C;IAC3C,aAAa,EAAE,IAAI,CAAC;IACpB,kGAAkG;IAClG,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,uBAAuB;IACvC,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;;;;OAOG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;;;;OASG;IACH,EAAE,EAAE,EAAE,CAAC;IACP;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5C;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACjD;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,uBAAuB,KAAG,sBAgbrE,CAAC"}
@@ -32,8 +32,9 @@ import { get_request_context, require_request_context, token_scope_surface_denia
32
32
  import { hash_session_token } from '../auth/session_queries.js';
33
33
  import { get_client_ip } from '../http/client_ip.js';
34
34
  import { flush_pending_effects, flush_post_commit_effects } from '../http/pending_effects.js';
35
- import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
36
- import { create_jsonrpc_error_response, create_jsonrpc_notification, to_jsonrpc_message_id, to_jsonrpc_params, is_jsonrpc_request, is_jsonrpc_response, is_jsonrpc_error_response } from '../http/jsonrpc_helpers.js';
35
+ import { JsonrpcRequest } from '../http/jsonrpc.js';
36
+ import { dev_only, jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
37
+ import { create_jsonrpc_error_response, create_jsonrpc_notification, to_jsonrpc_envelope_id, to_jsonrpc_message_id, to_jsonrpc_params, is_jsonrpc_object, is_jsonrpc_request, is_jsonrpc_request_id } from '../http/jsonrpc_helpers.js';
37
38
  import { CREDENTIAL_TYPE_KEY, TOKEN_SCOPE_KEY, AUTH_API_TOKEN_ID_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
38
39
  import {} from './action_types.js';
39
40
  import { compile_action_registry } from './compile_action_registry.js';
@@ -44,6 +45,39 @@ import { audit_unmatched_peer_response } from './peer_request.js';
44
45
  import { perform_action, perform_action_result_to_envelope } from './perform_action.js';
45
46
  /** Default inactivity window before the server closes a silent socket. */
46
47
  export const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60_000;
48
+ /** Send one JSON-RPC error response frame on `ws`. */
49
+ const send_error_response = (ws, id, error) => {
50
+ ws.send(JSON.stringify(create_jsonrpc_error_response(id, error)));
51
+ };
52
+ /**
53
+ * Whether a frame is a reply to a server-initiated request, in the shape the
54
+ * Rust twin's `classify` accepts: `jsonrpc: '2.0'`, **no** string `method`, a
55
+ * string/number `id`, and exactly one of `result` / `error`.
56
+ *
57
+ * The method axis is string-typed like the twin, whose
58
+ * `obj.get("method").and_then(Value::as_str)` reads a present-but-non-string
59
+ * `method` as absent — so `{id, method: 123, result}` is a response on both
60
+ * sides, not a request. The `result` / `error` XOR likewise refuses a frame
61
+ * carrying both, so neither side has to pick a winner between them.
62
+ *
63
+ * Deliberately narrower than the `is_jsonrpc_response` /
64
+ * `is_jsonrpc_error_response` guards, which only ask for a version, an `id`,
65
+ * and a `result` or `error` — those stay loose because a client matching a
66
+ * reply must still recognize an error response carrying `id: null`. Here a
67
+ * loose match would swallow hybrids the twin answers: `{method, id, result}`
68
+ * is a request the dispatcher should run, and `{id, result, error}` or a
69
+ * boolean `id` is `invalid_request`. Everything this rejects falls through to
70
+ * the version guard / notification / envelope steps below.
71
+ */
72
+ const is_peer_response = (json) => {
73
+ if (!is_jsonrpc_object(json))
74
+ return false;
75
+ if (typeof json.method === 'string')
76
+ return false;
77
+ if (!is_jsonrpc_request_id(json.id))
78
+ return false;
79
+ return 'result' in json !== 'error' in json;
80
+ };
47
81
  /**
48
82
  * Mount a JSON-RPC WebSocket endpoint that dispatches via the shared
49
83
  * `perform_action` core.
@@ -55,7 +89,7 @@ export const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60_000;
55
89
  * `ctx.signal` before bubbling out.
56
90
  * - Per-message dispatch goes through `perform_action`: pre-authorization
57
91
  * auth (401) → authorization phase → post-authorization auth (403) →
58
- * input validation (400) → rate limit (429) → handler (with transaction
92
+ * rate limit (429) → input validation (400) → handler (with transaction
59
93
  * wrap iff `spec.side_effects: true`) → DEV output validation.
60
94
  * - Authorization phase runs **per message** — role_grant changes during a
61
95
  * connection lifetime are picked up on the next message without any
@@ -219,7 +253,7 @@ export const register_action_ws = (options) => {
219
253
  catch (error) {
220
254
  log.error('on_socket_open failed — closing socket:', error);
221
255
  try {
222
- ws.send(JSON.stringify(create_jsonrpc_error_response(null, jsonrpc_error_messages.internal_error())));
256
+ send_error_response(ws, null, jsonrpc_error_messages.internal_error());
223
257
  }
224
258
  catch {
225
259
  // ignore — socket may already be dead
@@ -236,22 +270,23 @@ export const register_action_ws = (options) => {
236
270
  }
237
271
  catch (error) {
238
272
  log.error('JSON parse error:', error);
239
- ws.send(JSON.stringify(create_jsonrpc_error_response(null, jsonrpc_error_messages.parse_error())));
273
+ send_error_response(ws, null, jsonrpc_error_messages.parse_error());
240
274
  return;
241
275
  }
242
276
  // Batch JSON-RPC is not supported on the WebSocket path.
243
277
  if (Array.isArray(json)) {
244
- ws.send(JSON.stringify(create_jsonrpc_error_response(null, jsonrpc_error_messages.invalid_request('batch JSON-RPC requests are not supported on WebSocket'))));
278
+ send_error_response(ws, null, jsonrpc_error_messages.invalid_request('batch JSON-RPC requests are not supported on WebSocket'));
245
279
  return;
246
280
  }
247
281
  // Inbound responses to server-initiated requests (`peer/ping`,
248
282
  // etc.) — route to the pending-request registry scoped to THIS
249
- // socket. A response carries `result`/`error` + `id` and no
250
- // `method`, so it precedes the request/notification split below.
251
- // An unmatched id (unsolicited, cross-connection, or already
252
- // settled) resolves nothing and is dropped, so the read loop
253
- // survives a junk frame.
254
- if (is_jsonrpc_response(json) || is_jsonrpc_error_response(json)) {
283
+ // socket. A response carries a string/number `id`, exactly one
284
+ // of `result` / `error`, and no string `method`, so it precedes the
285
+ // request/notification split below. An unmatched id
286
+ // (unsolicited, cross-connection, or already settled) resolves
287
+ // nothing and is dropped, so the read loop survives a junk
288
+ // frame.
289
+ if (is_peer_response(json)) {
255
290
  if (captured_connection_id !== undefined) {
256
291
  const matched = transport.resolve_peer_response(captured_connection_id, json);
257
292
  if (!matched)
@@ -259,13 +294,32 @@ export const register_action_ws = (options) => {
259
294
  }
260
295
  return;
261
296
  }
262
- // Notifications (method + no id) — `cancel` is intercepted
263
- // for request-scoped cancellation; other notifications are
264
- // silenced per JSON-RPC spec (consumer notification handlers
265
- // are not a feature yet).
297
+ // Envelope version — the first thing the Rust twin's `classify`
298
+ // checks (version → id → method → params). A frame without
299
+ // `jsonrpc: '2.0'` is `invalid_request` on both transports
300
+ // rather than falling through to the notification branch,
301
+ // which would silently drop `{method: 'x'}` and
302
+ // `{jsonrpc: '1.0', method: 'x'}`. It also covers a
303
+ // non-object frame (a scalar has no id to echo — `id: null`).
304
+ // A genuine peer response carries the version and is matched
305
+ // above; a response-shaped frame that misses that shape check
306
+ // falls through here on purpose, so it is answered rather
307
+ // than swallowed.
308
+ if (!is_jsonrpc_object(json)) {
309
+ send_error_response(ws, to_jsonrpc_envelope_id(json), jsonrpc_error_messages.invalid_request());
310
+ return;
311
+ }
312
+ // Notifications (string method + no id) — `cancel` is
313
+ // intercepted for request-scoped cancellation; other
314
+ // notifications are silenced per JSON-RPC spec (consumer
315
+ // notification handlers are not a feature yet). The method
316
+ // axis is string-typed like the twin's `classify`, so a
317
+ // non-string `method` is answered `invalid_request` below
318
+ // rather than silently dropped as a notification.
266
319
  if (!is_jsonrpc_request(json)) {
267
- if (typeof json === 'object' && json !== null && 'method' in json && !('id' in json)) {
268
- if (json.method === cancel_action_spec.method) {
320
+ const notification_method = json.method;
321
+ if (typeof notification_method === 'string' && !('id' in json)) {
322
+ if (notification_method === cancel_action_spec.method) {
269
323
  const parsed = CancelNotificationParams.safeParse(json.params);
270
324
  if (!parsed.success) {
271
325
  log.debug('cancel: invalid params, ignoring', parsed.error.issues);
@@ -281,17 +335,30 @@ export const register_action_ws = (options) => {
281
335
  }
282
336
  return;
283
337
  }
284
- ws.send(JSON.stringify(create_jsonrpc_error_response(to_jsonrpc_message_id(json), jsonrpc_error_messages.invalid_request())));
338
+ send_error_response(ws, to_jsonrpc_message_id(json), jsonrpc_error_messages.invalid_request());
339
+ return;
340
+ }
341
+ // Envelope validation — the same `JsonrpcRequest.safeParse` the
342
+ // HTTP POST path runs, so a malformed envelope (`params: null`,
343
+ // an array `params`, a boolean `id`) answers `invalid_request`
344
+ // on both transports instead of reaching the dispatcher and
345
+ // surfacing as `invalid_params`. It sits after the
346
+ // notification / cancel block so notifications stay
347
+ // unvalidated (they carry no id to echo) and `cancel` routing
348
+ // precedes it.
349
+ const envelope = JsonrpcRequest.safeParse(json);
350
+ if (!envelope.success) {
351
+ send_error_response(ws, to_jsonrpc_message_id(json), jsonrpc_error_messages.invalid_request(dev_only({ issues: envelope.error.issues })));
285
352
  return;
286
353
  }
287
- const { method, id, params } = json;
354
+ const { method, id, params } = envelope.data;
288
355
  // Per-action method lookup — return method_not_found before
289
356
  // we engage the dispatch machinery. Specs without a handler
290
357
  // (client-only / dispatcher-handled) miss action_map and
291
358
  // surface as method_not_found just like unknown methods.
292
359
  const action = action_map.get(method);
293
360
  if (!action) {
294
- ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.method_not_found(method))));
361
+ send_error_response(ws, id, jsonrpc_error_messages.method_not_found(method));
295
362
  return;
296
363
  }
297
364
  if (artificial_delay > 0) {