@fuzdev/fuz_app 0.30.0 → 0.32.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 (222) hide show
  1. package/dist/actions/CLAUDE.md +630 -0
  2. package/dist/actions/action_rpc.d.ts +29 -0
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +42 -6
  5. package/dist/actions/action_types.d.ts +2 -2
  6. package/dist/actions/cancel.d.ts +12 -13
  7. package/dist/actions/cancel.d.ts.map +1 -1
  8. package/dist/actions/cancel.js +10 -13
  9. package/dist/actions/heartbeat.d.ts +8 -13
  10. package/dist/actions/heartbeat.d.ts.map +1 -1
  11. package/dist/actions/heartbeat.js +5 -8
  12. package/dist/actions/register_action_ws.d.ts +3 -3
  13. package/dist/actions/register_action_ws.js +2 -2
  14. package/dist/actions/register_ws_endpoint.d.ts +4 -4
  15. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  16. package/dist/actions/register_ws_endpoint.js +3 -3
  17. package/dist/actions/rpc_client.d.ts +29 -0
  18. package/dist/actions/rpc_client.d.ts.map +1 -1
  19. package/dist/actions/rpc_client.js +31 -0
  20. package/dist/actions/socket.svelte.d.ts +16 -16
  21. package/dist/actions/socket.svelte.d.ts.map +1 -1
  22. package/dist/actions/socket.svelte.js +15 -15
  23. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  24. package/dist/auth/CLAUDE.md +945 -0
  25. package/dist/auth/account_action_specs.d.ts +216 -0
  26. package/dist/auth/account_action_specs.d.ts.map +1 -0
  27. package/dist/auth/account_action_specs.js +159 -0
  28. package/dist/auth/account_actions.d.ts +51 -0
  29. package/dist/auth/account_actions.d.ts.map +1 -0
  30. package/dist/auth/account_actions.js +119 -0
  31. package/dist/auth/account_queries.d.ts +6 -2
  32. package/dist/auth/account_queries.d.ts.map +1 -1
  33. package/dist/auth/account_queries.js +40 -4
  34. package/dist/auth/account_routes.d.ts +94 -16
  35. package/dist/auth/account_routes.d.ts.map +1 -1
  36. package/dist/auth/account_routes.js +108 -180
  37. package/dist/auth/account_schema.d.ts +85 -30
  38. package/dist/auth/account_schema.d.ts.map +1 -1
  39. package/dist/auth/account_schema.js +40 -8
  40. package/dist/auth/admin_action_specs.d.ts +674 -0
  41. package/dist/auth/admin_action_specs.d.ts.map +1 -0
  42. package/dist/auth/admin_action_specs.js +287 -0
  43. package/dist/auth/admin_actions.d.ts +69 -0
  44. package/dist/auth/admin_actions.d.ts.map +1 -0
  45. package/dist/auth/admin_actions.js +256 -0
  46. package/dist/auth/admin_rpc_actions.d.ts +49 -0
  47. package/dist/auth/admin_rpc_actions.d.ts.map +1 -0
  48. package/dist/auth/admin_rpc_actions.js +32 -0
  49. package/dist/auth/api_token.d.ts +10 -0
  50. package/dist/auth/api_token.d.ts.map +1 -1
  51. package/dist/auth/api_token.js +9 -0
  52. package/dist/auth/api_token_queries.d.ts +3 -3
  53. package/dist/auth/api_token_queries.js +3 -3
  54. package/dist/auth/app_settings_schema.d.ts +4 -3
  55. package/dist/auth/app_settings_schema.d.ts.map +1 -1
  56. package/dist/auth/app_settings_schema.js +2 -1
  57. package/dist/auth/audit_log_routes.d.ts +14 -6
  58. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  59. package/dist/auth/audit_log_routes.js +22 -79
  60. package/dist/auth/audit_log_schema.d.ts +100 -29
  61. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  62. package/dist/auth/audit_log_schema.js +83 -11
  63. package/dist/auth/bootstrap_routes.d.ts +14 -0
  64. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  65. package/dist/auth/bootstrap_routes.js +10 -3
  66. package/dist/auth/cleanup.d.ts +63 -0
  67. package/dist/auth/cleanup.d.ts.map +1 -0
  68. package/dist/auth/cleanup.js +80 -0
  69. package/dist/auth/invite_schema.d.ts +11 -10
  70. package/dist/auth/invite_schema.d.ts.map +1 -1
  71. package/dist/auth/invite_schema.js +4 -3
  72. package/dist/auth/migrations.d.ts +6 -0
  73. package/dist/auth/migrations.d.ts.map +1 -1
  74. package/dist/auth/migrations.js +28 -0
  75. package/dist/auth/permit_offer_action_specs.d.ts +364 -0
  76. package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
  77. package/dist/auth/permit_offer_action_specs.js +216 -0
  78. package/dist/auth/permit_offer_actions.d.ts +96 -0
  79. package/dist/auth/permit_offer_actions.d.ts.map +1 -0
  80. package/dist/auth/permit_offer_actions.js +428 -0
  81. package/dist/auth/permit_offer_notifications.d.ts +361 -0
  82. package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
  83. package/dist/auth/permit_offer_notifications.js +179 -0
  84. package/dist/auth/permit_offer_queries.d.ts +165 -0
  85. package/dist/auth/permit_offer_queries.d.ts.map +1 -0
  86. package/dist/auth/permit_offer_queries.js +390 -0
  87. package/dist/auth/permit_offer_schema.d.ts +103 -0
  88. package/dist/auth/permit_offer_schema.d.ts.map +1 -0
  89. package/dist/auth/permit_offer_schema.js +142 -0
  90. package/dist/auth/permit_queries.d.ts +77 -14
  91. package/dist/auth/permit_queries.d.ts.map +1 -1
  92. package/dist/auth/permit_queries.js +119 -24
  93. package/dist/auth/session_queries.d.ts +4 -2
  94. package/dist/auth/session_queries.d.ts.map +1 -1
  95. package/dist/auth/session_queries.js +4 -2
  96. package/dist/auth/signup_routes.d.ts +13 -0
  97. package/dist/auth/signup_routes.d.ts.map +1 -1
  98. package/dist/auth/signup_routes.js +14 -7
  99. package/dist/http/CLAUDE.md +584 -0
  100. package/dist/http/pending_effects.d.ts +29 -0
  101. package/dist/http/pending_effects.d.ts.map +1 -0
  102. package/dist/http/pending_effects.js +31 -0
  103. package/dist/http/route_spec.d.ts.map +1 -1
  104. package/dist/http/route_spec.js +4 -3
  105. package/dist/rate_limiter.d.ts +30 -0
  106. package/dist/rate_limiter.d.ts.map +1 -1
  107. package/dist/rate_limiter.js +25 -2
  108. package/dist/realtime/sse_auth_guard.d.ts +2 -0
  109. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  110. package/dist/realtime/sse_auth_guard.js +5 -3
  111. package/dist/server/app_server.d.ts +13 -2
  112. package/dist/server/app_server.d.ts.map +1 -1
  113. package/dist/server/app_server.js +12 -1
  114. package/dist/testing/CLAUDE.md +668 -1
  115. package/dist/testing/admin_integration.d.ts +10 -7
  116. package/dist/testing/admin_integration.d.ts.map +1 -1
  117. package/dist/testing/admin_integration.js +382 -482
  118. package/dist/testing/app_server.d.ts +7 -6
  119. package/dist/testing/app_server.d.ts.map +1 -1
  120. package/dist/testing/attack_surface.d.ts +9 -3
  121. package/dist/testing/attack_surface.d.ts.map +1 -1
  122. package/dist/testing/attack_surface.js +4 -4
  123. package/dist/testing/audit_completeness.d.ts +11 -0
  124. package/dist/testing/audit_completeness.d.ts.map +1 -1
  125. package/dist/testing/audit_completeness.js +169 -134
  126. package/dist/testing/auth_apps.d.ts.map +1 -1
  127. package/dist/testing/auth_apps.js +4 -33
  128. package/dist/testing/db.d.ts +1 -1
  129. package/dist/testing/db.d.ts.map +1 -1
  130. package/dist/testing/db.js +2 -0
  131. package/dist/testing/entities.d.ts +35 -13
  132. package/dist/testing/entities.d.ts.map +1 -1
  133. package/dist/testing/entities.js +17 -0
  134. package/dist/testing/integration.d.ts +10 -0
  135. package/dist/testing/integration.d.ts.map +1 -1
  136. package/dist/testing/integration.js +352 -340
  137. package/dist/testing/integration_helpers.d.ts +16 -5
  138. package/dist/testing/integration_helpers.d.ts.map +1 -1
  139. package/dist/testing/integration_helpers.js +24 -4
  140. package/dist/testing/rate_limiting.d.ts +7 -0
  141. package/dist/testing/rate_limiting.d.ts.map +1 -1
  142. package/dist/testing/rate_limiting.js +41 -10
  143. package/dist/testing/rpc_helpers.d.ts +153 -1
  144. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  145. package/dist/testing/rpc_helpers.js +184 -8
  146. package/dist/testing/sse_round_trip.d.ts +8 -0
  147. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  148. package/dist/testing/sse_round_trip.js +10 -3
  149. package/dist/testing/standard.d.ts +9 -1
  150. package/dist/testing/standard.d.ts.map +1 -1
  151. package/dist/testing/standard.js +6 -2
  152. package/dist/testing/stubs.d.ts +10 -2
  153. package/dist/testing/stubs.d.ts.map +1 -1
  154. package/dist/testing/stubs.js +17 -2
  155. package/dist/testing/surface_invariants.d.ts +7 -3
  156. package/dist/testing/surface_invariants.d.ts.map +1 -1
  157. package/dist/testing/surface_invariants.js +5 -4
  158. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  159. package/dist/testing/ws_round_trip.js +9 -38
  160. package/dist/ui/AccountSessions.svelte +8 -4
  161. package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
  162. package/dist/ui/AdminAccounts.svelte +61 -33
  163. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  164. package/dist/ui/AdminAuditLog.svelte +3 -2
  165. package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
  166. package/dist/ui/AdminInvites.svelte +3 -2
  167. package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
  168. package/dist/ui/AdminOverview.svelte +14 -9
  169. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  170. package/dist/ui/AdminPermitHistory.svelte +3 -2
  171. package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
  172. package/dist/ui/AdminSessions.svelte +29 -25
  173. package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
  174. package/dist/ui/CLAUDE.md +363 -0
  175. package/dist/ui/OpenSignupToggle.svelte +6 -3
  176. package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
  177. package/dist/ui/PermitOfferForm.svelte +141 -0
  178. package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
  179. package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
  180. package/dist/ui/PermitOfferHistory.svelte +109 -0
  181. package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
  182. package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
  183. package/dist/ui/PermitOfferInbox.svelte +121 -0
  184. package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
  185. package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
  186. package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
  187. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
  188. package/dist/ui/account_sessions_state.svelte.js +39 -16
  189. package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
  190. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  191. package/dist/ui/admin_accounts_state.svelte.js +99 -23
  192. package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
  193. package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
  194. package/dist/ui/admin_invites_state.svelte.js +38 -26
  195. package/dist/ui/admin_rpc_adapters.d.ts +94 -0
  196. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -0
  197. package/dist/ui/admin_rpc_adapters.js +100 -0
  198. package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
  199. package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
  200. package/dist/ui/admin_sessions_state.svelte.js +35 -21
  201. package/dist/ui/app_settings_state.svelte.d.ts +39 -0
  202. package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
  203. package/dist/ui/app_settings_state.svelte.js +34 -18
  204. package/dist/ui/audit_log_state.svelte.d.ts +40 -3
  205. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  206. package/dist/ui/audit_log_state.svelte.js +36 -42
  207. package/dist/ui/auth_state.svelte.d.ts +4 -3
  208. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  209. package/dist/ui/auth_state.svelte.js +4 -1
  210. package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
  211. package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
  212. package/dist/ui/permit_offers_state.svelte.js +197 -0
  213. package/package.json +3 -3
  214. package/dist/auth/admin_routes.d.ts +0 -29
  215. package/dist/auth/admin_routes.d.ts.map +0 -1
  216. package/dist/auth/admin_routes.js +0 -226
  217. package/dist/auth/app_settings_routes.d.ts +0 -27
  218. package/dist/auth/app_settings_routes.d.ts.map +0 -1
  219. package/dist/auth/app_settings_routes.js +0 -66
  220. package/dist/auth/invite_routes.d.ts +0 -18
  221. package/dist/auth/invite_routes.d.ts.map +0 -1
  222. package/dist/auth/invite_routes.js +0 -129
@@ -0,0 +1,584 @@
1
+ # http/
2
+
3
+ Generic HTTP framework infrastructure — route specs, error schemas, attack
4
+ surface introspection, JSON-RPC envelope + error taxonomy, proxy/origin
5
+ middleware primitives, post-commit effect helper, generic admin route specs.
6
+
7
+ **Nothing in this directory is auth-specific.** Auth middleware, routes, and
8
+ guards live in `../auth/` and consume these primitives. Routes and actions in
9
+ other domains should do the same — extend, don't special-case.
10
+
11
+ For the design rationale behind declarative routes, DEV-only output
12
+ validation, the three-layer error-schema merge, and fire-and-forget effects,
13
+ see `../../docs/architecture.md`.
14
+
15
+ ## Module Map
16
+
17
+ | File | Role |
18
+ | -------------------- | ------------------------------------------------------------------------- |
19
+ | `route_spec.ts` | `RouteSpec` + `apply_route_specs`, validation pipeline, transactions |
20
+ | `error_schemas.ts` | `ERROR_*` constants, standard error shapes, `derive_error_schemas` |
21
+ | `schema_helpers.ts` | Shared Zod introspection (null/strict/surface/merge/middleware-applies) |
22
+ | `middleware_spec.ts` | `MiddlewareSpec` interface |
23
+ | `surface.ts` | `AppSurface`, `AppSurfaceSpec`, `generate_app_surface`, diagnostics |
24
+ | `surface_query.ts` | Pure filters/groupings over `AppSurface` |
25
+ | `proxy.ts` | Trusted-proxy middleware, CIDR parsing, rightmost-first XFF resolution |
26
+ | `origin.ts` | Origin/Referer allowlist middleware with wildcard patterns |
27
+ | `jsonrpc.ts` | JSON-RPC 2.0 envelope schemas (MCP superset), `JsonrpcErrorCode`, `_meta` |
28
+ | `jsonrpc_errors.ts` | `ThrownJsonrpcError`, `jsonrpc_errors` throwers, HTTP-status mappings |
29
+ | `jsonrpc_helpers.ts` | Message builders, type guards, input/result normalizers |
30
+ | `common_routes.ts` | Health check + authenticated server-status + surface route specs |
31
+ | `db_routes.ts` | Generic keeper-only table browser route specs (public schema) |
32
+ | `pending_effects.ts` | `emit_after_commit(ctx, fn)` + `PendingEffectsContext` |
33
+
34
+ ## Route Spec System
35
+
36
+ `RouteSpec` (in `route_spec.ts`) is the unit of the attack surface — routes
37
+ are **data**, registered with Hono by `apply_route_specs`, and introspected
38
+ by `generate_app_surface`. Same-shaped data, different consumers.
39
+
40
+ ### `RouteSpec` fields
41
+
42
+ - `method` — `'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'`
43
+ - `path` — Hono path (supports `:param` segments)
44
+ - `auth: RouteAuth` — `{type: 'none' | 'authenticated' | 'role'; role} | {type: 'keeper'}`
45
+ - `handler: RouteHandler` — `(c: Context, route: RouteContext) => Response | Promise<Response>`
46
+ - `description` — free-text, surfaced in `AppSurface`
47
+ - `params?: z.ZodObject` — strict-object schema for URL path params
48
+ - `query?: z.ZodObject` — strict-object schema for URL query string
49
+ - `input: z.ZodType` — request body schema; `z.null()` for no-body (GET/DELETE)
50
+ - `output: z.ZodType` — success response schema
51
+ - `rate_limit?: RateLimitKey` — metadata only (`'ip' | 'account' | 'both'`); auto-derives 429
52
+ - `errors?: RouteErrorSchemas` — handler-specific error schemas keyed by HTTP status
53
+ - `transaction?: boolean` — declarative transaction wrapping (see below)
54
+
55
+ Input/output naming mirrors SAES `ActionSpec`. Use `z.strictObject()` for
56
+ inputs — the surface diagnostic warns on non-strict objects because unknown
57
+ keys are silently stripped by Zod 4's default `z.object()`.
58
+
59
+ ### `RouteContext` — per-request deps
60
+
61
+ The second handler argument is always a `RouteContext`:
62
+
63
+ ```typescript
64
+ interface RouteContext {
65
+ db: Db; // transaction-scoped for mutations, pool-level for reads
66
+ background_db: Db; // always pool-level — for fire-and-forget effects
67
+ pending_effects: Array<Promise<void>>;
68
+ }
69
+ ```
70
+
71
+ - **`route.db`** — use for the handler's main DB work. Wrapped in a transaction
72
+ when `transaction: true` (the default for non-GET). Do NOT use for
73
+ fire-and-forget effects that must outlive the transaction.
74
+ - **`route.background_db`** — use for audit logs, session touches, token
75
+ tracking. Always pool-level, never rolled back.
76
+ - **`route.pending_effects`** — push promises for post-response flushing.
77
+ Prefer `emit_after_commit` from `pending_effects.ts` for WS fan-out.
78
+
79
+ ### Declarative transactions
80
+
81
+ `RouteSpec.transaction` defaults by method:
82
+
83
+ - `GET` → `false` (read-only, no transaction)
84
+ - All mutations (POST, PUT, DELETE, PATCH) → `true`
85
+
86
+ Override explicitly when a mutation route must manage its own transactions
87
+ (e.g. signup, which does a multi-step flow that can't live inside a single
88
+ wrapper). See `../auth/signup_routes.ts`.
89
+
90
+ ### Validation pipeline (per-route middleware order)
91
+
92
+ `apply_route_specs` assembles the following middleware chain per spec:
93
+
94
+ 1. **Auth guards** — `resolve_auth_guards(spec.auth)`, injected via
95
+ `AuthGuardResolver` (use `fuz_auth_guard_resolver` from `../auth/route_guards.ts`)
96
+ 2. **Params validation** — `spec.params` → `validated_params` context var;
97
+ mismatch returns 400 `ERROR_INVALID_ROUTE_PARAMS` with Zod `issues`
98
+ 3. **Query validation** — `spec.query` → `validated_query`; mismatch returns
99
+ 400 `ERROR_INVALID_QUERY_PARAMS`
100
+ 4. **Input validation** — JSON body parsed + validated; mismatch returns 400
101
+ `ERROR_INVALID_JSON_BODY` (not JSON) or `ERROR_INVALID_REQUEST_BODY`
102
+ (schema failure with `issues`). Skipped on GET and `z.null()` inputs
103
+ 5. **Handler** — wrapped in transaction when `use_transaction` (see above),
104
+ receives `RouteContext`
105
+ 6. **DEV-only output + error validation** — wraps the handler (see below)
106
+ 7. **Error catch** — catches `ThrownJsonrpcError` → maps to HTTP status +
107
+ JSON-RPC error body; catches generic `Error` → 500 `internal_error`
108
+ (message only included in DEV)
109
+
110
+ Duplicate `method path` pairs throw at registration.
111
+
112
+ Validated values are accessed via `get_route_input<T>(c)`,
113
+ `get_route_params<T>(c)`, `get_route_query<T>(c)` — typed helpers that
114
+ read the `validated_*` context vars.
115
+
116
+ ### DEV-only output + error validation
117
+
118
+ **Input schemas are validated unconditionally** (DEV + production) — they
119
+ are the contract with external callers.
120
+
121
+ **Output and error schemas are validated DEV-only** via `DEV` from
122
+ `esm-env`. `wrap_output_validation`:
123
+
124
+ - Skips streaming responses (non-`application/json` Content-Type) so SSE
125
+ doesn't hang on `.json()`
126
+ - On 2xx JSON: validates body against `spec.output`
127
+ - On non-2xx JSON: validates body against the merged error schema for
128
+ that HTTP status
129
+ - **Logs on mismatch, returns the response unchanged** — never throws,
130
+ never mutates the body
131
+
132
+ The production behavior short-circuits to the unwrapped handler — no
133
+ parse work on the hot path. Uniform across all three action-handler
134
+ surfaces (REST, RPC, WS); see `../../docs/architecture.md` §DEV-only
135
+ Output Validation.
136
+
137
+ ### Helpers
138
+
139
+ - `apply_middleware_specs(app, specs)` — registers middleware specs on
140
+ Hono by `{name, path, handler}`
141
+ - `prefix_route_specs(prefix, specs)` — prepends a path prefix to every
142
+ spec; `/` collapses to the bare prefix
143
+
144
+ ## Error Schemas
145
+
146
+ `error_schemas.ts` is the **declarative** error surface:
147
+
148
+ - `ERROR_*` snake*case string constants — single source of truth; use
149
+ `.literal(ERROR*\*)` in Zod schemas and inline checks in handlers
150
+ - `ApiError`, `ValidationError`, `PermissionError`, `KeeperError`,
151
+ `RateLimitError`, `PayloadTooLargeError`, `ForeignKeyError` — standard
152
+ shapes
153
+ - `RouteErrorSchemas = Partial<Record<number, z.ZodType>>`
154
+ - `RateLimitKey = 'ip' | 'account' | 'both'`
155
+
156
+ All standard shapes use `z.looseObject` — intentional because multiple
157
+ producers (middleware + handler) can emit different extra fields at the
158
+ same status code. The `error` string literal is the contract; extra keys
159
+ (`required_role`, `retry_after`, `detail`) are diagnostic.
160
+
161
+ Pair every schema with the `z.infer` type export (`export type ApiError = z.infer<typeof ApiError>`).
162
+
163
+ ### Three-layer error-schema merge
164
+
165
+ `merge_error_schemas(spec, middleware_errors?)` (in `schema_helpers.ts`)
166
+ merges three layers, later overrides earlier at the same status code:
167
+
168
+ 1. **Derived** — from `derive_error_schemas(auth, has_input, has_params, has_query, rate_limit)`:
169
+ - `has_input || has_params || has_query` → 400 `ValidationError`
170
+ - `auth.type === 'authenticated'` → 401 `ApiError`
171
+ - `auth.type === 'role'` → 401 `ApiError` + 403 `PermissionError`
172
+ - `auth.type === 'keeper'` → 401 `ApiError` + 403 `KeeperError`
173
+ - `rate_limit` → 429 `RateLimitError`
174
+ 2. **Middleware** — from `MiddlewareSpec.errors` that apply to the route's
175
+ path (via `middleware_applies`)
176
+ 3. **Explicit** — `RouteSpec.errors` — always wins
177
+
178
+ Routes typically only need `errors` for handler-specific codes (404, 409, 422).
179
+
180
+ ### `ERROR_*` constants by category
181
+
182
+ - **Validation**: `ERROR_INVALID_REQUEST_BODY`, `ERROR_INVALID_JSON_BODY`,
183
+ `ERROR_INVALID_ROUTE_PARAMS`, `ERROR_INVALID_QUERY_PARAMS`
184
+ - **Auth**: `ERROR_AUTHENTICATION_REQUIRED`, `ERROR_INSUFFICIENT_PERMISSIONS`,
185
+ `ERROR_RATE_LIMIT_EXCEEDED`, `ERROR_INVALID_CREDENTIALS`,
186
+ `ERROR_PAYLOAD_TOO_LARGE`
187
+ - **Origin + bearer**: `ERROR_FORBIDDEN_ORIGIN`, `ERROR_FORBIDDEN_REFERER`,
188
+ `ERROR_BEARER_REJECTED_BROWSER`, `ERROR_INVALID_TOKEN`, `ERROR_ACCOUNT_NOT_FOUND`
189
+ - **Keeper/daemon**: `ERROR_KEEPER_REQUIRES_DAEMON_TOKEN`,
190
+ `ERROR_INVALID_DAEMON_TOKEN`, `ERROR_KEEPER_ACCOUNT_NOT_CONFIGURED`,
191
+ `ERROR_KEEPER_ACCOUNT_NOT_FOUND`
192
+ - **Bootstrap**: `ERROR_ALREADY_BOOTSTRAPPED`, `ERROR_TOKEN_FILE_MISSING`,
193
+ `ERROR_BOOTSTRAP_NOT_CONFIGURED`
194
+ - **Signup/invites**: `ERROR_NO_MATCHING_INVITE`, `ERROR_SIGNUP_CONFLICT`,
195
+ `ERROR_INVITE_NOT_FOUND`, `ERROR_INVITE_MISSING_IDENTIFIER`,
196
+ `ERROR_INVITE_DUPLICATE`, `ERROR_INVITE_ACCOUNT_EXISTS_USERNAME`,
197
+ `ERROR_INVITE_ACCOUNT_EXISTS_EMAIL`
198
+ - **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_PERMIT_NOT_FOUND`,
199
+ `ERROR_INVALID_EVENT_TYPE`
200
+ - **DB browser**: `ERROR_FOREIGN_KEY_VIOLATION`, `ERROR_TABLE_NOT_FOUND`,
201
+ `ERROR_TABLE_NO_PRIMARY_KEY`, `ERROR_ROW_NOT_FOUND`
202
+
203
+ ## Schema Helpers
204
+
205
+ `schema_helpers.ts` is the canonical home for shared Zod introspection —
206
+ extracted to break a circular dependency between `route_spec.ts` (uses
207
+ them for input validation) and `surface.ts` (uses them for surface
208
+ generation).
209
+
210
+ **Import `is_null_schema`, `is_strict_object_schema`, `schema_to_surface`,
211
+ `middleware_applies`, and `merge_error_schemas` from `schema_helpers.ts`,
212
+ not from `surface.ts`.** The helpers were moved; `surface.ts` only imports
213
+ and re-uses them for generation logic.
214
+
215
+ Key helpers:
216
+
217
+ - `is_null_schema(schema)` — `instanceof z.ZodNull`. Uses `instanceof`, not
218
+ parse-null, to avoid false positives from `z.nullable(z.string())`
219
+ - `is_strict_object_schema(schema)` — detects `z.strictObject()` by
220
+ checking `schema.def.catchall instanceof z.ZodNever`
221
+ - `schema_to_surface(schema)` — Zod → JSON Schema, with `$schema` and
222
+ `default` keys stripped recursively (defaults may be non-deterministic
223
+ and `$schema` is snapshot noise)
224
+ - `middleware_applies(mw_path, route_path)` — Hono pattern matching:
225
+ `'*'`, exact, `'/api/*'` prefix (handles `prefix.slice(0, -1)` so
226
+ `/api/*` also matches the bare `/api`)
227
+ - `merge_error_schemas(spec, middleware_errors?)` — three-layer merge
228
+ described above
229
+
230
+ ## Surface Generation
231
+
232
+ `surface.ts` produces a JSON-serializable attack surface from middleware
233
+
234
+ - route + RPC + env + event specs. Used for startup logging, snapshot
235
+ testing, surface explorer UI, adversarial test generation, and policy
236
+ invariants.
237
+
238
+ ### Types
239
+
240
+ - `AppSurface` — JSON-serializable output (`middleware`, `routes`,
241
+ `rpc_endpoints`, `env`, `events`, `diagnostics`)
242
+ - `AppSurfaceSpec` — the surface bundled with the **source specs** that
243
+ produced it (`surface`, `route_specs`, `middleware_specs`, `rpc_endpoints`).
244
+ Runtime-only — use for tests and introspection
245
+ - `AppSurfaceRoute`, `AppSurfaceMiddleware`, `AppSurfaceEnv`,
246
+ `AppSurfaceEvent`, `AppSurfaceRpcEndpoint`, `AppSurfaceRpcMethod` —
247
+ per-entity entries
248
+ - `AppSurfaceDiagnostic` — `{level: 'warning' | 'info'; category; message; source?}`
249
+ - `RpcEndpointSpec` — `{path, actions: Array<RpcAction>}`; fed into
250
+ `generate_app_surface` so RPC endpoints appear in the surface without
251
+ coupling to `create_rpc_endpoint`
252
+ - `GenerateAppSurfaceOptions` — `{route_specs, middleware_specs, env_schema?, event_specs?, rpc_endpoints?}`
253
+
254
+ ### `generate_app_surface(options)` behavior
255
+
256
+ - Emits a `warning` diagnostic for every input schema that's not strict —
257
+ unknown keys silently strip under `z.object()`
258
+ - Per-route error schemas: runs the three-layer merge (derived + middleware
259
+ - explicit) via `merge_error_schemas` + `collect_middleware_errors`
260
+ - Per-route `is_mutation` = `method !== 'GET'`
261
+ - Per-route `transaction` mirrors the handler default (`spec.transaction ?? method !== 'GET'`)
262
+ - `env_schema_to_surface(schema)` reads `SchemaFieldMeta` from `.meta()`
263
+ — `description`, `sensitivity`, and probes `safeParse(undefined)` to
264
+ detect `optional` + `has_default`
265
+ - `events_to_surface(event_specs)` — SSE events surface as `{method, description, channel, params_schema}`
266
+ - RPC methods surface through `map_action_auth` (from `actions/action_bridge.ts`;
267
+ see `../actions/CLAUDE.md` §HTTP bridge) so `ActionAuth` translates to the shared `RouteAuth` shape
268
+
269
+ `create_app_surface_spec(options)` = `generate_app_surface(options)` plus
270
+ the source specs, for tests that need to iterate over raw specs.
271
+
272
+ ### `surface_query.ts` — pure queries
273
+
274
+ No side effects, no state — filters and groupings over `AppSurface`:
275
+
276
+ - `filter_protected_routes` / `filter_public_routes`
277
+ - `filter_role_routes` / `filter_authenticated_routes` / `filter_keeper_routes` / `filter_routes_for_role(role)`
278
+ - `filter_routes_by_prefix(prefix)` / `filter_routes_with_input` /
279
+ `filter_routes_with_params` / `filter_routes_with_query` /
280
+ `filter_mutation_routes` / `filter_rate_limited_routes`
281
+ - `routes_by_auth_type(surface)` — `Map<'none' | 'authenticated' | 'keeper' | 'role:NAME', Array<AppSurfaceRoute>>`
282
+ - `format_route_key(route)` → `'METHOD /path'`
283
+ - `surface_auth_summary(surface)` — counts per auth type, roles broken
284
+ out by name
285
+
286
+ Consumer code (tests, attack-surface helpers, `SurfaceExplorer.svelte`)
287
+ should reach for these rather than inlining `.filter` chains.
288
+
289
+ ## Middleware Infrastructure
290
+
291
+ ### `MiddlewareSpec`
292
+
293
+ ```typescript
294
+ interface MiddlewareSpec {
295
+ name: string;
296
+ path: string; // Hono pattern — '*', exact, or '/api/*'
297
+ handler: MiddlewareHandler;
298
+ errors?: RouteErrorSchemas; // schemas this layer may emit, keyed by status
299
+ }
300
+ ```
301
+
302
+ Declared in `middleware_spec.ts` (separate from `route_spec.ts` so
303
+ middleware modules don't pull in route types).
304
+
305
+ ### Trusted proxy — `proxy.ts`
306
+
307
+ Resolves the real client IP from `X-Forwarded-For` only when the TCP
308
+ connection is from a configured trusted proxy. Without this middleware,
309
+ `get_client_ip(c)` returns `'unknown'`.
310
+
311
+ Must run **before** auth and rate-limiting middleware. See the root
312
+ `../../CLAUDE.md` §Middleware Ordering.
313
+
314
+ - `normalize_ip(ip)` — idempotent: lowercase + strip `::ffff:` prefix on
315
+ IPv4-mapped IPv6 addresses; safe on non-IP strings (`'unknown'` → `'unknown'`).
316
+ Subtle: only strips `::ffff:` when the suffix contains `.`, so pure
317
+ IPv6 like `::ffff:1` is preserved
318
+ - `ProxyOptions` — `{trusted_proxies, get_connection_ip, log?}`
319
+ - `ParsedProxy` — `{type: 'ip'; address}` or `{type: 'cidr'; network; prefix; address_type}`
320
+ - `parse_proxy_entry(entry)` — accepts `'127.0.0.1'`, `'::1'`,
321
+ `'10.0.0.0/8'`, `'fe80::/10'`. Throws on invalid IPs, NaN/negative/
322
+ over-range prefix, non-network-aligned CIDRs, or bad input
323
+ - `is_trusted_ip(ip, proxies)` — normalizes before matching; skips
324
+ mismatched address families for CIDR matches
325
+ - `resolve_client_ip(forwarded_for, proxies)` — walks **right-to-left**,
326
+ skipping trusted entries. First untrusted wins. If all entries are
327
+ trusted, returns the leftmost (edge case, likely misconfigured)
328
+ - `create_proxy_middleware(options)` + `create_proxy_middleware_spec(options)` —
329
+ three-branch logic:
330
+ 1. No XFF → use connection IP directly
331
+ 2. XFF present + connection untrusted → ignore XFF (spoof-proof), use
332
+ connection IP, log debug
333
+ 3. XFF present + connection trusted → resolve from header, log warn if
334
+ all XFF entries turn out to be trusted
335
+ - `get_client_ip(c)` — returns `'unknown'` when the proxy middleware
336
+ hasn't run
337
+
338
+ Non-standard proxies that include ports in XFF entries (`203.0.113.1:8080`)
339
+ fail `distinctRemoteAddr` and are treated as untrusted — safe default but
340
+ rate-limiting keys the port-suffixed string. nginx and cloud LBs don't do
341
+ this.
342
+
343
+ ### Origin/Referer allowlist — `origin.ts`
344
+
345
+ Origin allowlisting for locally-running services — **not** the CSRF
346
+ layer. CSRF is handled by `SameSite: strict` on session cookies (see
347
+ `../auth/session_middleware.ts`).
348
+
349
+ - `parse_allowed_origins(env_value)` — comma-separated patterns → `Array<RegExp>`
350
+ - `should_allow_origin(origin, patterns)` — case-insensitive match
351
+ - `verify_request_source(allowed_patterns)` — Hono handler:
352
+ 1. `Origin` header present → must match allowlist or 403 `ERROR_FORBIDDEN_ORIGIN`
353
+ 2. No `Origin` + `Referer` present → extract origin, check, 403
354
+ `ERROR_FORBIDDEN_REFERER` on mismatch
355
+ 3. Neither header → allow through (curl, CLI, token auth is primary control)
356
+
357
+ Pattern syntax:
358
+
359
+ - Exact: `https://api.fuz.dev`
360
+ - Wildcard subdomain (complete label only): `https://*.fuz.dev` —
361
+ matches `api.fuz.dev`, NOT `fuz.dev`
362
+ - Multiple wildcards: `https://*.*.corp.fuz.dev` matches `api.staging.corp.fuz.dev`
363
+ - Port wildcard: `http://localhost:*` (optional port, matches with or without)
364
+ - IPv6: `http://[::1]:3000`, `https://[2001:db8::1]` (no wildcards inside brackets)
365
+ - Combined: `https://*.fuz.dev:*`
366
+
367
+ Patterns normalize through the `URL` constructor — IPv4-mapped IPv6 like
368
+ `[::ffff:127.0.0.1]` becomes `[::ffff:7f00:1]`. IPv6 zone identifiers
369
+ (`%eth0`) are not supported. Throws on paths, partial wildcards
370
+ (`*fuz.dev`), wildcards inside IPv6 brackets, or missing protocol.
371
+
372
+ ## JSON-RPC
373
+
374
+ Three files, split by concern:
375
+
376
+ - `jsonrpc.ts` — **declarative**: Zod schemas for the envelope and error codes
377
+ - `jsonrpc_errors.ts` — **runtime**: throwable errors, named constructors,
378
+ HTTP-status mapping
379
+ - `jsonrpc_helpers.ts` — **plumbing**: message builders, type guards, converters
380
+
381
+ Follows the JSON-RPC 2.0 spec and is an **MCP superset** — params and
382
+ result are always object-only (no positional arrays), `_meta` and
383
+ `progressToken` are first-class. The schemas are sourced from the MCP
384
+ TypeScript SDK for compatibility.
385
+
386
+ ### `jsonrpc.ts` — envelope + code schemas
387
+
388
+ `JSONRPC_VERSION = '2.0'` plus Zod schemas paired with inferred types:
389
+
390
+ - `JsonrpcRequestId`, `JsonrpcMethod`, `JsonrpcProgressToken`
391
+ - `JsonrpcMcpMeta` — `z.looseObject({})` — the MCP `_meta` extension point
392
+ - `JsonrpcRequestParamsMeta` — `JsonrpcMcpMeta.extend({progressToken: ...})`
393
+ - `JsonrpcRequestParams`, `JsonrpcNotificationParams`, `JsonrpcResult` — loose
394
+ - `JsonrpcRequest`, `JsonrpcNotification`, `JsonrpcResponse`,
395
+ `JsonrpcErrorResponse`, `JsonrpcResponseOrError`, `JsonrpcMessage`
396
+ - `JsonrpcMessageFromClientToServer`, `JsonrpcMessageFromServerToClient`
397
+
398
+ `_meta` is intentionally **not** envelope-validated — that lives in
399
+ per-action schemas so mismatches surface as `invalid_params` rather than
400
+ `invalid_request`.
401
+
402
+ Error codes:
403
+
404
+ - Standard constants: `JSONRPC_PARSE_ERROR` (-32700), `JSONRPC_INVALID_REQUEST`
405
+ (-32600), `JSONRPC_METHOD_NOT_FOUND` (-32601), `JSONRPC_INVALID_PARAMS`
406
+ (-32602), `JSONRPC_INTERNAL_ERROR` (-32603)
407
+ - Server-defined range: `JSONRPC_SERVER_ERROR_START = -32000`,
408
+ `JSONRPC_SERVER_ERROR_END = -32099`; `JsonrpcServerErrorCode` is a
409
+ branded Zod number in that range
410
+ - `JsonrpcErrorCode` — union of the 5 literals + `JsonrpcServerErrorCode`
411
+ - `JsonrpcErrorObject` — `{code, message, data?}`
412
+
413
+ ### `jsonrpc_errors.ts` — 15-code taxonomy
414
+
415
+ Runtime complement to `error_schemas.ts`. Five standard codes + ten
416
+ general application codes (consumers add their own by casting
417
+ `as JsonrpcErrorCode`):
418
+
419
+ | Name | Code | HTTP | Use |
420
+ | --------------------- | ------ | ---- | ------------------------------------------------------ |
421
+ | `parse_error` | -32700 | 400 | JSON parse failure |
422
+ | `invalid_request` | -32600 | 400 | Envelope malformed |
423
+ | `method_not_found` | -32601 | 404 | Unknown RPC method |
424
+ | `invalid_params` | -32602 | 400 | Params schema failure |
425
+ | `internal_error` | -32603 | 500 | Unhandled exception |
426
+ | `unauthenticated` | -32001 | 401 | HTTP 401 renamed ("unauthorized" is wrong for 401) |
427
+ | `forbidden` | -32002 | 403 | Authorized but denied |
428
+ | `not_found` | -32003 | 404 | Resource not found |
429
+ | `conflict` | -32004 | 409 | Uniqueness/state conflict |
430
+ | `validation_error` | -32005 | 422 | **Application-level** validation (business logic) |
431
+ | `rate_limited` | -32006 | 429 | Server-side policy |
432
+ | `service_unavailable` | -32007 | 503 | Upstream down / maintenance |
433
+ | `timeout` | -32008 | 504 | Handler exceeded time budget |
434
+ | `queue_overflow` | -32009 | 429 | **Client-side** backpressure (WS reconnect queue full) |
435
+ | `request_cancelled` | -32010 | 499 | Caller-initiated cancellation (nginx "client closed") |
436
+
437
+ `invalid_params` vs `validation_error`: use `invalid_params` (standard
438
+ code) for Zod parse failures; reserve `validation_error` (app code) for
439
+ business rules. `rate_limited` vs `queue_overflow`: both 429, but the
440
+ reverse map `HTTP_STATUS_TO_JSONRPC_ERROR_CODE[429] = rate_limited`
441
+ because rate limiting is the default interpretation when translating
442
+ generic HTTP back to a JSON-RPC code.
443
+
444
+ APIs:
445
+
446
+ - `JSONRPC_ERROR_CODES` — `Record<JsonrpcErrorName, JsonrpcErrorCode>`
447
+ with the 15 entries above
448
+ - `jsonrpc_error_messages` — named constructors returning `JsonrpcErrorObject`
449
+ - `jsonrpc_errors` — named constructors returning `ThrownJsonrpcError`
450
+ (derived from `jsonrpc_error_messages` via `create_error_thrower`).
451
+ Usage: `throw jsonrpc_errors.not_found('user')`, `throw jsonrpc_errors.forbidden()`
452
+ - `ThrownJsonrpcError` — `Error` subclass carrying `code` + optional `data`
453
+ - `JSONRPC_ERROR_CODE_TO_HTTP_STATUS` / `HTTP_STATUS_TO_JSONRPC_ERROR_CODE`
454
+ and the `jsonrpc_error_code_to_http_status` / `http_status_to_jsonrpc_error_code`
455
+ accessors (fall back to 500 / `internal_error`)
456
+
457
+ Handlers can `throw jsonrpc_errors.*` — `apply_route_specs`' error-catch
458
+ layer converts them to `{error: JsonrpcErrorObject}` at the correct HTTP
459
+ status. Generic `Error` maps to 500 `internal_error` (message in DEV only).
460
+
461
+ ### `jsonrpc_helpers.ts` — builders, guards, converters
462
+
463
+ Used by the SAES runtime (`ActionEvent`, `ActionPeer`, transports) and
464
+ the RPC endpoint dispatcher.
465
+
466
+ Builders (all emit correctly-shaped messages with `jsonrpc: '2.0'`):
467
+
468
+ - `create_jsonrpc_request(method, params, id)`
469
+ - `create_jsonrpc_notification(method, params)`
470
+ - `create_jsonrpc_response(id, result)`
471
+ - `create_jsonrpc_error_response(id, error)`
472
+ - `create_jsonrpc_error_response_from_thrown(id, error)` — `ThrownJsonrpcError`
473
+ → preserves code/message/data; plain `Error` → `internal_error`, includes
474
+ `{stack}` in `data` under DEV only
475
+
476
+ Type guards:
477
+
478
+ - `is_jsonrpc_request_id` — string or finite number (rejects NaN/Infinity)
479
+ - `is_jsonrpc_object` — object with `jsonrpc: '2.0'` (not array)
480
+ - `is_jsonrpc_message` — single message or non-empty batch array
481
+ - `is_jsonrpc_request` / `is_jsonrpc_notification` / `is_jsonrpc_response` / `is_jsonrpc_error_response`
482
+
483
+ Converters:
484
+
485
+ - `to_jsonrpc_message_id(message_or_id)` — extracts a valid id or returns `null`
486
+ - `to_jsonrpc_params(input)` — normalizes to `Record<string, any>` or
487
+ `undefined`; primitives wrap as `{value}`
488
+ - `to_jsonrpc_result(output)` — normalizes for a response; null/undefined
489
+ becomes `{}`, primitives wrap as `{value}`
490
+
491
+ ## Pending Effects
492
+
493
+ `emit_after_commit(ctx, fn)` in `pending_effects.ts` is the canonical
494
+ post-commit fan-out helper. Used for WS sends (`NotificationSender.send_to_account`
495
+ for permit-offer notifications — see `../auth/CLAUDE.md` §WS notifications) and any side effect that must run only
496
+ after the transaction commits.
497
+
498
+ ```typescript
499
+ interface PendingEffectsContext {
500
+ log: Logger;
501
+ pending_effects: Array<Promise<void>>;
502
+ }
503
+
504
+ emit_after_commit(ctx, () => notification_sender.send_to_account(account_id, msg));
505
+ ```
506
+
507
+ Key properties:
508
+
509
+ - The enqueued promise **never rejects** — `fn` is wrapped in `try/catch`
510
+ and failures go to `ctx.log.error`. One failing send cannot starve
511
+ sibling sends in the same batch, nor corrupt the already-committed
512
+ response
513
+ - Also safe under test mode's `await_pending_effects: true` (which runs
514
+ `Promise.all(pending_effects)`) because the promise always resolves
515
+ - Structurally satisfied by both `RouteContext` (HTTP) and `ActionContext`
516
+ (RPC) — they share the `{log, pending_effects}` shape, which is why
517
+ this helper lives in `http/` rather than `actions/` or `auth/`
518
+
519
+ WS sends are **not** wrapped by `create_validated_broadcaster` (that only
520
+ guards SSE `broadcast(channel, data)`). Zod input schemas on
521
+ `RemoteNotificationActionSpec`s are contracts for consumers, not enforced
522
+ at send time.
523
+
524
+ ## Common Routes
525
+
526
+ `common_routes.ts` exposes three generic route-spec factories with no
527
+ auth-domain dependencies:
528
+
529
+ - `create_health_route_spec()` — `GET /health`, public, returns
530
+ `{status: 'ok'}`. Infrastructure endpoint for uptime monitors
531
+ - `create_server_status_route_spec({version, get_uptime_ms})` — `GET /api/server/status`,
532
+ authenticated, returns `{version, uptime_ms}`
533
+ - `create_surface_route_spec({surface})` — `GET /api/surface`,
534
+ authenticated, serves the `AppSurface` JSON. Authenticated because
535
+ surface data reveals API structure (schemas, auth, routes)
536
+
537
+ Auth-aware variants (account status, bootstrap status) live in
538
+ `../auth/` — `common_routes.ts` stays generic.
539
+
540
+ ## DB Routes (Generic Browser)
541
+
542
+ `db_routes.ts` creates keeper-only route specs for administering the
543
+ `public` schema via `information_schema`. Wired by consumers that want
544
+ a generic table browser; the factory is domain-agnostic.
545
+
546
+ `create_db_route_specs({db_type, db_name, extra_stats?, log?})`:
547
+
548
+ - `GET /health` — connected probe + table count + optional `extra_stats(db)`.
549
+ Returns `{connected: false}` at 503 on failure
550
+ - `GET /tables` — list public tables with row counts
551
+ - `GET /tables/:name` — columns + rows (paginated via `?offset`/`?limit`,
552
+ limit clamped to `[1, 1000]` with default 100) + total count + primary key
553
+ - `DELETE /tables/:name/rows/:id` — delete by PK. Returns 400 if table has
554
+ no PK (`ERROR_TABLE_NO_PRIMARY_KEY`), 404 if row missing (`ERROR_ROW_NOT_FOUND`)
555
+ or table missing (`ERROR_TABLE_NOT_FOUND`), 409 on FK violation (pg
556
+ error code `23503`)
557
+
558
+ All four routes use `{type: 'keeper'}` auth. Param schemas use
559
+ `VALID_SQL_IDENTIFIER` regex, and every table name gets
560
+ `assert_valid_sql_identifier()` before string-interpolating into SQL —
561
+ the identifier validation is the only reason the interpolation is safe.
562
+
563
+ Interfaces exported for consumer use: `TableInfo`, `TableWithCount`,
564
+ `PrimaryKeyInfo`, `ColumnInfo`, `DbRouteOptions`.
565
+
566
+ ## Cross-Module Notes
567
+
568
+ - **Middleware ordering** is assembled by `create_app_server` — see the
569
+ root `../../CLAUDE.md` §Middleware Ordering. The invariants `http/`
570
+ needs consumers to uphold: trusted-proxy runs before auth/rate-limit;
571
+ origin verification runs before session parsing; `client_ip` must be
572
+ set before any handler or rate limiter reads it
573
+ - **No re-exports.** Import every symbol from its canonical source
574
+ module. `surface.ts` no longer re-exports schema helpers — go through
575
+ `schema_helpers.ts`
576
+ - **Input/output schemas align with SAES.** When wiring RPC via
577
+ `actions/action_rpc.ts` or bridging to `RouteSpec` via
578
+ `actions/action_bridge.ts`, the same Zod types flow through unchanged
579
+ (see `../actions/CLAUDE.md` §Single JSON-RPC 2.0 endpoint and §HTTP bridge)
580
+ - **Error modules are complementary, not redundant.** `error_schemas.ts`
581
+ is Zod-first (for routes and surface); `jsonrpc_errors.ts` is
582
+ throw-first (for handlers and the catch layer). A single `ERROR_*`
583
+ code can be raised either way depending on whether the handler needs
584
+ to also attach diagnostic fields
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared post-commit side-effect helper.
3
+ *
4
+ * WS sends and `on_audit_event` SSE broadcasts must never fire mid-transaction —
5
+ * a rollback would leak state that never existed. Anything pushed onto
6
+ * `pending_effects` runs after the response is sent (see the request-context
7
+ * middleware), so this helper is the canonical home for post-commit fan-out.
8
+ *
9
+ * Satisfied by both `RouteContext` (HTTP routes) and `ActionContext` (RPC
10
+ * actions) — they share `{log, pending_effects}` by convention, so this
11
+ * module stays in `http/` (both depend on it).
12
+ *
13
+ * @module
14
+ */
15
+ import type { Logger } from '@fuzdev/fuz_util/log.js';
16
+ /** Minimal structural context required by `emit_after_commit`. */
17
+ export interface PendingEffectsContext {
18
+ log: Logger;
19
+ pending_effects: Array<Promise<void>>;
20
+ }
21
+ /**
22
+ * Defer a side effect until after the handler's transaction commits.
23
+ *
24
+ * Exceptions thrown by `fn` are caught and logged via `ctx.log.error`, so one
25
+ * failed send cannot corrupt the already-committed response or starve other
26
+ * queued effects in the same tick.
27
+ */
28
+ export declare const emit_after_commit: (ctx: PendingEffectsContext, fn: () => void) => void;
29
+ //# sourceMappingURL=pending_effects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending_effects.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/pending_effects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACtC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,qBAAqB,EAAE,IAAI,MAAM,IAAI,KAAG,IAU9E,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared post-commit side-effect helper.
3
+ *
4
+ * WS sends and `on_audit_event` SSE broadcasts must never fire mid-transaction —
5
+ * a rollback would leak state that never existed. Anything pushed onto
6
+ * `pending_effects` runs after the response is sent (see the request-context
7
+ * middleware), so this helper is the canonical home for post-commit fan-out.
8
+ *
9
+ * Satisfied by both `RouteContext` (HTTP routes) and `ActionContext` (RPC
10
+ * actions) — they share `{log, pending_effects}` by convention, so this
11
+ * module stays in `http/` (both depend on it).
12
+ *
13
+ * @module
14
+ */
15
+ /**
16
+ * Defer a side effect until after the handler's transaction commits.
17
+ *
18
+ * Exceptions thrown by `fn` are caught and logged via `ctx.log.error`, so one
19
+ * failed send cannot corrupt the already-committed response or starve other
20
+ * queued effects in the same tick.
21
+ */
22
+ export const emit_after_commit = (ctx, fn) => {
23
+ ctx.pending_effects.push(Promise.resolve().then(() => {
24
+ try {
25
+ fn();
26
+ }
27
+ catch (err) {
28
+ ctx.log.error('post-commit side effect failed:', err);
29
+ }
30
+ }));
31
+ };
@@ -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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"}