@fuzdev/fuz_app 0.55.0 → 0.57.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 +211 -155
- package/dist/actions/action_bridge.d.ts +8 -5
- package/dist/actions/action_bridge.d.ts.map +1 -1
- package/dist/actions/action_bridge.js +1 -11
- package/dist/actions/action_codegen.d.ts +19 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +20 -14
- package/dist/actions/action_registry.d.ts.map +1 -1
- package/dist/actions/action_registry.js +5 -2
- package/dist/actions/action_rpc.d.ts +110 -44
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +92 -287
- package/dist/actions/action_spec.d.ts +55 -16
- package/dist/actions/action_spec.d.ts.map +1 -1
- package/dist/actions/action_spec.js +16 -11
- package/dist/actions/action_types.d.ts +28 -60
- package/dist/actions/action_types.d.ts.map +1 -1
- package/dist/actions/action_types.js +13 -5
- package/dist/actions/broadcast_api.d.ts +2 -2
- package/dist/actions/broadcast_api.js +2 -2
- package/dist/actions/compile_action_registry.d.ts +50 -0
- package/dist/actions/compile_action_registry.d.ts.map +1 -0
- package/dist/actions/compile_action_registry.js +69 -0
- package/dist/actions/heartbeat.d.ts +8 -4
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -4
- package/dist/actions/perform_action.d.ts +145 -0
- package/dist/actions/perform_action.d.ts.map +1 -0
- package/dist/actions/perform_action.js +258 -0
- package/dist/actions/register_action_ws.d.ts +44 -38
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +101 -159
- package/dist/actions/register_ws_endpoint.d.ts +2 -10
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +32 -10
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
- package/dist/actions/transports_ws_auth_guard.js +1 -1
- package/dist/actions/transports_ws_backend.d.ts +1 -1
- package/dist/actions/transports_ws_backend.js +1 -1
- package/dist/auth/CLAUDE.md +673 -442
- package/dist/auth/account_action_specs.d.ts +28 -7
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_action_specs.js +7 -7
- package/dist/auth/account_actions.d.ts +8 -14
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +26 -32
- package/dist/auth/account_queries.d.ts +46 -13
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +73 -33
- package/dist/auth/account_routes.d.ts +4 -3
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +58 -33
- package/dist/auth/account_schema.d.ts +46 -54
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +21 -48
- package/dist/auth/admin_action_specs.d.ts +55 -21
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +42 -26
- package/dist/auth/admin_actions.d.ts +14 -21
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +47 -44
- package/dist/auth/audit_emitter.d.ts +160 -0
- package/dist/auth/audit_emitter.d.ts.map +1 -0
- package/dist/auth/audit_emitter.js +83 -0
- package/dist/auth/audit_log_queries.d.ts +17 -87
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +17 -96
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +7 -3
- package/dist/auth/audit_log_schema.d.ts +48 -42
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +56 -43
- package/dist/auth/auth_guard_resolver.d.ts +44 -0
- package/dist/auth/auth_guard_resolver.d.ts.map +1 -0
- package/dist/auth/auth_guard_resolver.js +56 -0
- package/dist/auth/bootstrap_account.d.ts +7 -7
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +7 -7
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +11 -10
- package/dist/auth/cleanup.d.ts +20 -26
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +33 -47
- package/dist/auth/credential_type_schema.d.ts +115 -0
- package/dist/auth/credential_type_schema.d.ts.map +1 -0
- package/dist/auth/credential_type_schema.js +127 -0
- package/dist/auth/daemon_token_middleware.d.ts +1 -1
- package/dist/auth/daemon_token_middleware.js +3 -3
- package/dist/auth/ddl.d.ts +2 -2
- package/dist/auth/ddl.d.ts.map +1 -1
- package/dist/auth/ddl.js +6 -6
- package/dist/auth/deps.d.ts +7 -32
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/grant_path_schema.d.ts +117 -0
- package/dist/auth/grant_path_schema.d.ts.map +1 -0
- package/dist/auth/grant_path_schema.js +137 -0
- package/dist/auth/invite_queries.d.ts +12 -1
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +12 -1
- package/dist/auth/invite_schema.d.ts +1 -1
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +5 -2
- package/dist/auth/migrations.d.ts +22 -7
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +64 -25
- package/dist/auth/request_context.d.ts +157 -170
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +224 -268
- package/dist/auth/{permit_offer_action_specs.d.ts → role_grant_offer_action_specs.d.ts} +130 -100
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_action_specs.js +262 -0
- package/dist/auth/role_grant_offer_actions.d.ts +104 -0
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -0
- package/dist/auth/{permit_offer_actions.js → role_grant_offer_actions.js} +153 -140
- package/dist/auth/{permit_offer_notifications.d.ts → role_grant_offer_notifications.d.ts} +80 -70
- package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_notifications.js +182 -0
- package/dist/auth/{permit_offer_queries.d.ts → role_grant_offer_queries.d.ts} +64 -64
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -0
- package/dist/auth/{permit_offer_queries.js → role_grant_offer_queries.js} +136 -123
- package/dist/auth/role_grant_offer_schema.d.ts +150 -0
- package/dist/auth/role_grant_offer_schema.d.ts.map +1 -0
- package/dist/auth/{permit_offer_schema.js → role_grant_offer_schema.js} +55 -36
- package/dist/auth/role_grant_queries.d.ts +231 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -0
- package/dist/auth/role_grant_queries.js +320 -0
- package/dist/auth/role_schema.d.ts +150 -40
- package/dist/auth/role_schema.d.ts.map +1 -1
- package/dist/auth/role_schema.js +144 -45
- package/dist/auth/scope_kind_schema.d.ts +96 -0
- package/dist/auth/scope_kind_schema.d.ts.map +1 -0
- package/dist/auth/scope_kind_schema.js +94 -0
- package/dist/auth/self_service_role_action_specs.d.ts +4 -1
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +2 -2
- package/dist/auth/self_service_role_actions.d.ts +35 -29
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +58 -48
- package/dist/auth/session_cookie.d.ts +43 -6
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +31 -5
- package/dist/auth/session_middleware.d.ts +37 -3
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +33 -7
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +48 -19
- package/dist/auth/standard_action_specs.d.ts +2 -2
- package/dist/auth/standard_action_specs.js +4 -4
- package/dist/auth/standard_rpc_actions.d.ts +23 -19
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +12 -12
- package/dist/db/migrate.d.ts +1 -1
- package/dist/db/migrate.js +1 -1
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +4 -4
- package/dist/env/load.d.ts +1 -1
- package/dist/env/load.js +1 -1
- package/dist/hono_context.d.ts +27 -45
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +14 -28
- package/dist/http/CLAUDE.md +235 -121
- package/dist/http/auth_shape.d.ts +191 -0
- package/dist/http/auth_shape.d.ts.map +1 -0
- package/dist/http/auth_shape.js +237 -0
- package/dist/http/common_routes.js +3 -3
- package/dist/http/db_routes.d.ts +4 -0
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +44 -7
- package/dist/http/error_schemas.d.ts +72 -39
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +81 -33
- package/dist/http/pending_effects.d.ts +71 -18
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +87 -18
- package/dist/http/proxy.d.ts +52 -5
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +92 -14
- package/dist/http/route_spec.d.ts +89 -75
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +54 -72
- package/dist/http/schema_helpers.d.ts +3 -14
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +2 -14
- package/dist/http/surface.d.ts +2 -10
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +3 -4
- package/dist/http/surface_query.d.ts +39 -35
- package/dist/http/surface_query.d.ts.map +1 -1
- package/dist/http/surface_query.js +79 -36
- package/dist/primitive_schemas.d.ts +39 -0
- package/dist/primitive_schemas.d.ts.map +1 -0
- package/dist/primitive_schemas.js +40 -0
- package/dist/realtime/sse_auth_guard.d.ts +5 -5
- package/dist/realtime/sse_auth_guard.js +9 -9
- package/dist/runtime/mock.d.ts +1 -1
- package/dist/runtime/mock.js +1 -1
- package/dist/server/app_backend.d.ts +14 -11
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +12 -8
- package/dist/server/app_server.d.ts +7 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +35 -40
- package/dist/server/validate_nginx.d.ts +1 -1
- package/dist/server/validate_nginx.js +1 -1
- package/dist/testing/CLAUDE.md +50 -38
- package/dist/testing/admin_integration.d.ts +5 -6
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +87 -85
- package/dist/testing/app_server.d.ts +11 -14
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +16 -15
- package/dist/testing/assertions.d.ts.map +1 -1
- package/dist/testing/assertions.js +2 -1
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +15 -9
- package/dist/testing/audit_completeness.d.ts +2 -2
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +36 -36
- package/dist/testing/auth_apps.d.ts +5 -4
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +22 -19
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +5 -5
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +4 -4
- package/dist/testing/db_entities.d.ts +22 -0
- package/dist/testing/db_entities.d.ts.map +1 -0
- package/dist/testing/db_entities.js +28 -0
- package/dist/testing/entities.d.ts +8 -7
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +21 -18
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +13 -14
- package/dist/testing/integration_helpers.d.ts +4 -4
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +20 -18
- package/dist/testing/middleware.d.ts +4 -4
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +12 -11
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +40 -24
- package/dist/testing/rpc_round_trip.d.ts +1 -1
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +14 -13
- package/dist/testing/sse_round_trip.d.ts +3 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +7 -11
- package/dist/testing/standard.d.ts +1 -1
- package/dist/testing/stubs.d.ts +25 -0
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +43 -2
- package/dist/testing/surface_invariants.d.ts +14 -6
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +119 -43
- package/dist/testing/ws_round_trip.d.ts +12 -13
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -11
- package/dist/ui/AdminAccounts.svelte +23 -20
- package/dist/ui/AdminOverview.svelte +15 -13
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/{AdminPermitHistory.svelte → AdminRoleGrantHistory.svelte} +12 -12
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts +4 -0
- package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -0
- package/dist/ui/BootstrapForm.svelte +1 -1
- package/dist/ui/CLAUDE.md +60 -60
- package/dist/ui/{PermitOfferForm.svelte → RoleGrantOfferForm.svelte} +27 -26
- package/dist/ui/{PermitOfferForm.svelte.d.ts → RoleGrantOfferForm.svelte.d.ts} +7 -7
- package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/{PermitOfferHistory.svelte → RoleGrantOfferHistory.svelte} +12 -12
- package/dist/ui/{PermitOfferHistory.svelte.d.ts → RoleGrantOfferHistory.svelte.d.ts} +4 -4
- package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/{PermitOfferInbox.svelte → RoleGrantOfferInbox.svelte} +14 -14
- package/dist/ui/{PermitOfferInbox.svelte.d.ts → RoleGrantOfferInbox.svelte.d.ts} +4 -4
- package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/SignupForm.svelte +1 -1
- package/dist/ui/SurfaceExplorer.svelte +35 -15
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.d.ts +2 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +2 -3
- package/dist/ui/admin_accounts_state.svelte.d.ts +18 -18
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -16
- package/dist/ui/admin_rpc_adapters.d.ts +20 -20
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +17 -17
- package/dist/ui/admin_sessions_state.svelte.d.ts +2 -2
- package/dist/ui/admin_sessions_state.svelte.js +2 -2
- package/dist/ui/audit_log_state.svelte.d.ts +7 -7
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +6 -6
- package/dist/ui/auth_state.svelte.d.ts +3 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +6 -6
- package/dist/ui/format_scope.d.ts +2 -2
- package/dist/ui/format_scope.js +2 -2
- package/dist/ui/{permit_offers_state.svelte.d.ts → role_grant_offers_state.svelte.d.ts} +30 -30
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/{permit_offers_state.svelte.js → role_grant_offers_state.svelte.js} +18 -18
- package/dist/ui/ui_format.js +2 -2
- package/package.json +3 -3
- package/dist/auth/permit_offer_action_specs.d.ts.map +0 -1
- package/dist/auth/permit_offer_action_specs.js +0 -258
- package/dist/auth/permit_offer_actions.d.ts +0 -110
- package/dist/auth/permit_offer_actions.d.ts.map +0 -1
- package/dist/auth/permit_offer_notifications.d.ts.map +0 -1
- package/dist/auth/permit_offer_notifications.js +0 -182
- package/dist/auth/permit_offer_queries.d.ts.map +0 -1
- package/dist/auth/permit_offer_schema.d.ts +0 -125
- package/dist/auth/permit_offer_schema.d.ts.map +0 -1
- package/dist/auth/permit_queries.d.ts +0 -222
- package/dist/auth/permit_queries.d.ts.map +0 -1
- package/dist/auth/permit_queries.js +0 -305
- package/dist/auth/require_keeper.d.ts +0 -20
- package/dist/auth/require_keeper.d.ts.map +0 -1
- package/dist/auth/require_keeper.js +0 -35
- package/dist/auth/route_guards.d.ts +0 -27
- package/dist/auth/route_guards.d.ts.map +0 -1
- package/dist/auth/route_guards.js +0 -38
- package/dist/auth/session_lifecycle.d.ts +0 -37
- package/dist/auth/session_lifecycle.d.ts.map +0 -1
- package/dist/auth/session_lifecycle.js +0 -29
- package/dist/ui/AdminPermitHistory.svelte.d.ts +0 -4
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +0 -1
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +0 -1
- package/dist/ui/permit_offers_state.svelte.d.ts.map +0 -1
package/dist/http/CLAUDE.md
CHANGED
|
@@ -14,22 +14,23 @@ see `../../docs/architecture.md`.
|
|
|
14
14
|
|
|
15
15
|
## Module Map
|
|
16
16
|
|
|
17
|
-
| File | Role
|
|
18
|
-
| -------------------- |
|
|
19
|
-
| `route_spec.ts` | `RouteSpec` + `apply_route_specs`, validation pipeline, transactions
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
17
|
+
| File | Role |
|
|
18
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
19
|
+
| `route_spec.ts` | `RouteSpec` + `apply_route_specs`, validation pipeline, transactions |
|
|
20
|
+
| `auth_shape.ts` | Canonical `RouteAuth` Zod schema + cross-axis invariants + predicates |
|
|
21
|
+
| `error_schemas.ts` | `ERROR_*` constants, standard error shapes, `derive_error_schemas` |
|
|
22
|
+
| `schema_helpers.ts` | Shared Zod introspection (null/strict/surface/merge/middleware-applies) |
|
|
23
|
+
| `middleware_spec.ts` | `MiddlewareSpec` interface |
|
|
24
|
+
| `surface.ts` | `AppSurface`, `AppSurfaceSpec`, `generate_app_surface`, diagnostics |
|
|
25
|
+
| `surface_query.ts` | Pure filters/groupings over `AppSurface` |
|
|
26
|
+
| `proxy.ts` | Trusted-proxy middleware, CIDR parsing, rightmost-first XFF resolution |
|
|
27
|
+
| `origin.ts` | Origin/Referer allowlist middleware with wildcard patterns |
|
|
28
|
+
| `jsonrpc.ts` | JSON-RPC 2.0 envelope schemas (MCP superset), `JsonrpcErrorCode`, `_meta` |
|
|
29
|
+
| `jsonrpc_errors.ts` | `ThrownJsonrpcError`, `jsonrpc_errors` throwers, HTTP-status mappings |
|
|
30
|
+
| `jsonrpc_helpers.ts` | Message builders, type guards, input/result normalizers |
|
|
31
|
+
| `common_routes.ts` | Health check + authenticated server-status + surface route specs |
|
|
32
|
+
| `db_routes.ts` | Generic keeper-only table browser route specs (public schema) |
|
|
33
|
+
| `pending_effects.ts` | `emit_after_commit` + `flush_pending_effects` + `flush_post_commit_effects` + `EmitAfterCommitContext` |
|
|
33
34
|
|
|
34
35
|
## Route Spec System
|
|
35
36
|
|
|
@@ -41,7 +42,7 @@ by `generate_app_surface`. Same-shaped data, different consumers.
|
|
|
41
42
|
|
|
42
43
|
- `method` — `'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'`
|
|
43
44
|
- `path` — Hono path (supports `:param` segments)
|
|
44
|
-
- `auth: RouteAuth` — `{
|
|
45
|
+
- `auth: RouteAuth` — flat record `{account, actor, roles?, credential_types?}` from `auth_shape.ts`. Each axis is `'none' | 'optional' | 'required'`. Same shape governs `ActionSpec.auth` (see `../actions/CLAUDE.md`).
|
|
45
46
|
- `handler: RouteHandler` — `(c: Context, route: RouteContext) => Response | Promise<Response>`
|
|
46
47
|
- `description` — free-text, surfaced in `AppSurface`
|
|
47
48
|
- `params?: z.ZodObject` — strict-object schema for URL path params
|
|
@@ -62,19 +63,31 @@ The second handler argument is always a `RouteContext`:
|
|
|
62
63
|
|
|
63
64
|
```typescript
|
|
64
65
|
interface RouteContext {
|
|
65
|
-
db: Db; // transaction-scoped
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
db: Db; // transaction-scoped when `transaction: true`, pool-level otherwise
|
|
67
|
+
pending_effects: Array<Promise<void>>; // eager pool writes already in flight
|
|
68
|
+
post_commit_effects: Array<() => void | Promise<void>>; // deferred — push via `emit_after_commit`
|
|
68
69
|
}
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
- **`route.db`** — use for the handler's main DB work. Wrapped in a transaction
|
|
72
|
-
when `transaction: true` (the default for non-GET)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
|
|
73
|
+
when `transaction: true` (the default for non-GET); routes that opt out
|
|
74
|
+
(`transaction: false`, e.g. signup / bootstrap) get the pool here directly
|
|
75
|
+
and may call `route.db.transaction(...)` for their own scope.
|
|
76
|
+
- **`route.pending_effects`** — direct push for eager fire-and-forget pool
|
|
77
|
+
writes (audit, session touch, api-token usage tracking). Push the in-flight
|
|
78
|
+
`Promise<void>` to register it for test-mode flushing.
|
|
79
|
+
- **`route.post_commit_effects`** — do not push directly; reach for
|
|
80
|
+
`emit_after_commit` from `pending_effects.ts`. The helper pushes a
|
|
81
|
+
thunk that the flush middleware invokes after the handler returns,
|
|
82
|
+
closing the microtask-ordering window that an eager
|
|
83
|
+
`Promise.resolve().then(fn)` leaves open inside the wrapping
|
|
84
|
+
`db.transaction`.
|
|
85
|
+
|
|
86
|
+
Pool-level fire-and-forget writes (audit logs, etc.) run through the bound
|
|
87
|
+
`AppDeps.audit` capability — see `../auth/CLAUDE.md` §Deps. Handlers that
|
|
88
|
+
need rollback-resilient writes call `deps.audit.emit(route, input)`, which
|
|
89
|
+
captures the pool inside the bound emitter so the row lands even when
|
|
90
|
+
the handler's transaction rolls back.
|
|
78
91
|
|
|
79
92
|
### Declarative transactions
|
|
80
93
|
|
|
@@ -97,41 +110,37 @@ wrapper). See `../auth/signup_routes.ts`.
|
|
|
97
110
|
2. **Query validation** — `spec.query` → `validated_query`; mismatch
|
|
98
111
|
returns 400 `ERROR_INVALID_QUERY_PARAMS`
|
|
99
112
|
3. **Pre-validation auth guards** — `require_auth` (401
|
|
100
|
-
`ERROR_AUTHENTICATION_REQUIRED`)
|
|
101
|
-
before any body parsing so
|
|
102
|
-
route-shape information from
|
|
103
|
-
`AuthGuardResolver` (e.g.
|
|
104
|
-
`../auth/
|
|
105
|
-
`pre_validation: Array<MiddlewareHandler>`.
|
|
106
|
-
4. **
|
|
107
|
-
|
|
113
|
+
`ERROR_AUTHENTICATION_REQUIRED`) when `auth.account === 'required'`
|
|
114
|
+
or `auth.actor === 'required'`. Fires before any body parsing so
|
|
115
|
+
unauthenticated callers never see route-shape information from
|
|
116
|
+
input parse failures. The `AuthGuardResolver` (e.g.
|
|
117
|
+
`fuz_auth_guard_resolver` from `../auth/auth_guard_resolver.ts`) returns
|
|
118
|
+
this set as `pre_validation: Array<MiddlewareHandler>`.
|
|
119
|
+
4. **Input validation** — JSON body parsed + validated; mismatch returns
|
|
120
|
+
400 `ERROR_INVALID_JSON_BODY` (not JSON) or `ERROR_INVALID_REQUEST_BODY`
|
|
121
|
+
(schema failure with `issues`). Skipped on GET and `z.null()` inputs.
|
|
122
|
+
The validated input lands on `c.var.validated_input` so the
|
|
123
|
+
authorization phase reads `acting` as a typed Zod field.
|
|
124
|
+
5. **Authorization phase** — when `spec.auth.actor !== 'none'`,
|
|
108
125
|
resolves the acting actor against `c.var.account_id` (set by the
|
|
109
|
-
auth middleware) plus
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`
|
|
126
|
+
auth middleware) plus `validated_input.acting` (or
|
|
127
|
+
`validated_query.acting` for GET routes), builds `RequestContext`
|
|
128
|
+
via `build_request_context`, and sets `REQUEST_CONTEXT_KEY`. When
|
|
129
|
+
`auth.account !== 'none' && auth.actor === 'none'`, an account-only
|
|
130
|
+
context is built. Resolution failures return 400
|
|
113
131
|
`ERROR_ACTOR_REQUIRED` (with `available[]`) or
|
|
114
132
|
`ERROR_ACTOR_NOT_ON_ACCOUNT` (or 500 `ERROR_NO_ACTORS_ON_ACCOUNT`
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
`
|
|
125
|
-
|
|
126
|
-
`post_authorization: Array<MiddlewareHandler>`.
|
|
127
|
-
6. **Input validation** — JSON body parsed + validated; mismatch returns
|
|
128
|
-
400 `ERROR_INVALID_JSON_BODY` (not JSON) or `ERROR_INVALID_REQUEST_BODY`
|
|
129
|
-
(schema failure with `issues`). Skipped on GET and `z.null()` inputs.
|
|
130
|
-
On mutating methods, the parse result is shared with the authorization
|
|
131
|
-
phase's pre-parse via `c.var.cached_request_body`
|
|
132
|
-
(`CACHED_REQUEST_BODY_KEY` from `hono_context.ts`) — the cache is
|
|
133
|
-
fuz_app-owned, not Hono's internal `bodyCache`, so a future Hono
|
|
134
|
-
internals refactor can't break our second-parse-avoidance contract.
|
|
133
|
+
on signup-invariant violation, 500 `ERROR_ACCOUNT_VANISHED` on
|
|
134
|
+
torn account/actor reads after a successful resolve). Public
|
|
135
|
+
routes (`account: 'none' && actor: 'none'`) skip this phase
|
|
136
|
+
entirely.
|
|
137
|
+
6. **Post-authorization auth guards** — `require_credential_types(types)`
|
|
138
|
+
(403 `ERROR_CREDENTIAL_TYPE_REQUIRED` with `required_credential_types: ReadonlyArray<string>`)
|
|
139
|
+
fires first when `auth.credential_types?.length`; `require_role(roles)` (403
|
|
140
|
+
`ERROR_INSUFFICIENT_PERMISSIONS` with `required_roles: ReadonlyArray<string>`)
|
|
141
|
+
fires next when `auth.roles?.length`. Both read
|
|
142
|
+
`REQUEST_CONTEXT_KEY` populated by step 5. Multi-role specs admit
|
|
143
|
+
any-of via `has_any_scoped_role(ctx, roles, null)`.
|
|
135
144
|
7. **Handler** — wrapped in transaction when `use_transaction` (see
|
|
136
145
|
above), receives `RouteContext`
|
|
137
146
|
8. **DEV-only output + error validation** — wraps the handler (see below)
|
|
@@ -148,15 +157,21 @@ wrapper). See `../auth/signup_routes.ts`.
|
|
|
148
157
|
the JSON-RPC dispatcher keeps its own `{jsonrpc, id, error: {code,
|
|
149
158
|
message, data}}` envelope on the RPC mount
|
|
150
159
|
|
|
151
|
-
|
|
152
|
-
(`actions/action_rpc.ts`) so HTTP RPC and REST
|
|
153
|
-
|
|
160
|
+
Ordering: **401 → 400 → 403 → handler**. Mirrors the RPC dispatcher
|
|
161
|
+
(`actions/action_rpc.ts`) so HTTP RPC and REST fail with the same
|
|
162
|
+
priority. The alternative (403-before-400) was rejected because
|
|
163
|
+
defense-in-depth via attack-surface obscurity is illusory when the
|
|
164
|
+
surface is published in `library.json` codegen anyway. The trade-off
|
|
165
|
+
is that an authenticated-but-unauthorized caller can distinguish 400
|
|
166
|
+
from 403.
|
|
154
167
|
|
|
155
168
|
Duplicate `method path` pairs throw at registration.
|
|
156
169
|
|
|
157
|
-
Validated values are accessed via `get_route_input
|
|
158
|
-
`get_route_params
|
|
159
|
-
|
|
170
|
+
Validated values are accessed via `get_route_input(c, schema)`,
|
|
171
|
+
`get_route_params(c, schema)`, `get_route_query(c, schema)` — pass the
|
|
172
|
+
matching Zod schema and the return type infers as `z.infer<typeof
|
|
173
|
+
schema>`. Each helper also has a `<T>(c)` overload (no schema arg) for
|
|
174
|
+
callers who don't have the schema in scope.
|
|
160
175
|
|
|
161
176
|
### DEV-only output + error validation
|
|
162
177
|
|
|
@@ -192,35 +207,40 @@ Output Validation.
|
|
|
192
207
|
|
|
193
208
|
- `ERROR_*` snake*case string constants — single source of truth; use
|
|
194
209
|
`.literal(ERROR*\*)` in Zod schemas and inline checks in handlers
|
|
195
|
-
- `ApiError`, `ValidationError`, `PermissionError`,
|
|
196
|
-
`RateLimitError`, `PayloadTooLargeError`,
|
|
197
|
-
shapes
|
|
210
|
+
- `ApiError`, `ValidationError`, `PermissionError`,
|
|
211
|
+
`CredentialTypeRequiredError`, `RateLimitError`, `PayloadTooLargeError`,
|
|
212
|
+
`ForeignKeyError` — standard shapes
|
|
198
213
|
- `RouteErrorSchemas = Partial<Record<number, z.ZodType>>`
|
|
199
214
|
- `RateLimitKey = 'ip' | 'account' | 'both'`
|
|
200
215
|
|
|
201
216
|
All standard shapes use `z.looseObject` — intentional because multiple
|
|
202
217
|
producers (middleware + handler) can emit different extra fields at the
|
|
203
218
|
same status code. The `error` string literal is the contract; extra keys
|
|
204
|
-
(`
|
|
219
|
+
(`required_roles`, `required_credential_types`, `retry_after`, `detail`)
|
|
220
|
+
are diagnostic.
|
|
205
221
|
|
|
206
222
|
Pair every schema with the `z.infer` type export (`export type ApiError = z.infer<typeof ApiError>`).
|
|
207
223
|
|
|
208
224
|
### Three-layer error-schema merge
|
|
209
225
|
|
|
210
|
-
`merge_error_schemas(spec, middleware_errors
|
|
226
|
+
`merge_error_schemas(spec, middleware_errors?)` (in `schema_helpers.ts`)
|
|
211
227
|
merges three layers, later overrides earlier at the same status code:
|
|
212
228
|
|
|
213
|
-
1. **Derived** — from `derive_error_schemas({auth, has_input?, has_params?, has_query?, rate_limit
|
|
229
|
+
1. **Derived** — from `derive_error_schemas({auth, has_input?, has_params?, has_query?, rate_limit?})`:
|
|
214
230
|
- `has_input || has_params || has_query` → 400 `ValidationError`
|
|
215
|
-
- `auth.
|
|
216
|
-
- `auth.
|
|
217
|
-
- `auth.
|
|
231
|
+
- `auth.account === 'required'` or `auth.actor === 'required'` → 401 `ApiError`
|
|
232
|
+
- `auth.roles?.length` → 403 `PermissionError` (carries `required_roles: ReadonlyArray<string>`)
|
|
233
|
+
- `auth.credential_types?.length` → 403 `CredentialTypeRequiredError`
|
|
234
|
+
(carries `required_credential_types: ReadonlyArray<string>` echoing
|
|
235
|
+
the spec's allowlist — symmetric with `PermissionError`'s
|
|
236
|
+
`required_roles`; literal is `ERROR_CREDENTIAL_TYPE_REQUIRED`; both
|
|
237
|
+
gates set yields a `z.union([PermissionError, CredentialTypeRequiredError])`)
|
|
218
238
|
- `rate_limit` → 429 `RateLimitError`
|
|
219
|
-
- `
|
|
239
|
+
- `auth.actor !== 'none'` → widens 400 to a union with `ActorRequiredError` /
|
|
220
240
|
`ActorNotOnAccountError` and adds 500 union of `NoActorsOnAccountError`
|
|
221
241
|
/ `AccountVanishedError`. Mirrors what the dispatcher's authorization
|
|
222
242
|
phase actually emits on routes whose input declares `acting?: ActingActor`
|
|
223
|
-
|
|
243
|
+
(per registry-time invariant 2) — so DEV-mode error-schema validation in
|
|
224
244
|
`wrap_output_validation` doesn't reject the auth phase's body.
|
|
225
245
|
2. **Middleware** — from `MiddlewareSpec.errors` that apply to the route's
|
|
226
246
|
path (via `middleware_applies`)
|
|
@@ -228,37 +248,49 @@ merges three layers, later overrides earlier at the same status code:
|
|
|
228
248
|
|
|
229
249
|
Routes typically only need `errors` for handler-specific codes (404, 409, 422).
|
|
230
250
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
251
|
+
Actor-failure folding reads `spec.auth.actor !== 'none'` directly —
|
|
252
|
+
per registry-time invariant 2 (`actor !== 'none' ⟺ input declares
|
|
253
|
+
acting?: ActingActor`), the auth-shape axis is the single source of
|
|
254
|
+
truth.
|
|
255
|
+
|
|
256
|
+
**Framework-emitted vs consumer-authored.** The error-schema derivation
|
|
257
|
+
above is sound because the framework authors the errors at fixed
|
|
258
|
+
middleware sites — 401 from `require_auth`, 400 from
|
|
259
|
+
`create_input_validation`, 403 from `require_role` /
|
|
260
|
+
`require_credential_types`, 429 from rate limiters. Auto-derivation
|
|
261
|
+
documents the framework's own emissions; consumers tighten via
|
|
262
|
+
`RouteSpec.errors` when their handler narrows the surface.
|
|
263
|
+
|
|
264
|
+
The same auto-derivation pattern is **not** appropriate for consumer-
|
|
265
|
+
authored inputs (or handler outputs). A consumer's spec declares the
|
|
266
|
+
exact `acting?: ActingActor` slot, and the framework reads it back via
|
|
267
|
+
reference-equality to drive the authorization phase — auto-extending
|
|
268
|
+
schemas at registration time would obscure the source of truth ("did
|
|
269
|
+
the spec declare this, or did the framework graft it on?") and quietly
|
|
270
|
+
shadow consumer fields named `acting` that aren't the canonical
|
|
271
|
+
`ActingActor`. The asymmetry is the design rule: derive what the
|
|
272
|
+
framework emits, never what the consumer authors. The keeper
|
|
273
|
+
`db_routes` bug (an early consumer registration failure caught by
|
|
274
|
+
invariant 2's throw) was the empirical confirmation.
|
|
242
275
|
|
|
243
276
|
### `ERROR_*` constants by category
|
|
244
277
|
|
|
245
278
|
- **Validation**: `ERROR_INVALID_REQUEST_BODY`, `ERROR_INVALID_JSON_BODY`,
|
|
246
279
|
`ERROR_INVALID_ROUTE_PARAMS`, `ERROR_INVALID_QUERY_PARAMS`
|
|
247
280
|
- **Auth**: `ERROR_AUTHENTICATION_REQUIRED`, `ERROR_INSUFFICIENT_PERMISSIONS`,
|
|
248
|
-
`
|
|
249
|
-
`ERROR_PAYLOAD_TOO_LARGE`
|
|
281
|
+
`ERROR_CREDENTIAL_TYPE_REQUIRED`, `ERROR_RATE_LIMIT_EXCEEDED`,
|
|
282
|
+
`ERROR_INVALID_CREDENTIALS`, `ERROR_PAYLOAD_TOO_LARGE`
|
|
250
283
|
- **Origin + bearer**: `ERROR_FORBIDDEN_ORIGIN`, `ERROR_FORBIDDEN_REFERER`,
|
|
251
284
|
`ERROR_BEARER_REJECTED_BROWSER`, `ERROR_INVALID_TOKEN`, `ERROR_ACCOUNT_NOT_FOUND`
|
|
252
|
-
- **Keeper/daemon**: `
|
|
253
|
-
`
|
|
254
|
-
`ERROR_KEEPER_ACCOUNT_NOT_FOUND`
|
|
285
|
+
- **Keeper/daemon**: `ERROR_INVALID_DAEMON_TOKEN`,
|
|
286
|
+
`ERROR_KEEPER_ACCOUNT_NOT_CONFIGURED`, `ERROR_KEEPER_ACCOUNT_NOT_FOUND`
|
|
255
287
|
- **Bootstrap**: `ERROR_ALREADY_BOOTSTRAPPED`, `ERROR_TOKEN_FILE_MISSING`,
|
|
256
288
|
`ERROR_BOOTSTRAP_NOT_CONFIGURED`
|
|
257
289
|
- **Signup/invites**: `ERROR_NO_MATCHING_INVITE`, `ERROR_SIGNUP_CONFLICT`,
|
|
258
290
|
`ERROR_INVITE_NOT_FOUND`, `ERROR_INVITE_MISSING_IDENTIFIER`,
|
|
259
291
|
`ERROR_INVITE_DUPLICATE`, `ERROR_INVITE_ACCOUNT_EXISTS_USERNAME`,
|
|
260
292
|
`ERROR_INVITE_ACCOUNT_EXISTS_EMAIL`
|
|
261
|
-
- **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `
|
|
293
|
+
- **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_ROLE_GRANT_NOT_FOUND`,
|
|
262
294
|
`ERROR_INVALID_EVENT_TYPE`
|
|
263
295
|
- **DB browser**: `ERROR_FOREIGN_KEY_VIOLATION`, `ERROR_TABLE_NOT_FOUND`,
|
|
264
296
|
`ERROR_TABLE_NO_PRIMARY_KEY`, `ERROR_ROW_NOT_FOUND`
|
|
@@ -288,7 +320,7 @@ Key helpers:
|
|
|
288
320
|
`'*'`, exact, `'/api/*'` prefix (handles `prefix.slice(0, -1)` so
|
|
289
321
|
`/api/*` also matches the bare `/api`)
|
|
290
322
|
- `merge_error_schemas(spec, middleware_errors?)` — three-layer merge
|
|
291
|
-
described above
|
|
323
|
+
described above.
|
|
292
324
|
|
|
293
325
|
## Surface Generation
|
|
294
326
|
|
|
@@ -326,8 +358,7 @@ Key helpers:
|
|
|
326
358
|
— `description`, `sensitivity`, and probes `safeParse(undefined)` to
|
|
327
359
|
detect `optional` + `has_default`
|
|
328
360
|
- `events_to_surface(event_specs)` — SSE events surface as `{method, description, channel, params_schema}`
|
|
329
|
-
- RPC methods surface
|
|
330
|
-
see `../actions/CLAUDE.md` §HTTP bridge) so `ActionAuth` translates to the shared `RouteAuth` shape
|
|
361
|
+
- RPC methods surface their `RouteAuth` directly — same shape on both `ActionSpec.auth` and `RouteSpec.auth`, no translation step.
|
|
331
362
|
|
|
332
363
|
`create_app_surface_spec(options)` = `generate_app_surface(options)` plus
|
|
333
364
|
the source specs, for tests that need to iterate over raw specs.
|
|
@@ -341,11 +372,21 @@ No side effects, no state — filters and groupings over `AppSurface`:
|
|
|
341
372
|
- `filter_routes_by_prefix(prefix)` / `filter_routes_with_input` /
|
|
342
373
|
`filter_routes_with_params` / `filter_routes_with_query` /
|
|
343
374
|
`filter_mutation_routes` / `filter_rate_limited_routes`
|
|
344
|
-
- `routes_by_auth_type(surface)` — `Map<'none' | 'authenticated' | 'keeper' | 'role
|
|
375
|
+
- `routes_by_auth_type(surface)` — `Map<RouteAuthCategory, Array<AppSurfaceRoute>>` where `RouteAuthCategory = 'none' | 'authenticated' | 'optional' | 'keeper' | 'role:<name>' | 'other'`. Multi-role specs appear under each of their role buckets; the `'optional'` and `'other'` buckets exist for shapes that don't fit the four-axis categorical view.
|
|
345
376
|
- `format_route_key(route)` → `'METHOD /path'`
|
|
346
377
|
- `surface_auth_summary(surface)` — counts per auth type, roles broken
|
|
347
378
|
out by name
|
|
348
379
|
|
|
380
|
+
The per-route auth predicates these filters compose over (`is_public_auth`,
|
|
381
|
+
`is_role_auth`, `is_credential_gated_auth`, `is_keeper_auth`,
|
|
382
|
+
`is_plain_authenticated_auth`, plus `needs_actor` / `needs_account`)
|
|
383
|
+
live in `auth_shape.ts` next to the canonical `RouteAuth` schema —
|
|
384
|
+
import them from there, not from this module. Same predicates back the
|
|
385
|
+
dispatcher's authorization phase, the route-spec auth-guard resolver,
|
|
386
|
+
`derive_error_schemas`'s actor-failure folding, and the testing
|
|
387
|
+
harnesses, so every consumer that branches on the four-axis shape
|
|
388
|
+
shares one source of truth.
|
|
389
|
+
|
|
349
390
|
Consumer code (tests, attack-surface helpers, `SurfaceExplorer.svelte`)
|
|
350
391
|
should reach for these rather than inlining `.filter` chains.
|
|
351
392
|
|
|
@@ -383,11 +424,27 @@ Must run **before** auth and rate-limiting middleware. See the root
|
|
|
383
424
|
- `parse_proxy_entry(entry)` — accepts `'127.0.0.1'`, `'::1'`,
|
|
384
425
|
`'10.0.0.0/8'`, `'fe80::/10'`. Throws on invalid IPs, NaN/negative/
|
|
385
426
|
over-range prefix, non-network-aligned CIDRs, or bad input
|
|
386
|
-
-
|
|
427
|
+
- **`validate_ip_strict(ip)`** — defensive validator for any IP string
|
|
428
|
+
read from an untrusted source. Hono's `distinctRemoteAddr` is lax —
|
|
429
|
+
classifies anything-with-colons as `'IPv6'`, and
|
|
430
|
+
`convertIPv6ToBinary` silently accepts `'[::1]:8080'`, `'::1\n'`,
|
|
431
|
+
etc. as binary-valid IPv6. The two-layer check here (character-set
|
|
432
|
+
pre-filter + round-trip through `convertIPv*ToBinary`) closes both
|
|
433
|
+
holes: returns `'IPv4' | 'IPv6'` on a strictly-valid bare literal,
|
|
434
|
+
`undefined` on anything else.
|
|
435
|
+
- `is_trusted_ip(ip, proxies)` — normalizes before matching; uses
|
|
436
|
+
`validate_ip_strict` to reject malformed input up front (without it,
|
|
437
|
+
CIDR proxies would surface a 500 from a thrown
|
|
438
|
+
`convertIPv6ToBinary` on entries like `'203.0.113.1:8080'`); skips
|
|
387
439
|
mismatched address families for CIDR matches
|
|
388
440
|
- `resolve_client_ip(forwarded_for, proxies)` — walks **right-to-left**,
|
|
389
|
-
skipping trusted entries
|
|
390
|
-
|
|
441
|
+
skipping trusted entries AND any entry that fails strict validation
|
|
442
|
+
(closes the rate-limit-key poisoning surface where an attacker who
|
|
443
|
+
controls XFF and transits through a trusted proxy could rotate
|
|
444
|
+
garbage strings to evade per-IP limits). First untrusted +
|
|
445
|
+
strictly-valid wins. If everything is trusted-or-malformed, returns
|
|
446
|
+
the leftmost strictly-valid entry, or `undefined` to let the
|
|
447
|
+
middleware fall back to the connection IP
|
|
391
448
|
- `create_proxy_middleware(options)` + `create_proxy_middleware_spec(options)` —
|
|
392
449
|
three-branch logic:
|
|
393
450
|
1. No XFF → use connection IP directly
|
|
@@ -398,10 +455,11 @@ Must run **before** auth and rate-limiting middleware. See the root
|
|
|
398
455
|
- `get_client_ip(c)` — returns `'unknown'` when the proxy middleware
|
|
399
456
|
hasn't run
|
|
400
457
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
458
|
+
Tradeoff for the strict validation: legitimate non-standard proxies
|
|
459
|
+
that include ports in XFF entries (`203.0.113.1:8080`) lose per-client
|
|
460
|
+
distinction in rate limiting and collapse to the proxy's connection
|
|
461
|
+
IP (one bucket for everyone behind that proxy). nginx + cloud LBs
|
|
462
|
+
don't include ports — bounded by operator configuration in practice.
|
|
405
463
|
|
|
406
464
|
### Origin/Referer allowlist — `origin.ts`
|
|
407
465
|
|
|
@@ -553,31 +611,87 @@ Converters:
|
|
|
553
611
|
|
|
554
612
|
## Pending Effects
|
|
555
613
|
|
|
556
|
-
|
|
557
|
-
post-commit fan-out helper. Used for WS sends (`NotificationSender.send_to_account`
|
|
558
|
-
for permit-offer notifications — see `../auth/CLAUDE.md` §WS notifications) and any side effect that must run only
|
|
559
|
-
after the transaction commits.
|
|
614
|
+
Two queues, one timing contract each:
|
|
560
615
|
|
|
561
616
|
```typescript
|
|
562
|
-
interface
|
|
617
|
+
interface EmitAfterCommitContext {
|
|
563
618
|
log: Logger;
|
|
564
|
-
|
|
619
|
+
post_commit_effects: Array<() => void | Promise<void>>;
|
|
565
620
|
}
|
|
566
621
|
|
|
567
|
-
|
|
622
|
+
// `RouteContext` and `ActionContext` carry both:
|
|
623
|
+
// pending_effects: Array<Promise<void>>
|
|
624
|
+
// post_commit_effects: Array<() => void | Promise<void>>
|
|
568
625
|
```
|
|
569
626
|
|
|
570
|
-
|
|
627
|
+
- **`pending_effects: Array<Promise<void>>`** — eager. Producers push the
|
|
628
|
+
in-flight `Promise<void>` for fire-and-forget pool writes already
|
|
629
|
+
running: audit emits via `AppDeps.audit`, session-touch UPDATE,
|
|
630
|
+
api-token usage tracking. The pool write is rollback-resilient by
|
|
631
|
+
virtue of running outside the request transaction; pushing the
|
|
632
|
+
in-flight handle lets test mode (`await_pending_effects: true`) await
|
|
633
|
+
it. Drain rule: `flush_pending_effects(effects, log, on_rejection?)`.
|
|
634
|
+
- **`post_commit_effects: Array<() => void | Promise<void>>`** —
|
|
635
|
+
deferred. Producers go through `emit_after_commit(ctx, fn)` exclusively;
|
|
636
|
+
raw thunks should not be pushed directly. The flush middleware (in
|
|
637
|
+
`server/app_server.ts` and the per-message WS dispatcher in
|
|
638
|
+
`actions/register_action_ws.ts`) is the only site that invokes each
|
|
639
|
+
thunk, after the wrapping `db.transaction` (and the rest of the
|
|
640
|
+
handler chain) has resolved. Drain rule: `flush_post_commit_effects(effects, log)`.
|
|
641
|
+
|
|
642
|
+
### Why split
|
|
643
|
+
|
|
644
|
+
Both shapes used to coexist on a single `Array<PendingEffect>` discriminated
|
|
645
|
+
union. The shapes encode different contracts — eager pushers say "wait
|
|
646
|
+
for this work that's already started"; thunk pushers say "run this after
|
|
647
|
+
the handler returns" — and burying both behind one field made
|
|
648
|
+
`c.var.pending_effects.push(x)` ambiguous at the call site. Splitting
|
|
649
|
+
turns the field name into the contract.
|
|
650
|
+
|
|
651
|
+
### Why `emit_after_commit` defers
|
|
652
|
+
|
|
653
|
+
The thunk shape is **load-bearing for correctness**. Pushing
|
|
654
|
+
`Promise.resolve().then(fn)` onto an eager queue — what
|
|
655
|
+
`emit_after_commit` used to do — schedules `fn` as a microtask that
|
|
656
|
+
drains _before_ the wrapping `await db.query('COMMIT')` resumes, so a
|
|
657
|
+
rolled-back transaction would leak a notification for state that never
|
|
658
|
+
landed. The thunk defers the work to flush time; the `try/finally` in the
|
|
659
|
+
flush middleware runs after the handler (and any wrapping transaction)
|
|
660
|
+
returns.
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
emit_after_commit(ctx, () => notification_sender.send_to_account(account_id, msg));
|
|
664
|
+
```
|
|
571
665
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
-
|
|
579
|
-
|
|
580
|
-
|
|
666
|
+
Used for WS sends (`NotificationSender.send_to_account` for
|
|
667
|
+
role-grant-offer notifications — see `../auth/CLAUDE.md` §WS notifications)
|
|
668
|
+
and any side effect that must run only after the transaction commits.
|
|
669
|
+
|
|
670
|
+
### Key properties
|
|
671
|
+
|
|
672
|
+
- **The flush owns the safety net.** `flush_post_commit_effects` wraps
|
|
673
|
+
every thunk in `try/catch` and routes errors through `ctx.log.error`,
|
|
674
|
+
so one failing send cannot starve sibling effects in the same batch
|
|
675
|
+
nor corrupt the already-committed response. Per-thunk `try/catch`
|
|
676
|
+
inside `emit_after_commit` would skip directly-pushed thunks (e.g.
|
|
677
|
+
tests); centralizing the wrap in the flush closes that gap.
|
|
678
|
+
- **Test mode (`await_pending_effects: true`) flushes both queues.**
|
|
679
|
+
Eager: `await flush_pending_effects(pending_effects, log)`. Deferred:
|
|
680
|
+
`await flush_post_commit_effects(post_commit_effects, log)`. Both
|
|
681
|
+
complete before the response returns. Production mode wraps the same
|
|
682
|
+
helpers in `void ...` and threads `on_effect_error` into
|
|
683
|
+
`flush_pending_effects`'s `on_rejection` callback for fan-out.
|
|
684
|
+
- **Same drain location for both.** The outer flush middleware
|
|
685
|
+
(`server/app_server.ts`) and the per-message WS flush handle the two
|
|
686
|
+
queues adjacent to each other. The deferred queue does not drain inside
|
|
687
|
+
the route-spec wrapper / `perform_action` — that would tighten the
|
|
688
|
+
"post-commit" timing further but would force three drain sites (REST
|
|
689
|
+
wrapper, RPC dispatcher, WS dispatcher) to gain timing no current
|
|
690
|
+
consumer needs (the WS-fan-out use case is happy with post-handler).
|
|
691
|
+
- Structurally satisfied by both `RouteContext` (HTTP) and
|
|
692
|
+
`ActionContext` (RPC + WS) — they share the `{log, post_commit_effects}`
|
|
693
|
+
shape, which is why this helper lives in `http/` rather than
|
|
694
|
+
`actions/` or `auth/`.
|
|
581
695
|
|
|
582
696
|
WS sends are **not** wrapped by `create_validated_broadcaster` (that only
|
|
583
697
|
guards SSE `broadcast(channel, data)`). Zod input schemas on
|
|
@@ -618,7 +732,7 @@ a generic table browser; the factory is domain-agnostic.
|
|
|
618
732
|
or table missing (`ERROR_TABLE_NOT_FOUND`), 409 on FK violation (pg
|
|
619
733
|
error code `23503`)
|
|
620
734
|
|
|
621
|
-
All four routes use `{
|
|
735
|
+
All four routes use the keeper auth shape (`{account: 'required', actor: 'required', roles: ['keeper'], credential_types: ['daemon_token']}`). Param schemas use
|
|
622
736
|
`VALID_SQL_IDENTIFIER` regex, and every table name gets
|
|
623
737
|
`assert_valid_sql_identifier()` before string-interpolating into SQL —
|
|
624
738
|
the identifier validation is the only reason the interpolation is safe.
|