@fuzdev/fuz_app 0.30.0 → 0.31.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.
- package/dist/actions/CLAUDE.md +630 -0
- package/dist/actions/action_rpc.d.ts +29 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +42 -6
- package/dist/actions/action_types.d.ts +2 -2
- package/dist/actions/cancel.d.ts +12 -13
- package/dist/actions/cancel.d.ts.map +1 -1
- package/dist/actions/cancel.js +10 -13
- package/dist/actions/heartbeat.d.ts +8 -13
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -8
- package/dist/actions/register_action_ws.d.ts +3 -3
- package/dist/actions/register_action_ws.js +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +4 -4
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +3 -3
- package/dist/actions/socket.svelte.d.ts +16 -16
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +15 -15
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +923 -0
- package/dist/auth/account_action_specs.d.ts +216 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -0
- package/dist/auth/account_action_specs.js +159 -0
- package/dist/auth/account_actions.d.ts +51 -0
- package/dist/auth/account_actions.d.ts.map +1 -0
- package/dist/auth/account_actions.js +119 -0
- package/dist/auth/account_queries.d.ts +6 -2
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +40 -4
- package/dist/auth/account_routes.d.ts +94 -16
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +108 -180
- package/dist/auth/account_schema.d.ts +85 -30
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +40 -8
- package/dist/auth/admin_action_specs.d.ts +674 -0
- package/dist/auth/admin_action_specs.d.ts.map +1 -0
- package/dist/auth/admin_action_specs.js +287 -0
- package/dist/auth/admin_actions.d.ts +69 -0
- package/dist/auth/admin_actions.d.ts.map +1 -0
- package/dist/auth/admin_actions.js +256 -0
- package/dist/auth/api_token.d.ts +10 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +9 -0
- package/dist/auth/api_token_queries.d.ts +3 -3
- package/dist/auth/api_token_queries.js +3 -3
- package/dist/auth/app_settings_schema.d.ts +4 -3
- package/dist/auth/app_settings_schema.d.ts.map +1 -1
- package/dist/auth/app_settings_schema.js +2 -1
- package/dist/auth/audit_log_routes.d.ts +14 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +22 -79
- package/dist/auth/audit_log_schema.d.ts +100 -29
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +83 -11
- package/dist/auth/bootstrap_routes.d.ts +14 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +10 -3
- package/dist/auth/cleanup.d.ts +63 -0
- package/dist/auth/cleanup.d.ts.map +1 -0
- package/dist/auth/cleanup.js +80 -0
- package/dist/auth/invite_schema.d.ts +11 -10
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +4 -3
- package/dist/auth/migrations.d.ts +6 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +28 -0
- package/dist/auth/permit_offer_action_specs.d.ts +364 -0
- package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/permit_offer_action_specs.js +216 -0
- package/dist/auth/permit_offer_actions.d.ts +96 -0
- package/dist/auth/permit_offer_actions.d.ts.map +1 -0
- package/dist/auth/permit_offer_actions.js +428 -0
- package/dist/auth/permit_offer_notifications.d.ts +361 -0
- package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
- package/dist/auth/permit_offer_notifications.js +179 -0
- package/dist/auth/permit_offer_queries.d.ts +165 -0
- package/dist/auth/permit_offer_queries.d.ts.map +1 -0
- package/dist/auth/permit_offer_queries.js +390 -0
- package/dist/auth/permit_offer_schema.d.ts +103 -0
- package/dist/auth/permit_offer_schema.d.ts.map +1 -0
- package/dist/auth/permit_offer_schema.js +142 -0
- package/dist/auth/permit_queries.d.ts +77 -14
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +119 -24
- package/dist/auth/session_queries.d.ts +4 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +4 -2
- package/dist/auth/signup_routes.d.ts +13 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -7
- package/dist/http/CLAUDE.md +584 -0
- package/dist/http/pending_effects.d.ts +29 -0
- package/dist/http/pending_effects.d.ts.map +1 -0
- package/dist/http/pending_effects.js +31 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +4 -3
- package/dist/rate_limiter.d.ts +30 -0
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +25 -2
- package/dist/realtime/sse_auth_guard.d.ts +2 -0
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +5 -3
- package/dist/testing/CLAUDE.md +668 -1
- package/dist/testing/admin_integration.d.ts +10 -7
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +382 -482
- package/dist/testing/app_server.d.ts +7 -6
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/attack_surface.d.ts +9 -3
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +4 -4
- package/dist/testing/audit_completeness.d.ts +6 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +158 -134
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +4 -33
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +2 -0
- package/dist/testing/entities.d.ts +35 -13
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +17 -0
- package/dist/testing/integration.d.ts +10 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +352 -340
- package/dist/testing/integration_helpers.d.ts +16 -5
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +24 -4
- package/dist/testing/rate_limiting.d.ts +7 -0
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +41 -10
- package/dist/testing/rpc_helpers.d.ts +153 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +184 -8
- package/dist/testing/sse_round_trip.d.ts +8 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +10 -3
- package/dist/testing/standard.d.ts +9 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +6 -2
- package/dist/testing/surface_invariants.d.ts +7 -3
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +5 -4
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +9 -38
- package/dist/ui/AccountSessions.svelte +8 -4
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAccounts.svelte +61 -33
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAuditLog.svelte +3 -2
- package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
- package/dist/ui/AdminInvites.svelte +3 -2
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +14 -9
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminPermitHistory.svelte +3 -2
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +29 -25
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +351 -0
- package/dist/ui/OpenSignupToggle.svelte +6 -3
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
- package/dist/ui/PermitOfferForm.svelte +141 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferHistory.svelte +109 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferInbox.svelte +121 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +39 -16
- package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +99 -23
- package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_invites_state.svelte.js +38 -26
- package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_sessions_state.svelte.js +35 -21
- package/dist/ui/app_settings_state.svelte.d.ts +39 -0
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
- package/dist/ui/app_settings_state.svelte.js +34 -18
- package/dist/ui/audit_log_state.svelte.d.ts +40 -3
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +36 -42
- package/dist/ui/auth_state.svelte.d.ts +4 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +4 -1
- package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
- package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/permit_offers_state.svelte.js +197 -0
- package/package.json +3 -3
- package/dist/auth/admin_routes.d.ts +0 -29
- package/dist/auth/admin_routes.d.ts.map +0 -1
- package/dist/auth/admin_routes.js +0 -226
- package/dist/auth/app_settings_routes.d.ts +0 -27
- package/dist/auth/app_settings_routes.d.ts.map +0 -1
- package/dist/auth/app_settings_routes.js +0 -66
- package/dist/auth/invite_routes.d.ts +0 -18
- package/dist/auth/invite_routes.d.ts.map +0 -1
- 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":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAW,IAAI,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AACpE,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAE3B,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAKjB,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAClB;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GACd;IAAC,IAAI,EAAE,eAAe,CAAA;CAAC,GACvB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,GAC5B;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,KAAK,CAAC,iBAAiB,CAAC,CAAC;AAE9E,6CAA6C;AAC7C,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC5B,8DAA8D;IAC9D,EAAE,EAAE,EAAE,CAAC;IACP,oFAAoF;IACpF,aAAa,EAAE,EAAE,CAAC;IAClB,2EAA2E;IAC3E,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACtC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE7F;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,YAAY,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,mEAAmE;IACnE,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,oCAAoC;IACpC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,GAAG,OAAO,KAAG,CAE/C,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAAE,GAAG,OAAO,KAAG,CAEhD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,GAAG,OAAO,KAAG,CAE/C,CAAC;
|
|
1
|
+
{"version":3,"file":"route_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/route_spec.ts"],"names":[],"mappings":"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"}
|