@fuzdev/fuz_app 0.131.0 → 0.132.1

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 (58) hide show
  1. package/dist/actions/transports_http.d.ts +7 -0
  2. package/dist/actions/transports_http.d.ts.map +1 -1
  3. package/dist/actions/transports_http.js +18 -1
  4. package/dist/auth/CLAUDE.md +1 -1
  5. package/dist/auth/middleware.d.ts +13 -10
  6. package/dist/auth/middleware.d.ts.map +1 -1
  7. package/dist/auth/middleware.js +14 -10
  8. package/dist/http/CLAUDE.md +3 -2
  9. package/dist/http/error_schemas.d.ts +16 -1
  10. package/dist/http/error_schemas.d.ts.map +1 -1
  11. package/dist/http/error_schemas.js +14 -1
  12. package/dist/http/origin.d.ts +10 -6
  13. package/dist/http/origin.d.ts.map +1 -1
  14. package/dist/http/origin.js +10 -6
  15. package/dist/http/route_spec.d.ts +19 -12
  16. package/dist/http/route_spec.d.ts.map +1 -1
  17. package/dist/http/route_spec.js +33 -6
  18. package/dist/http/schema_helpers.d.ts +3 -1
  19. package/dist/http/schema_helpers.d.ts.map +1 -1
  20. package/dist/http/schema_helpers.js +3 -2
  21. package/dist/http/simple_request.d.ts +63 -0
  22. package/dist/http/simple_request.d.ts.map +1 -0
  23. package/dist/http/simple_request.js +84 -0
  24. package/dist/server/app_server.d.ts +2 -2
  25. package/dist/server/app_server.d.ts.map +1 -1
  26. package/dist/server/app_server.js +5 -1
  27. package/dist/testing/CLAUDE.md +17 -0
  28. package/dist/testing/admin_integration.d.ts.map +1 -1
  29. package/dist/testing/admin_integration.js +4 -2
  30. package/dist/testing/adversarial_404.d.ts.map +1 -1
  31. package/dist/testing/adversarial_404.js +5 -3
  32. package/dist/testing/adversarial_input.d.ts.map +1 -1
  33. package/dist/testing/adversarial_input.js +9 -3
  34. package/dist/testing/attack_surface.d.ts.map +1 -1
  35. package/dist/testing/attack_surface.js +15 -16
  36. package/dist/testing/audit_completeness.js +1 -1
  37. package/dist/testing/cross_backend/simple_request.d.ts +11 -0
  38. package/dist/testing/cross_backend/simple_request.d.ts.map +1 -0
  39. package/dist/testing/cross_backend/simple_request.js +142 -0
  40. package/dist/testing/cross_backend/standard.d.ts +6 -0
  41. package/dist/testing/cross_backend/standard.d.ts.map +1 -1
  42. package/dist/testing/cross_backend/standard.js +2 -1
  43. package/dist/testing/data_exposure.d.ts.map +1 -1
  44. package/dist/testing/data_exposure.js +11 -8
  45. package/dist/testing/integration.d.ts.map +1 -1
  46. package/dist/testing/integration.js +40 -11
  47. package/dist/testing/round_trip.d.ts.map +1 -1
  48. package/dist/testing/round_trip.js +3 -9
  49. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  50. package/dist/testing/rpc_helpers.js +19 -6
  51. package/dist/testing/schema_generators.d.ts +12 -0
  52. package/dist/testing/schema_generators.d.ts.map +1 -1
  53. package/dist/testing/schema_generators.js +10 -0
  54. package/dist/testing/stubs.d.ts.map +1 -1
  55. package/dist/testing/stubs.js +3 -0
  56. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  57. package/dist/ui/auth_state.svelte.js +5 -1
  58. package/package.json +3 -3
@@ -22,6 +22,13 @@ import type { Transport, TransportSendOptions } from './transports.js';
22
22
  export declare class FrontendHttpTransport implements Transport {
23
23
  #private;
24
24
  readonly transport_name: "frontend_http_rpc";
25
+ /**
26
+ * @param url - the JSON-RPC endpoint
27
+ * @param headers - extra headers, merged over the JSON `content-type` and
28
+ * `accept` defaults case-insensitively — a POST without
29
+ * `application/json` is refused (415)
30
+ * @param has_side_effects - when it returns `false` for a method, the call is a GET
31
+ */
25
32
  constructor(url: string, headers?: Record<string, string>, has_side_effects?: (method: string) => boolean);
26
33
  send(message: JsonrpcRequest, options?: TransportSendOptions): Promise<JsonrpcResponseOrError>;
27
34
  send(message: JsonrpcNotification, options?: TransportSendOptions): Promise<JsonrpcErrorResponse | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"transports_http.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_http.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiBH,OAAO,EACN,oBAAoB,EAGpB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAEnB,KAAK,sBAAsB,EAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvE;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,SAAS;;IACtD,QAAQ,CAAC,cAAc,EAAG,mBAAmB,CAAU;gBAOtD,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO;IAOzC,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA+FvC,QAAQ,IAAI,OAAO;CAGnB"}
1
+ {"version":3,"file":"transports_http.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_http.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiBH,OAAO,EACN,oBAAoB,EAGpB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAEnB,KAAK,sBAAsB,EAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAQvE;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,SAAS;;IACtD,QAAQ,CAAC,cAAc,EAAG,mBAAmB,CAAU;IAMvD;;;;;;OAMG;gBAEF,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO;IAYzC,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA+FvC,QAAQ,IAAI,OAAO;CAGnB"}
@@ -7,6 +7,11 @@ import { DEV } from 'esm-env';
7
7
  import { ThrownJsonrpcError, jsonrpc_error_messages, http_status_to_jsonrpc_error_code, jsonrpc_error_code_to_http_status, UNKNOWN_ERROR_MESSAGE } from '../http/jsonrpc_errors.js';
8
8
  import { create_jsonrpc_error_response, to_jsonrpc_message_id, is_jsonrpc_error_response, is_jsonrpc_response } from '../http/jsonrpc_helpers.js';
9
9
  import { JsonrpcErrorResponse } from '../http/jsonrpc.js';
10
+ /** Headers every request carries unless the caller's replace them. */
11
+ const DEFAULT_HEADERS = {
12
+ 'content-type': 'application/json',
13
+ accept: 'application/json'
14
+ };
10
15
  /**
11
16
  * Thin `fetch` adapter for the JSON-RPC endpoint. POST by default; GET when
12
17
  * the optional `has_side_effects(method)` callback returns `false` for the
@@ -26,9 +31,21 @@ export class FrontendHttpTransport {
26
31
  #url;
27
32
  #headers;
28
33
  #has_side_effects;
34
+ /**
35
+ * @param url - the JSON-RPC endpoint
36
+ * @param headers - extra headers, merged over the JSON `content-type` and
37
+ * `accept` defaults case-insensitively — a POST without
38
+ * `application/json` is refused (415)
39
+ * @param has_side_effects - when it returns `false` for a method, the call is a GET
40
+ */
29
41
  constructor(url, headers, has_side_effects) {
30
42
  this.#url = url;
31
- this.#headers = headers ?? { 'content-type': 'application/json', accept: 'application/json' };
43
+ this.#headers = { ...DEFAULT_HEADERS };
44
+ if (headers) {
45
+ for (const [name, value] of Object.entries(headers)) {
46
+ this.#headers[name.toLowerCase()] = value;
47
+ }
48
+ }
32
49
  this.#has_side_effects = has_side_effects;
33
50
  }
34
51
  async send(message, options) {
@@ -215,7 +215,7 @@ Everything else listed under §RPC action surfaces.
215
215
 
216
216
  ### Middleware
217
217
 
218
- - `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token`. `AUTH_MIDDLEWARE_PATH` (`'/api/*'`) is where `create_app_server` mounts the stack; two assembly asserts lean on it: `assert_endpoint_in_auth_scope` (an `rpc_endpoints` / `ws_endpoints` path must sit inside it) and `assert_middleware_stack_mounted(stack_specs, middleware_specs)`, run on a `transform_middleware` result against the pre-transform stack (`[proxy, ...auth]`) — each stack spec, matched by its `handler` closure (not its spoofable `name`), mounted exactly once at its original path (`'*'` for the trusted proxy, `AUTH_MIDDLEWARE_PATH` for auth), in its original order; other specs may sit anywhere, including before the proxy and between the auth layers. The proxy is in the stack because dropping it turns every `get_client_ip` into `'unknown'`, one shared bucket for every IP-keyed limiter. The transform receives shallow copies in a fresh array, so an in-place mutation can't rewrite the originals the check reads. The check pins where those layers sit, not what added layers do — a layer you add runs with full context access. Both run in `create_test_app_surface_spec` too.
218
+ - `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token` (the content-type refusal isn't a middleware: `apply_route_specs` checks every `POST` route, see `http/simple_request.ts`). `AUTH_MIDDLEWARE_PATH` (`'/api/*'`) is where `create_app_server` mounts the stack; two assembly asserts lean on it: `assert_endpoint_in_auth_scope` (an `rpc_endpoints` / `ws_endpoints` path, or the bootstrap route under `bootstrap.route_prefix`, must sit inside it) and `assert_middleware_stack_mounted(stack_specs, middleware_specs)`, run on a `transform_middleware` result against the pre-transform stack (`[proxy, ...auth]`) — each stack spec, matched by its `handler` closure (not its spoofable `name`), mounted exactly once at its original path (`'*'` for the trusted proxy, `AUTH_MIDDLEWARE_PATH` for auth), in its original order; other specs may sit anywhere, including before the proxy and between the auth layers. The proxy is in the stack because dropping it turns every `get_client_ip` into `'unknown'`, one shared bucket for every IP-keyed limiter. The transform receives shallow copies in a fresh array, so an in-place mutation can't rewrite the originals the check reads. The check pins where those layers sit, not what added layers do — a layer you add runs with full context access. Both run in `create_test_app_surface_spec` too.
219
219
  - `auth/request_context.ts` — `RequestContext`, `resolve_acting_actor`, `build_request_context`, predicates (`has_role`, `has_scoped_role`, `has_any_scoped_role` — the dispatcher's role gate and every builtin-role check read the **global** grant via `has_scoped_role(_, role, null)` / `has_any_scoped_role(_, roles, null)`; scope-blind `has_role` is never the gate for a builtin role), guards (`require_auth`, `require_role`, `require_credential_types`, `require_token_scope`), `token_scope_surface_denial` (the direct-call form, for surfaces that aren't route specs), `refresh_role_grants`.
220
220
  - `auth/session_middleware.ts` — `process_session_cookie` integration, `create_session_and_set_cookie` (shared by login / signup / bootstrap; returns the token hashes of the sessions the cap evicted, which the caller closes after its commit — only login passes a cap).
221
221
  - `auth/resolved_auth.ts` — `ResolvedAuth` (the resolved credential as one value: `account_id`, `credential_type`, `token_hash`, `api_token_id`), `get_resolved_auth(c)` (gathers it from the context keys the auth middleware set), and `revalidate_resolved_auth(deps, resolved)`, the no-touch re-read a long-lived connection runs once it is registered: session row / token row (by id) still live and still on the account, account not soft-deleted; a daemon token re-reads its account only. Reuses the middleware's own queries and predicates. Called by the WS upgrade (`actions/register_action_ws.ts`) and the audit SSE route between registering a connection pending and admitting it. Twin of the Rust `revalidate_resolved_auth`.
@@ -17,27 +17,30 @@ import type { MiddlewareSpec } from '../http/middleware_spec.js';
17
17
  *
18
18
  * `create_app_server` mounts the stack here, and refuses at assembly (via
19
19
  * `assert_endpoint_in_auth_scope`, which `create_test_app_surface_spec` also
20
- * runs) an `rpc_endpoints` or `ws_endpoints` path the pattern does not match (tested
21
- * with `middleware_applies`, which follows Hono: `'/api/*'` matches `/api`
22
- * and everything under `/api/`): an endpoint outside it would get no
23
- * session parsing, so every call or upgrade would be unauthenticated.
20
+ * runs) an `rpc_endpoints`, `ws_endpoints`, or bootstrap path the pattern does
21
+ * not match (tested with `middleware_applies`, which follows Hono: `'/api/*'`
22
+ * matches `/api` and everything under `/api/`): an endpoint outside it would
23
+ * get no origin check or session parsing, so every call or upgrade would be
24
+ * unauthenticated and any page could send it.
24
25
  */
25
26
  export declare const AUTH_MIDDLEWARE_PATH = "/api/*";
26
27
  /**
27
28
  * Throw when an endpoint path sits outside the auth middleware's scope
28
- * (`AUTH_MIDDLEWARE_PATH`) — there it would get no session or bearer
29
- * parsing, so every call or upgrade would be unauthenticated.
29
+ * (`AUTH_MIDDLEWARE_PATH`) — there it would get no origin check and no
30
+ * session or bearer parsing, so every call or upgrade would be
31
+ * unauthenticated and any page could send it.
30
32
  * `middleware_applies` matches the way Hono matches the middleware's pattern.
31
33
  *
32
34
  * `create_app_server` calls it for every `rpc_endpoints` and `ws_endpoints`
33
- * path at assembly, and `create_test_app_surface_spec` does the same, so a
34
- * surface snapshot never records an endpoint the real server refuses.
35
+ * path and the bootstrap route at assembly, and `create_test_app_surface_spec`
36
+ * does the same, so a surface snapshot never records an endpoint the real
37
+ * server refuses.
35
38
  *
36
39
  * @param option - the option the path came from, named in the error
37
40
  * @param path - the endpoint path to check
38
41
  * @throws Error when `path` is outside `AUTH_MIDDLEWARE_PATH`
39
42
  */
40
- export declare const assert_endpoint_in_auth_scope: (option: "rpc_endpoints" | "ws_endpoints", path: string) => void;
43
+ export declare const assert_endpoint_in_auth_scope: (option: "rpc_endpoints" | "ws_endpoints" | "bootstrap.route_prefix", path: string) => void;
41
44
  /**
42
45
  * Throw unless a middleware list still mounts every spec of the stack it was
43
46
  * built from — the trusted-proxy spec and the auth middleware — exactly once,
@@ -84,7 +87,7 @@ export interface AuthMiddlewareOptions {
84
87
  *
85
88
  * Returns `[origin, session, request_context, bearer_auth]` middleware specs
86
89
  * for the given path pattern. When `daemon_token_state` is provided, appends
87
- * a 5th `daemon_token` layer. Apps can append extra entries for non-standard
90
+ * a `daemon_token` layer. Apps can append extra entries for non-standard
88
91
  * paths (e.g., tx's `/tx` binary endpoint).
89
92
  *
90
93
  * @param deps - stateless capabilities (keyring, db)
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAIjE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,6BAA6B,GACzC,QAAQ,eAAe,GAAG,cAAc,EACxC,MAAM,MAAM,KACV,IAOF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,+BAA+B,GAC3C,aAAa,aAAa,CAAC,cAAc,CAAC,EAC1C,kBAAkB,aAAa,CAAC,cAAc,CAAC,KAC7C,IA+BF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CA4E/B,CAAC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAIjE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,6BAA6B,GACzC,QAAQ,eAAe,GAAG,cAAc,GAAG,wBAAwB,EACnE,MAAM,MAAM,KACV,IAQF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,+BAA+B,GAC3C,aAAa,aAAa,CAAC,cAAc,CAAC,EAC1C,kBAAkB,aAAa,CAAC,cAAc,CAAC,KAC7C,IA+BF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,OAAO,EACb,SAAS,qBAAqB,KAC5B,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CA4E/B,CAAC"}
@@ -15,21 +15,24 @@ import { middleware_applies } from '../http/schema_helpers.js';
15
15
  *
16
16
  * `create_app_server` mounts the stack here, and refuses at assembly (via
17
17
  * `assert_endpoint_in_auth_scope`, which `create_test_app_surface_spec` also
18
- * runs) an `rpc_endpoints` or `ws_endpoints` path the pattern does not match (tested
19
- * with `middleware_applies`, which follows Hono: `'/api/*'` matches `/api`
20
- * and everything under `/api/`): an endpoint outside it would get no
21
- * session parsing, so every call or upgrade would be unauthenticated.
18
+ * runs) an `rpc_endpoints`, `ws_endpoints`, or bootstrap path the pattern does
19
+ * not match (tested with `middleware_applies`, which follows Hono: `'/api/*'`
20
+ * matches `/api` and everything under `/api/`): an endpoint outside it would
21
+ * get no origin check or session parsing, so every call or upgrade would be
22
+ * unauthenticated and any page could send it.
22
23
  */
23
24
  export const AUTH_MIDDLEWARE_PATH = '/api/*';
24
25
  /**
25
26
  * Throw when an endpoint path sits outside the auth middleware's scope
26
- * (`AUTH_MIDDLEWARE_PATH`) — there it would get no session or bearer
27
- * parsing, so every call or upgrade would be unauthenticated.
27
+ * (`AUTH_MIDDLEWARE_PATH`) — there it would get no origin check and no
28
+ * session or bearer parsing, so every call or upgrade would be
29
+ * unauthenticated and any page could send it.
28
30
  * `middleware_applies` matches the way Hono matches the middleware's pattern.
29
31
  *
30
32
  * `create_app_server` calls it for every `rpc_endpoints` and `ws_endpoints`
31
- * path at assembly, and `create_test_app_surface_spec` does the same, so a
32
- * surface snapshot never records an endpoint the real server refuses.
33
+ * path and the bootstrap route at assembly, and `create_test_app_surface_spec`
34
+ * does the same, so a surface snapshot never records an endpoint the real
35
+ * server refuses.
33
36
  *
34
37
  * @param option - the option the path came from, named in the error
35
38
  * @param path - the endpoint path to check
@@ -38,7 +41,8 @@ export const AUTH_MIDDLEWARE_PATH = '/api/*';
38
41
  export const assert_endpoint_in_auth_scope = (option, path) => {
39
42
  if (!middleware_applies(AUTH_MIDDLEWARE_PATH, path)) {
40
43
  throw new Error(`${option} path ${path} is outside the auth middleware's scope ` +
41
- `(${AUTH_MIDDLEWARE_PATH}) — it would see every caller as unauthenticated — mount it under /api/`);
44
+ `(${AUTH_MIDDLEWARE_PATH}) — it would skip the origin check and see every caller ` +
45
+ `as unauthenticated — mount it under /api/`);
42
46
  }
43
47
  };
44
48
  /**
@@ -105,7 +109,7 @@ export const assert_middleware_stack_mounted = (stack_specs, middleware_specs) =
105
109
  *
106
110
  * Returns `[origin, session, request_context, bearer_auth]` middleware specs
107
111
  * for the given path pattern. When `daemon_token_state` is provided, appends
108
- * a 5th `daemon_token` layer. Apps can append extra entries for non-standard
112
+ * a `daemon_token` layer. Apps can append extra entries for non-standard
109
113
  * paths (e.g., tx's `/tx` binary endpoint).
110
114
  *
111
115
  * @param deps - stateless capabilities (keyring, db)
@@ -151,7 +151,7 @@ WS); see ../../../docs/architecture.md §DEV-only Output Validation.
151
151
  `http/error_schemas.ts` is the **declarative** error surface:
152
152
 
153
153
  - `ERROR_*` `snake_case` string constants — single source of truth; use `.literal(ERROR_*)` in Zod schemas and inline checks in handlers
154
- - `ApiError`, `ValidationError`, `PermissionError`, `CredentialTypeRequiredError`, `RateLimitError`, `PayloadTooLargeError`, `ForeignKeyError` — standard shapes
154
+ - `ApiError`, `ValidationError`, `PermissionError`, `CredentialTypeRequiredError`, `RateLimitError`, `PayloadTooLargeError`, `UnsupportedContentTypeError`, `ForeignKeyError` — standard shapes
155
155
  - `RouteErrorSchemas = Partial<Record<number, z.ZodType>>`
156
156
  - `RateLimitKey = 'ip' | 'account' | 'both'`
157
157
 
@@ -167,12 +167,13 @@ Pair every schema with the `z.infer` type export.
167
167
  `merge_error_schemas(spec, middleware_errors?)` (in `http/schema_helpers.ts`)
168
168
  merges three layers, later overrides earlier at the same status code:
169
169
 
170
- 1. **Derived** — from `derive_error_schemas({auth, has_input?, has_params?, has_query?, rate_limit?})`:
170
+ 1. **Derived** — from `derive_error_schemas({auth, has_input?, has_params?, has_query?, rate_limit?, checks_content_type?})`:
171
171
  - `has_input || has_params || has_query` → 400 `ValidationError`
172
172
  - `auth.account === 'required'` or `auth.actor === 'required'` → 401 `ApiError`
173
173
  - `auth.roles?.length` → 403 `PermissionError` (carries `required_roles`)
174
174
  - `auth.credential_types?.length` → 403 `CredentialTypeRequiredError` (carries `required_credential_types`; both gates set yields `z.union([PermissionError, CredentialTypeRequiredError])`)
175
175
  - `rate_limit` → 429 `RateLimitError`
176
+ - `checks_content_type` (every `POST`) → 415 `UnsupportedContentTypeError`
176
177
  - `auth.actor !== 'none'` → widens 400 to a union with `ActorRequiredError` / `ActorNotOnAccountError` and adds 500 union of `NoActorsOnAccountError` / `AccountVanishedError`. Mirrors what the dispatcher's authorization phase actually emits on routes whose input declares `acting?: ActingActor` (per registry-time invariant 2) — so DEV-mode error-schema validation doesn't reject the auth phase's body.
177
178
  2. **Middleware** — from `MiddlewareSpec.errors` that apply to the route's path (via `middleware_applies`)
178
179
  3. **Explicit** — `RouteSpec.errors` — always wins
@@ -55,6 +55,12 @@ export declare const ERROR_INVALID_CREDENTIALS: "invalid_credentials";
55
55
  export declare const ERROR_PAYLOAD_TOO_LARGE: "payload_too_large";
56
56
  /** Request origin not in allowlist. */
57
57
  export declare const ERROR_FORBIDDEN_ORIGIN: "forbidden_origin";
58
+ /**
59
+ * A `POST` whose `Content-Type` the route refuses: anything but
60
+ * `application/json` on a JSON route, a CORS-safelisted type or none on a
61
+ * `raw_body` one. See `http/simple_request.ts`.
62
+ */
63
+ export declare const ERROR_UNSUPPORTED_CONTENT_TYPE: "unsupported_content_type";
58
64
  /** Bearer token failed validation (missing, malformed, or revoked). */
59
65
  export declare const ERROR_INVALID_TOKEN: "invalid_token";
60
66
  /** Token references a deleted account. */
@@ -230,6 +236,11 @@ export declare const PayloadTooLargeError: z.ZodObject<{
230
236
  error: z.ZodLiteral<"payload_too_large">;
231
237
  }, z.core.$loose>;
232
238
  export type PayloadTooLargeError = z.infer<typeof PayloadTooLargeError>;
239
+ /** Unsupported content type error — returned when a `POST` route refuses the request's `Content-Type`. */
240
+ export declare const UnsupportedContentTypeError: z.ZodObject<{
241
+ error: z.ZodLiteral<"unsupported_content_type">;
242
+ }, z.core.$loose>;
243
+ export type UnsupportedContentTypeError = z.infer<typeof UnsupportedContentTypeError>;
233
244
  /** Foreign key violation error — returned when a delete is blocked by references. */
234
245
  export declare const ForeignKeyError: z.ZodObject<{
235
246
  error: z.ZodLiteral<"foreign_key_violation">;
@@ -320,6 +331,9 @@ export type RateLimitKey = z.infer<typeof RateLimitKey>;
320
331
  * of `NoActorsOnAccountError` / `AccountVanishedError`. The dispatcher's
321
332
  * authorization phase emits these whenever it tries to resolve an actor.
322
333
  * - **rate_limit**: 429 (`RateLimitError` with `retry_after`).
334
+ * - **checks_content_type**: 415 (`UnsupportedContentTypeError`) —
335
+ * every `POST` route (see `is_post_content_type_refused` in
336
+ * `http/simple_request.ts`).
323
337
  */
324
338
  export interface DeriveErrorSchemasOptions {
325
339
  auth: RouteAuth;
@@ -327,6 +341,7 @@ export interface DeriveErrorSchemasOptions {
327
341
  has_params?: boolean;
328
342
  has_query?: boolean;
329
343
  rate_limit?: RateLimitKey;
344
+ checks_content_type?: boolean;
330
345
  }
331
- export declare const derive_error_schemas: ({ auth, has_input, has_params, has_query, rate_limit }: DeriveErrorSchemasOptions) => RouteErrorSchemas;
346
+ export declare const derive_error_schemas: ({ auth, has_input, has_params, has_query, rate_limit, checks_content_type }: DeriveErrorSchemasOptions) => RouteErrorSchemas;
332
347
  //# sourceMappingURL=error_schemas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/error_schemas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"error_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/error_schemas.ts"],"names":[],"mappings":"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"}
@@ -58,6 +58,12 @@ export const ERROR_PAYLOAD_TOO_LARGE = 'payload_too_large';
58
58
  // --- Origin & bearer token verification ---
59
59
  /** Request origin not in allowlist. */
60
60
  export const ERROR_FORBIDDEN_ORIGIN = 'forbidden_origin';
61
+ /**
62
+ * A `POST` whose `Content-Type` the route refuses: anything but
63
+ * `application/json` on a JSON route, a CORS-safelisted type or none on a
64
+ * `raw_body` one. See `http/simple_request.ts`.
65
+ */
66
+ export const ERROR_UNSUPPORTED_CONTENT_TYPE = 'unsupported_content_type';
61
67
  /** Bearer token failed validation (missing, malformed, or revoked). */
62
68
  export const ERROR_INVALID_TOKEN = 'invalid_token';
63
69
  /** Token references a deleted account. */
@@ -233,6 +239,10 @@ export const RateLimitError = z.looseObject({
233
239
  export const PayloadTooLargeError = z.looseObject({
234
240
  error: z.literal(ERROR_PAYLOAD_TOO_LARGE)
235
241
  });
242
+ /** Unsupported content type error — returned when a `POST` route refuses the request's `Content-Type`. */
243
+ export const UnsupportedContentTypeError = z.looseObject({
244
+ error: z.literal(ERROR_UNSUPPORTED_CONTENT_TYPE)
245
+ });
236
246
  /** Foreign key violation error — returned when a delete is blocked by references. */
237
247
  export const ForeignKeyError = z.looseObject({
238
248
  error: z.literal(ERROR_FOREIGN_KEY_VIOLATION)
@@ -279,7 +289,7 @@ export const AccountVanishedError = z.looseObject({
279
289
  * - `'both'` — both keys.
280
290
  */
281
291
  export const RateLimitKey = z.enum(['ip', 'account', 'both']);
282
- export const derive_error_schemas = ({ auth, has_input = false, has_params = false, has_query = false, rate_limit }) => {
292
+ export const derive_error_schemas = ({ auth, has_input = false, has_params = false, has_query = false, rate_limit, checks_content_type = false }) => {
283
293
  const errors = {};
284
294
  const has_validation = has_input || has_params || has_query;
285
295
  if (needs_actor(auth)) {
@@ -320,5 +330,8 @@ export const derive_error_schemas = ({ auth, has_input = false, has_params = fal
320
330
  if (rate_limit) {
321
331
  errors[429] = RateLimitError;
322
332
  }
333
+ if (checks_content_type) {
334
+ errors[415] = UnsupportedContentTypeError;
335
+ }
323
336
  return errors;
324
337
  };
@@ -2,10 +2,13 @@
2
2
  * Request source verification middleware for API protection.
3
3
  *
4
4
  * Verifies requests are coming from expected origins/referers.
5
- * CSRF protection is provided by `SameSite: strict` on session cookies
6
- * (see `auth/session_middleware.ts`). This module provides origin allowlisting
7
- * for locally-running services — preventing untrusted websites from
8
- * making requests as the user browses the web.
5
+ * CSRF protection is `SameSite: strict` on session cookies
6
+ * (see `auth/session_middleware.ts`) plus the CORS simple-request refusal
7
+ * (`http/simple_request.ts`) — `SameSite` alone admits same-site pages, which
8
+ * include every other port on the same host. This module provides origin
9
+ * allowlisting for locally-running services — preventing untrusted websites
10
+ * from making requests as the user browses the web — and is the only gate on
11
+ * the WebSocket upgrade, which is never preflighted.
9
12
  *
10
13
  * @module
11
14
  */
@@ -30,7 +33,7 @@ export declare const is_browser_context: (c: Context) => boolean;
30
33
  /**
31
34
  * Parses FUZ_ALLOWED_ORIGINS env var into regex matchers for request source verification.
32
35
  * Origin allowlisting for locally-running services — not the CSRF layer
33
- * (that's `SameSite: strict` on session cookies).
36
+ * (that's `SameSite: strict` on session cookies plus `http/simple_request.ts`).
34
37
  *
35
38
  * Accepts comma-separated patterns with limited wildcards:
36
39
  * - Exact origins: `https://api.fuz.dev`
@@ -56,7 +59,8 @@ export declare const should_allow_origin: (origin: string, allowed_patterns: Rea
56
59
  /**
57
60
  * Middleware that verifies the request source against an allowlist.
58
61
  *
59
- * Origin allowlisting (not the CSRF layer — that's `SameSite: strict` cookies):
62
+ * Origin allowlisting (not the CSRF layer — that's `SameSite: strict` cookies
63
+ * plus `http/simple_request.ts`):
60
64
  * - Checks the `Origin` header (if present) against the allowlist
61
65
  * - Allows requests without an `Origin` header (direct access, curl, etc.)
62
66
  *
@@ -1 +1 @@
1
- {"version":3,"file":"origin.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAI7C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,GAAI,GAAG,OAAO,KAAG,OAC8B,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,KAAK,CAAC,MAAM,CAO5E,CAAC;AAEP;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC/B,QAAQ,MAAM,EACd,kBAAkB,aAAa,CAAC,MAAM,CAAC,KACrC,OAAuD,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,qBAAqB,GAChC,kBAAkB,aAAa,CAAC,MAAM,CAAC,KAAG,OAgB1C,CAAC"}
1
+ {"version":3,"file":"origin.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAI7C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,GAAI,GAAG,OAAO,KAAG,OAC8B,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,KAAK,CAAC,MAAM,CAO5E,CAAC;AAEP;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC/B,QAAQ,MAAM,EACd,kBAAkB,aAAa,CAAC,MAAM,CAAC,KACrC,OAAuD,CAAC;AAE3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,qBAAqB,GAChC,kBAAkB,aAAa,CAAC,MAAM,CAAC,KAAG,OAgB1C,CAAC"}
@@ -2,10 +2,13 @@
2
2
  * Request source verification middleware for API protection.
3
3
  *
4
4
  * Verifies requests are coming from expected origins/referers.
5
- * CSRF protection is provided by `SameSite: strict` on session cookies
6
- * (see `auth/session_middleware.ts`). This module provides origin allowlisting
7
- * for locally-running services — preventing untrusted websites from
8
- * making requests as the user browses the web.
5
+ * CSRF protection is `SameSite: strict` on session cookies
6
+ * (see `auth/session_middleware.ts`) plus the CORS simple-request refusal
7
+ * (`http/simple_request.ts`) — `SameSite` alone admits same-site pages, which
8
+ * include every other port on the same host. This module provides origin
9
+ * allowlisting for locally-running services — preventing untrusted websites
10
+ * from making requests as the user browses the web — and is the only gate on
11
+ * the WebSocket upgrade, which is never preflighted.
9
12
  *
10
13
  * @module
11
14
  */
@@ -31,7 +34,7 @@ export const is_browser_context = (c) => c.req.header('Origin') !== undefined ||
31
34
  /**
32
35
  * Parses FUZ_ALLOWED_ORIGINS env var into regex matchers for request source verification.
33
36
  * Origin allowlisting for locally-running services — not the CSRF layer
34
- * (that's `SameSite: strict` on session cookies).
37
+ * (that's `SameSite: strict` on session cookies plus `http/simple_request.ts`).
35
38
  *
36
39
  * Accepts comma-separated patterns with limited wildcards:
37
40
  * - Exact origins: `https://api.fuz.dev`
@@ -63,7 +66,8 @@ export const should_allow_origin = (origin, allowed_patterns) => allowed_pattern
63
66
  /**
64
67
  * Middleware that verifies the request source against an allowlist.
65
68
  *
66
- * Origin allowlisting (not the CSRF layer — that's `SameSite: strict` cookies):
69
+ * Origin allowlisting (not the CSRF layer — that's `SameSite: strict` cookies
70
+ * plus `http/simple_request.ts`):
67
71
  * - Checks the `Origin` header (if present) against the allowlist
68
72
  * - Allows requests without an `Origin` header (direct access, curl, etc.)
69
73
  *
@@ -51,8 +51,9 @@ export type AuthGuardResolver = (auth: RouteAuth) => AuthGuards;
51
51
  * and before input validation; resolves the acting actor (when `auth.actor !==
52
52
  * 'none'`) from the `acting` selector — `c.var.validated_query.acting` on GETs,
53
53
  * read off the raw body on mutations — and sets the request context on the Hono
54
- * context. Per-route order in `apply_route_specs`: params → query →
55
- * pre-authorization auth (401 + rule-3 scope) → authorization phase →
54
+ * context. Per-route order in `apply_route_specs`: content type (415) →
55
+ * params → query → pre-authorization auth (401 + rule-3 scope) →
56
+ * authorization phase →
56
57
  * post-authorization auth (403) → input validation (400) → handler.
57
58
  *
58
59
  * Returns a `Response` to short-circuit (resolution failure → 400 / 500),
@@ -138,12 +139,15 @@ export interface RouteSpec {
138
139
  * overloaded `input: z.null()`, which otherwise can't distinguish "no
139
140
  * body" (`GET /health`) from "raw bytes" (a binary upload).
140
141
  *
141
- * Purely descriptive metadata — the dispatcher doesn't read it. Its one
142
- * consumer is the schema-driven round-trip test suite, which auto-skips
143
- * `raw_body` routes (it can neither synthesize a meaningful body nor
144
- * assert a JSON output shape), so consumers no longer hand-maintain a
145
- * `skip_routes` entry per binary route. Also surfaces in `AppSurfaceRoute`
146
- * so generated docs render "raw" instead of a misleading `null` body.
142
+ * A `raw_body` `POST` takes any `Content-Type` a browser can't send
143
+ * cross-origin without a preflight; every other `POST` route requires
144
+ * `application/json`, bodyless or not (415 `unsupported_content_type`
145
+ * otherwise — see `http/simple_request.ts`). The schema-driven round-trip
146
+ * test suite auto-skips `raw_body` routes (it can neither synthesize a
147
+ * meaningful body nor assert a JSON output shape), so consumers don't
148
+ * hand-maintain a `skip_routes` entry per binary route. Also surfaces in
149
+ * `AppSurfaceRoute` so generated docs render "raw" instead of a
150
+ * misleading `null` body.
147
151
  */
148
152
  raw_body?: boolean;
149
153
  /**
@@ -228,10 +232,13 @@ export declare const apply_middleware_specs: (app: Hono, specs: Array<Middleware
228
232
  * validation, wraps with error catch layer (catches `ThrownJsonrpcError`
229
233
  * and generic errors), and registers the route.
230
234
  *
231
- * Per-route middleware order: params → query → pre-authorization auth
232
- * guards (401 + rule-3 scope) → authorization phase →
233
- * post-authorization auth guards (403) → input validation (400) →
234
- * handler. Body validation runs behind every authority gate, so a caller
235
+ * Per-route middleware order: content type (415, `POST` only) → params →
236
+ * query → pre-authorization auth guards (401 + rule-3 scope) →
237
+ * authorization phase → post-authorization auth guards (403) → input
238
+ * validation (400) → handler. The content type goes first because it says
239
+ * almost nothing about the route, and a forged request should be refused
240
+ * before any credential is read. Body
241
+ * validation runs behind every authority gate, so a caller
235
242
  * any of them refuse never sees route-shape information from a parse
236
243
  * failure — a 400 there would confirm the route exists and describe how
237
244
  * to call it. Params and query still validate first (they address the
@@ -1 +1 @@
1
- {"version":3,"file":"route_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/route_spec.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"route_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/route_spec.ts"],"names":[],"mappings":"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"}
@@ -13,7 +13,8 @@
13
13
  * @module
14
14
  */
15
15
  import { DEV } from 'esm-env';
16
- import { ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, ERROR_INVALID_ROUTE_PARAMS, ERROR_INVALID_QUERY_PARAMS } from './error_schemas.js';
16
+ import { ERROR_INVALID_JSON_BODY, ERROR_INVALID_REQUEST_BODY, ERROR_INVALID_ROUTE_PARAMS, ERROR_INVALID_QUERY_PARAMS, ERROR_UNSUPPORTED_CONTENT_TYPE } from './error_schemas.js';
17
+ import { is_post_content_type_refused } from './simple_request.js';
17
18
  import { ThrownJsonrpcError, jsonrpc_error_code_to_http_status, jsonrpc_error_code_to_name, dev_only } from './jsonrpc_errors.js';
18
19
  import { is_null_schema, merge_error_schemas } from './schema_helpers.js';
19
20
  import { dispatch_with_post_commit_rollback } from './pending_effects.js';
@@ -27,6 +28,28 @@ export function get_route_params(c, _schema) {
27
28
  export function get_route_query(c, _schema) {
28
29
  return c.get('validated_query');
29
30
  }
31
+ /**
32
+ * Create the content-type check for a route spec: a `POST` whose
33
+ * `Content-Type` the route refuses (`is_post_content_type_refused` — anything
34
+ * but JSON, or for a `raw_body` route anything a browser sends cross-origin
35
+ * without a preflight) gets 415 before anything else on the route runs. The
36
+ * Rust spine's JSON handlers check the same, first thing after the origin.
37
+ *
38
+ * Returns an empty array for other methods, which are never CORS-simple and
39
+ * so always preflighted.
40
+ */
41
+ const create_content_type_check = (spec) => {
42
+ if (spec.method !== 'POST')
43
+ return [];
44
+ const raw_body = spec.raw_body ?? false;
45
+ const check = async (c, next) => {
46
+ if (is_post_content_type_refused(c.req.header('content-type'), raw_body)) {
47
+ return c.json({ error: ERROR_UNSUPPORTED_CONTENT_TYPE }, 415);
48
+ }
49
+ await next();
50
+ };
51
+ return [check];
52
+ };
30
53
  /**
31
54
  * Create input validation middleware for a route spec.
32
55
  *
@@ -266,10 +289,13 @@ const build_rest_error_body = (err) => {
266
289
  * validation, wraps with error catch layer (catches `ThrownJsonrpcError`
267
290
  * and generic errors), and registers the route.
268
291
  *
269
- * Per-route middleware order: params → query → pre-authorization auth
270
- * guards (401 + rule-3 scope) → authorization phase →
271
- * post-authorization auth guards (403) → input validation (400) →
272
- * handler. Body validation runs behind every authority gate, so a caller
292
+ * Per-route middleware order: content type (415, `POST` only) → params →
293
+ * query → pre-authorization auth guards (401 + rule-3 scope) →
294
+ * authorization phase → post-authorization auth guards (403) → input
295
+ * validation (400) → handler. The content type goes first because it says
296
+ * almost nothing about the route, and a forged request should be refused
297
+ * before any credential is read. Body
298
+ * validation runs behind every authority gate, so a caller
273
299
  * any of them refuse never sees route-shape information from a parse
274
300
  * failure — a 400 there would confirm the route exists and describe how
275
301
  * to call it. Params and query still validate first (they address the
@@ -304,6 +330,7 @@ export const apply_route_specs = (app, specs, resolve_auth_guards, log, db, auth
304
330
  registered.add(route_key);
305
331
  assert_route_auth_acting_biconditional(spec.auth, { input: spec.input, query: spec.query }, `Route "${route_key}"`);
306
332
  const { pre_authorization: pre_authorization_guards, post_authorization: post_authorization_guards } = resolve_auth_guards(spec.auth);
333
+ const content_type_check = create_content_type_check(spec);
307
334
  const params_validation = create_params_validation(spec.params);
308
335
  const query_validation = create_query_validation(spec.query);
309
336
  const input_validation = create_input_validation(spec.input, spec.method);
@@ -340,7 +367,7 @@ export const apply_route_specs = (app, specs, resolve_auth_guards, log, db, auth
340
367
  handler = wrap_output_validation(handler, spec.output, merged_errors, log);
341
368
  // Step 3: error catch layer
342
369
  handler = wrap_error_catch(handler, log);
343
- app.on(spec.method, [spec.path], ...params_validation, ...query_validation, ...pre_authorization_guards, ...authorization, ...post_authorization_guards, ...input_validation, handler);
370
+ app.on(spec.method, [spec.path], ...content_type_check, ...params_validation, ...query_validation, ...pre_authorization_guards, ...authorization, ...post_authorization_guards, ...input_validation, handler);
344
371
  }
345
372
  };
346
373
  /**
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { z } from 'zod';
11
11
  import type { RouteAuth } from './auth_shape.js';
12
+ import type { RouteMethod } from './route_spec.js';
12
13
  import { type RateLimitKey, type RouteErrorSchemas } from './error_schemas.js';
13
14
  /**
14
15
  * Check if a schema is exactly `z.null()`.
@@ -111,11 +112,12 @@ export declare const middleware_applies: (mw_path: string, route_path: string) =
111
112
  * Merge order: derived -> middleware -> explicit route errors.
112
113
  * Later layers override earlier ones for the same status code.
113
114
  *
114
- * @param spec - the route spec (needs `auth`, `input`, `params`, `rate_limit`, `errors`)
115
+ * @param spec - the route spec (reads `method`, `auth`, `input`, `params`, `query`, `rate_limit`, `errors`)
115
116
  * @param middleware_errors - errors contributed by middleware whose path matches the route
116
117
  * @returns merged error schemas, or `null` if empty
117
118
  */
118
119
  export declare const merge_error_schemas: (spec: {
120
+ method: RouteMethod;
119
121
  auth: RouteAuth;
120
122
  input: z.ZodType;
121
123
  params?: z.ZodObject;
@@ -1 +1 @@
1
- {"version":3,"file":"schema_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/schema_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAEN,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAAsC,CAAC;AAE1F;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAAsC,CAAC;AAE1F;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,GAAI,YAAY,OAAO,KAAG,OAUpD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,CAAC,CAAC,OAAO,EAAE,YAAY,OAAO,KAAG,OACA,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,gCAAgC,GAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EACnE,QAAQ,CAAC,EACT,OAAO,OAAO,KACZ,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAclC,CAAC;AAWF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OACe,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAQrD,CAAC;AAoBF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,EAAE,YAAY,MAAM,KAAG,OAQxE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM;IACL,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC3B,EACD,oBAAoB,iBAAiB,GAAG,IAAI,KAC1C,iBAAiB,GAAG,IAUtB,CAAC"}
1
+ {"version":3,"file":"schema_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/schema_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAEN,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAAsC,CAAC;AAE1F;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAAsC,CAAC;AAE1F;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,GAAI,YAAY,OAAO,KAAG,OAUpD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,CAAC,CAAC,OAAO,EAAE,YAAY,OAAO,KAAG,OACA,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,gCAAgC,GAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EACnE,QAAQ,CAAC,EACT,OAAO,OAAO,KACZ,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAclC,CAAC;AAWF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OACe,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,OAQrD,CAAC;AAoBF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,EAAE,YAAY,MAAM,KAAG,OAQxE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM;IACL,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC3B,EACD,oBAAoB,iBAAiB,GAAG,IAAI,KAC1C,iBAAiB,GAAG,IAWtB,CAAC"}
@@ -185,7 +185,7 @@ export const middleware_applies = (mw_path, route_path) => {
185
185
  * Merge order: derived -> middleware -> explicit route errors.
186
186
  * Later layers override earlier ones for the same status code.
187
187
  *
188
- * @param spec - the route spec (needs `auth`, `input`, `params`, `rate_limit`, `errors`)
188
+ * @param spec - the route spec (reads `method`, `auth`, `input`, `params`, `query`, `rate_limit`, `errors`)
189
189
  * @param middleware_errors - errors contributed by middleware whose path matches the route
190
190
  * @returns merged error schemas, or `null` if empty
191
191
  */
@@ -195,7 +195,8 @@ export const merge_error_schemas = (spec, middleware_errors) => {
195
195
  has_input: !is_null_schema(spec.input),
196
196
  has_params: !!spec.params,
197
197
  has_query: !!spec.query,
198
- rate_limit: spec.rate_limit
198
+ rate_limit: spec.rate_limit,
199
+ checks_content_type: spec.method === 'POST'
199
200
  });
200
201
  const merged = { ...derived, ...middleware_errors, ...spec.errors };
201
202
  return Object.keys(merged).length > 0 ? merged : null;