@fuzdev/fuz_app 0.54.0 → 0.56.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 +214 -103
- 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 +32 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +35 -15
- 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 +141 -22
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +106 -187
- 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 +46 -40
- 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 +15 -10
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +54 -7
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +0 -4
- 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 +794 -410
- 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 +7 -13
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +26 -35
- package/dist/auth/account_queries.d.ts +52 -16
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +87 -38
- package/dist/auth/account_routes.d.ts +9 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +118 -46
- package/dist/auth/account_schema.d.ts +46 -35
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +21 -28
- package/dist/auth/admin_action_specs.d.ts +100 -32
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +64 -33
- package/dist/auth/admin_actions.d.ts +13 -19
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +37 -41
- 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 -48
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +20 -56
- 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 +92 -32
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +75 -46
- 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/bearer_auth.d.ts +9 -7
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +13 -21
- 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 -42
- 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 +23 -11
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +28 -22
- 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 -18
- 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 +9 -4
- package/dist/auth/migrations.d.ts +37 -14
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +79 -32
- package/dist/auth/request_context.d.ts +331 -61
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +378 -95
- package/dist/auth/{permit_offer_action_specs.d.ts → role_grant_offer_action_specs.d.ts} +163 -94
- 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/role_grant_offer_actions.js +473 -0
- package/dist/auth/{permit_offer_notifications.d.ts → role_grant_offer_notifications.d.ts} +90 -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/role_grant_offer_queries.d.ts +242 -0
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -0
- package/dist/auth/role_grant_offer_queries.js +533 -0
- 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} +60 -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 +6 -1
- package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
- package/dist/auth/self_service_role_action_specs.js +3 -1
- package/dist/auth/self_service_role_actions.d.ts +34 -27
- package/dist/auth/self_service_role_actions.d.ts.map +1 -1
- package/dist/auth/self_service_role_actions.js +68 -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 +12 -8
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +10 -7
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +9 -7
- package/dist/env/load.d.ts +1 -1
- package/dist/env/load.js +1 -1
- package/dist/hono_context.d.ts +64 -5
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +38 -2
- package/dist/http/CLAUDE.md +264 -87
- 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 +132 -19
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +132 -40
- package/dist/http/jsonrpc_errors.d.ts +27 -2
- package/dist/http/jsonrpc_errors.d.ts.map +1 -1
- package/dist/http/jsonrpc_errors.js +26 -2
- 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 +113 -41
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +130 -52
- package/dist/http/schema_helpers.d.ts +3 -2
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +9 -2
- package/dist/http/surface.d.ts +2 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +1 -2
- 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 +36 -31
- package/dist/server/validate_nginx.d.ts +1 -1
- package/dist/server/validate_nginx.js +1 -1
- package/dist/testing/CLAUDE.md +73 -55
- 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 +100 -96
- package/dist/testing/adversarial_headers.js +1 -1
- 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 +18 -17
- 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 +53 -39
- 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 +28 -22
- 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 +10 -8
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +22 -18
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +13 -14
- package/dist/testing/integration_helpers.d.ts +8 -6
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +29 -23
- package/dist/testing/middleware.d.ts +15 -11
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +75 -32
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +40 -24
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +3 -1
- 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 +2 -2
- 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 +24 -12
- 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 +65 -59
- package/dist/ui/{PermitOfferForm.svelte → RoleGrantOfferForm.svelte} +37 -22
- package/dist/ui/RoleGrantOfferForm.svelte.d.ts +20 -0
- 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 +25 -18
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +28 -17
- 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} +39 -31
- 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} +25 -19
- 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 -227
- 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_actions.js +0 -452
- 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 +0 -183
- package/dist/auth/permit_offer_queries.d.ts.map +0 -1
- package/dist/auth/permit_offer_queries.js +0 -408
- package/dist/auth/permit_offer_schema.d.ts +0 -103
- package/dist/auth/permit_offer_schema.d.ts.map +0 -1
- package/dist/auth/permit_queries.d.ts +0 -210
- package/dist/auth/permit_queries.d.ts.map +0 -1
- package/dist/auth/permit_queries.js +0 -294
- 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 -21
- package/dist/auth/route_guards.d.ts.map +0 -1
- package/dist/auth/route_guards.js +0 -32
- 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 +0 -14
- 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
|
@@ -1,33 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Two-queue side-effect machinery for request handlers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
4
|
+
* Handlers register fire-and-forget work in one of two queues, distinguished
|
|
5
|
+
* by their timing contract:
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* - `pending_effects: Array<Promise<void>>` — eager. Producers push pool
|
|
8
|
+
* writes that are already in flight (audit emits, session-touch UPDATE,
|
|
9
|
+
* api-token usage tracking). The pool write is rollback-resilient by
|
|
10
|
+
* virtue of running outside the request transaction; pushing the
|
|
11
|
+
* in-flight handle lets test mode (`await_pending_effects: true`) await
|
|
12
|
+
* it.
|
|
13
|
+
* - `post_commit_effects: Array<() => void | Promise<void>>` — deferred.
|
|
14
|
+
* Producers go through `emit_after_commit(ctx, fn)`; the flush
|
|
15
|
+
* middleware is the only site that ever invokes the thunk, and it does
|
|
16
|
+
* so after the request handler (and its wrapping `db.transaction`)
|
|
17
|
+
* returns. Used for WS sends and any work that must observe a committed
|
|
18
|
+
* transaction.
|
|
19
|
+
*
|
|
20
|
+
* The split exists because the two shapes encode different contracts:
|
|
21
|
+
* eager pushers are saying "wait for this work that's already started";
|
|
22
|
+
* thunk pushers are saying "run this after the handler returns." Burying
|
|
23
|
+
* both behind one `Array<PendingEffect>` made `c.var.pending_effects.push(x)`
|
|
24
|
+
* ambiguous at the call site. With separate queues, the field name is
|
|
25
|
+
* the contract.
|
|
26
|
+
*
|
|
27
|
+
* Both `RouteContext` (HTTP routes) and `ActionContext` (RPC + WS
|
|
28
|
+
* actions) carry both queues by convention, so this module stays in
|
|
29
|
+
* `http/` (every transport depends on it).
|
|
12
30
|
*
|
|
13
31
|
* @module
|
|
14
32
|
*/
|
|
15
33
|
import type { Logger } from '@fuzdev/fuz_util/log.js';
|
|
16
|
-
/**
|
|
17
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Minimal structural context required by `emit_after_commit`. Both
|
|
36
|
+
* `RouteContext` and `ActionContext` satisfy this — they each carry
|
|
37
|
+
* `log` and `post_commit_effects`.
|
|
38
|
+
*/
|
|
39
|
+
export interface EmitAfterCommitContext {
|
|
18
40
|
log: Logger;
|
|
19
|
-
|
|
41
|
+
post_commit_effects: Array<() => void | Promise<void>>;
|
|
20
42
|
}
|
|
21
43
|
/**
|
|
22
44
|
* Defer a side effect until after the handler's transaction commits.
|
|
23
45
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
46
|
+
* Pushes a raw thunk onto `ctx.post_commit_effects` — the flush
|
|
47
|
+
* middleware (in `server/app_server.ts` and the per-message WS dispatcher)
|
|
48
|
+
* is the only site that ever invokes `fn`. This is load-bearing: a
|
|
49
|
+
* previous implementation queued `Promise.resolve().then(fn)`, which
|
|
50
|
+
* JavaScript's microtask scheduler drains before the wrapping
|
|
51
|
+
* `await db.query('COMMIT')` resumes — `fn` fired mid-transaction and a
|
|
52
|
+
* rollback would leak a notification for state that never landed.
|
|
53
|
+
*
|
|
54
|
+
* The thunk shape closes that gap by deferring the work to flush time.
|
|
55
|
+
* The flush owns the per-thunk `try/catch` + `log.error` so any
|
|
56
|
+
* directly-pushed thunk (tests included) cannot escape the safety net.
|
|
57
|
+
*
|
|
58
|
+
* @param ctx - context carrying `log` and the `post_commit_effects` queue
|
|
59
|
+
* @param fn - side effect to run after commit; may return `void` or `Promise<void>`
|
|
60
|
+
* @mutates `ctx.post_commit_effects` - appends `fn` verbatim
|
|
61
|
+
*/
|
|
62
|
+
export declare const emit_after_commit: (ctx: EmitAfterCommitContext, fn: () => void | Promise<void>) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Drain an eager `pending_effects` queue: `Promise.allSettled` the
|
|
65
|
+
* in-flight handles, route every rejection through `log.error`, and
|
|
66
|
+
* fan out to `on_rejection` when supplied (production wires this to
|
|
67
|
+
* `on_effect_error` for monitoring).
|
|
68
|
+
*
|
|
69
|
+
* Returned promise resolves once every effect has settled. Never
|
|
70
|
+
* rejects. No-op when `effects` is empty (common on read-only
|
|
71
|
+
* requests).
|
|
72
|
+
*
|
|
73
|
+
* Symmetric with `flush_post_commit_effects` for the deferred queue.
|
|
74
|
+
*/
|
|
75
|
+
export declare const flush_pending_effects: (effects: ReadonlyArray<Promise<void>>, log: Logger, on_rejection?: (reason: unknown) => void) => Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Drain a `post_commit_effects` queue: invoke each thunk under
|
|
78
|
+
* `try/catch`, collect any returned promises, and `Promise.allSettled`
|
|
79
|
+
* them. Synchronous throws and async rejections are routed through
|
|
80
|
+
* `log.error` so one failing effect cannot starve siblings.
|
|
27
81
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @mutates `ctx.pending_effects` - appends a never-rejecting promise wrapping `fn`
|
|
82
|
+
* Returned promise resolves once every thunk has finished. Never
|
|
83
|
+
* rejects.
|
|
31
84
|
*/
|
|
32
|
-
export declare const
|
|
85
|
+
export declare const flush_post_commit_effects: (effects: ReadonlyArray<() => void | Promise<void>>, log: Logger) => Promise<void>;
|
|
33
86
|
//# sourceMappingURL=pending_effects.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending_effects.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/pending_effects.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pending_effects.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/pending_effects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,mBAAmB,EAAE,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,KAAK,sBAAsB,EAC3B,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAC5B,IAEF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EACrC,KAAK,MAAM,EACX,eAAe,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,KACtC,OAAO,CAAC,IAAI,CASd,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GACrC,SAAS,aAAa,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,EAClD,KAAK,MAAM,KACT,OAAO,CAAC,IAAI,CAiBd,CAAC"}
|
|
@@ -1,35 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Two-queue side-effect machinery for request handlers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
4
|
+
* Handlers register fire-and-forget work in one of two queues, distinguished
|
|
5
|
+
* by their timing contract:
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* - `pending_effects: Array<Promise<void>>` — eager. Producers push pool
|
|
8
|
+
* writes that are already in flight (audit emits, session-touch UPDATE,
|
|
9
|
+
* api-token usage tracking). The pool write is rollback-resilient by
|
|
10
|
+
* virtue of running outside the request transaction; pushing the
|
|
11
|
+
* in-flight handle lets test mode (`await_pending_effects: true`) await
|
|
12
|
+
* it.
|
|
13
|
+
* - `post_commit_effects: Array<() => void | Promise<void>>` — deferred.
|
|
14
|
+
* Producers go through `emit_after_commit(ctx, fn)`; the flush
|
|
15
|
+
* middleware is the only site that ever invokes the thunk, and it does
|
|
16
|
+
* so after the request handler (and its wrapping `db.transaction`)
|
|
17
|
+
* returns. Used for WS sends and any work that must observe a committed
|
|
18
|
+
* transaction.
|
|
19
|
+
*
|
|
20
|
+
* The split exists because the two shapes encode different contracts:
|
|
21
|
+
* eager pushers are saying "wait for this work that's already started";
|
|
22
|
+
* thunk pushers are saying "run this after the handler returns." Burying
|
|
23
|
+
* both behind one `Array<PendingEffect>` made `c.var.pending_effects.push(x)`
|
|
24
|
+
* ambiguous at the call site. With separate queues, the field name is
|
|
25
|
+
* the contract.
|
|
26
|
+
*
|
|
27
|
+
* Both `RouteContext` (HTTP routes) and `ActionContext` (RPC + WS
|
|
28
|
+
* actions) carry both queues by convention, so this module stays in
|
|
29
|
+
* `http/` (every transport depends on it).
|
|
12
30
|
*
|
|
13
31
|
* @module
|
|
14
32
|
*/
|
|
15
33
|
/**
|
|
16
34
|
* Defer a side effect until after the handler's transaction commits.
|
|
17
35
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
36
|
+
* Pushes a raw thunk onto `ctx.post_commit_effects` — the flush
|
|
37
|
+
* middleware (in `server/app_server.ts` and the per-message WS dispatcher)
|
|
38
|
+
* is the only site that ever invokes `fn`. This is load-bearing: a
|
|
39
|
+
* previous implementation queued `Promise.resolve().then(fn)`, which
|
|
40
|
+
* JavaScript's microtask scheduler drains before the wrapping
|
|
41
|
+
* `await db.query('COMMIT')` resumes — `fn` fired mid-transaction and a
|
|
42
|
+
* rollback would leak a notification for state that never landed.
|
|
21
43
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
44
|
+
* The thunk shape closes that gap by deferring the work to flush time.
|
|
45
|
+
* The flush owns the per-thunk `try/catch` + `log.error` so any
|
|
46
|
+
* directly-pushed thunk (tests included) cannot escape the safety net.
|
|
47
|
+
*
|
|
48
|
+
* @param ctx - context carrying `log` and the `post_commit_effects` queue
|
|
49
|
+
* @param fn - side effect to run after commit; may return `void` or `Promise<void>`
|
|
50
|
+
* @mutates `ctx.post_commit_effects` - appends `fn` verbatim
|
|
25
51
|
*/
|
|
26
52
|
export const emit_after_commit = (ctx, fn) => {
|
|
27
|
-
ctx.
|
|
53
|
+
ctx.post_commit_effects.push(fn);
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Drain an eager `pending_effects` queue: `Promise.allSettled` the
|
|
57
|
+
* in-flight handles, route every rejection through `log.error`, and
|
|
58
|
+
* fan out to `on_rejection` when supplied (production wires this to
|
|
59
|
+
* `on_effect_error` for monitoring).
|
|
60
|
+
*
|
|
61
|
+
* Returned promise resolves once every effect has settled. Never
|
|
62
|
+
* rejects. No-op when `effects` is empty (common on read-only
|
|
63
|
+
* requests).
|
|
64
|
+
*
|
|
65
|
+
* Symmetric with `flush_post_commit_effects` for the deferred queue.
|
|
66
|
+
*/
|
|
67
|
+
export const flush_pending_effects = async (effects, log, on_rejection) => {
|
|
68
|
+
if (effects.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
const results = await Promise.allSettled(effects);
|
|
71
|
+
for (const result of results) {
|
|
72
|
+
if (result.status === 'rejected') {
|
|
73
|
+
log.error('pending effect rejected:', result.reason);
|
|
74
|
+
on_rejection?.(result.reason);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Drain a `post_commit_effects` queue: invoke each thunk under
|
|
80
|
+
* `try/catch`, collect any returned promises, and `Promise.allSettled`
|
|
81
|
+
* them. Synchronous throws and async rejections are routed through
|
|
82
|
+
* `log.error` so one failing effect cannot starve siblings.
|
|
83
|
+
*
|
|
84
|
+
* Returned promise resolves once every thunk has finished. Never
|
|
85
|
+
* rejects.
|
|
86
|
+
*/
|
|
87
|
+
export const flush_post_commit_effects = async (effects, log) => {
|
|
88
|
+
const promises = [];
|
|
89
|
+
for (const fn of effects) {
|
|
28
90
|
try {
|
|
29
|
-
fn();
|
|
91
|
+
const result = fn();
|
|
92
|
+
if (result instanceof Promise) {
|
|
93
|
+
promises.push(result.catch((err) => {
|
|
94
|
+
log.error('post-commit side effect failed:', err);
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
30
97
|
}
|
|
31
98
|
catch (err) {
|
|
32
|
-
|
|
99
|
+
log.error('post-commit side effect failed:', err);
|
|
33
100
|
}
|
|
34
|
-
}
|
|
101
|
+
}
|
|
102
|
+
if (promises.length)
|
|
103
|
+
await Promise.allSettled(promises);
|
|
35
104
|
};
|
package/dist/http/proxy.d.ts
CHANGED
|
@@ -54,23 +54,70 @@ export type ParsedProxy = {
|
|
|
54
54
|
* @throws Error on invalid IP, invalid CIDR network, or NaN/negative/over-range prefix
|
|
55
55
|
*/
|
|
56
56
|
export declare const parse_proxy_entry: (entry: string) => ParsedProxy;
|
|
57
|
+
/**
|
|
58
|
+
* Strict IP validity check.
|
|
59
|
+
*
|
|
60
|
+
* Defense in depth around Hono's `hono/utils/ipaddr` helpers, which are
|
|
61
|
+
* lax in two ways:
|
|
62
|
+
*
|
|
63
|
+
* 1. `distinctRemoteAddr` classifies anything-with-a-colon as `'IPv6'`,
|
|
64
|
+
* including `'host:port'`, `'attacker:controlled'`, `'203.0.113.1:8080'`.
|
|
65
|
+
* 2. `convertIPv6ToBinary` silently accepts malformed forms like
|
|
66
|
+
* `'[::1]:8080'` and `'::1\n'`, parsing them as inconsistent binary
|
|
67
|
+
* values that would still serve as distinct rate-limit keys for an
|
|
68
|
+
* attacker rotating the suffix.
|
|
69
|
+
*
|
|
70
|
+
* Strict validation here is two-layered: a character-set pre-filter
|
|
71
|
+
* (`IP_LITERAL_CHARS`), then a round-trip through `convertIPv*ToBinary`
|
|
72
|
+
* to confirm the input parses cleanly. Either layer alone has holes;
|
|
73
|
+
* together they reject every input form we've seen Hono mis-handle.
|
|
74
|
+
*
|
|
75
|
+
* Used as the security primitive for any code path that takes an IP
|
|
76
|
+
* string from an untrusted source (XFF, query params) and uses it as a
|
|
77
|
+
* key (rate limiting, audit subject) or compares it against trusted
|
|
78
|
+
* proxies via CIDR (where the latent throw would otherwise bubble out).
|
|
79
|
+
*
|
|
80
|
+
* @returns the address family on success, `undefined` if the string is
|
|
81
|
+
* not a strictly-valid IP
|
|
82
|
+
*/
|
|
83
|
+
export declare const validate_ip_strict: (ip: string) => "IPv4" | "IPv6" | undefined;
|
|
57
84
|
/**
|
|
58
85
|
* Check whether `ip` matches any entry in the trusted proxy list.
|
|
59
86
|
*
|
|
60
87
|
* Normalizes `ip` before matching (lowercase, IPv4-mapped IPv6 stripped).
|
|
88
|
+
* Uses `validate_ip_strict` to reject malformed input — without strict
|
|
89
|
+
* validation, Hono's lax `distinctRemoteAddr` would let an entry like
|
|
90
|
+
* `'203.0.113.1:8080'` (false-positive `'IPv6'`) reach
|
|
91
|
+
* `convertIPv6ToBinary` in the CIDR-match branch and throw.
|
|
61
92
|
*/
|
|
62
93
|
export declare const is_trusted_ip: (ip: string, proxies: Array<ParsedProxy>) => boolean;
|
|
63
94
|
/**
|
|
64
95
|
* Resolve the real client IP from an `X-Forwarded-For` header value.
|
|
65
96
|
*
|
|
66
|
-
* Walks right-to-left, skipping trusted proxy entries
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
97
|
+
* Walks right-to-left, skipping trusted proxy entries AND any entry
|
|
98
|
+
* that fails strict IP validation (`validate_ip_strict`). The first
|
|
99
|
+
* untrusted, strictly-valid entry is the client IP. If every walked
|
|
100
|
+
* entry is trusted or malformed, returns the leftmost strictly-valid
|
|
101
|
+
* (trusted) entry (likely-misconfigured all-trusted case) or
|
|
102
|
+
* `undefined` (everything was malformed — middleware falls back to
|
|
103
|
+
* the connection IP). All entries are normalized before matching and
|
|
104
|
+
* in the returned value.
|
|
105
|
+
*
|
|
106
|
+
* Skipping malformed entries is the rate-limit-key fix for the
|
|
107
|
+
* "attacker controls XFF and the proxy passes it through" surface —
|
|
108
|
+
* without the skip, an attacker could rotate arbitrary strings (incl.
|
|
109
|
+
* `'attacker:controlled'`, which Hono's lax `distinctRemoteAddr`
|
|
110
|
+
* misclassifies as IPv6) as XFF values to get fresh per-IP rate-limit
|
|
111
|
+
* buckets. Tradeoff: legitimate non-standard proxies that include
|
|
112
|
+
* ports in XFF entries (e.g. `203.0.113.1:8080`) also fail strict
|
|
113
|
+
* validation, so those entries get skipped and the rate-limit bucket
|
|
114
|
+
* collapses to the proxy's connection IP (one bucket for everyone
|
|
115
|
+
* behind that proxy). Standard proxies (nginx, cloud LBs) don't
|
|
116
|
+
* include ports.
|
|
70
117
|
*
|
|
71
118
|
* @param forwarded_for - the `X-Forwarded-For` header value
|
|
72
119
|
* @param proxies - parsed trusted proxy entries
|
|
73
|
-
* @returns the normalized client IP, or `undefined` if the header is empty
|
|
120
|
+
* @returns the normalized client IP, or `undefined` if the header is empty / all entries malformed
|
|
74
121
|
*/
|
|
75
122
|
export declare const resolve_client_ip: (forwarded_for: string, proxies: Array<ParsedProxy>) => string | undefined;
|
|
76
123
|
/**
|
package/dist/http/proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAErD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAEzD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,IAAI,MAAM,KAAG,MAQzC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,sFAAsF;IACtF,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,+DAA+D;IAC/D,iBAAiB,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GACpB;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAC7B;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;CAAC,CAAC;AAElF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,WA6CjD,CAAC;
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,OAAO,EAAE,iBAAiB,EAAC,MAAM,MAAM,CAAC;AAErD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAEzD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,IAAI,MAAM,KAAG,MAQzC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,sFAAsF;IACtF,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,+DAA+D;IAC/D,iBAAiB,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GACpB;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAC7B;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;CAAC,CAAC;AAElF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,WA6CjD,CAAC;AA2BF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,MAAM,KAAG,MAAM,GAAG,MAAM,GAAG,SAWjE,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,EAAE,SAAS,KAAK,CAAC,WAAW,CAAC,KAAG,OAqBvE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,iBAAiB,GAC7B,eAAe,MAAM,EACrB,SAAS,KAAK,CAAC,WAAW,CAAC,KACzB,MAAM,GAAG,SA0BX,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,YAAY,KAAG,iBAyC/D,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GAAI,SAAS,YAAY,KAAG,cAInE,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,GAAG,OAAO,KAAG,MAAyC,CAAC"}
|
package/dist/http/proxy.js
CHANGED
|
@@ -91,14 +91,70 @@ const cidr_contains = (ip_binary, network, prefix, total_bits) => {
|
|
|
91
91
|
const shift = BigInt(total_bits - prefix);
|
|
92
92
|
return ip_binary >> shift === network >> shift;
|
|
93
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* Allowed character set for a bare IP literal.
|
|
96
|
+
*
|
|
97
|
+
* Covers the union of IPv4 (digits + `.`), IPv6 (hex digits + `:`), and
|
|
98
|
+
* IPv4-mapped IPv6 forms (`::ffff:127.0.0.1`). Anything outside this
|
|
99
|
+
* set — brackets, whitespace, control bytes, letters g-z — disqualifies
|
|
100
|
+
* the input regardless of what Hono's parser does with it.
|
|
101
|
+
*/
|
|
102
|
+
const IP_LITERAL_CHARS = /^[0-9a-fA-F.:]+$/;
|
|
103
|
+
/**
|
|
104
|
+
* Strict IP validity check.
|
|
105
|
+
*
|
|
106
|
+
* Defense in depth around Hono's `hono/utils/ipaddr` helpers, which are
|
|
107
|
+
* lax in two ways:
|
|
108
|
+
*
|
|
109
|
+
* 1. `distinctRemoteAddr` classifies anything-with-a-colon as `'IPv6'`,
|
|
110
|
+
* including `'host:port'`, `'attacker:controlled'`, `'203.0.113.1:8080'`.
|
|
111
|
+
* 2. `convertIPv6ToBinary` silently accepts malformed forms like
|
|
112
|
+
* `'[::1]:8080'` and `'::1\n'`, parsing them as inconsistent binary
|
|
113
|
+
* values that would still serve as distinct rate-limit keys for an
|
|
114
|
+
* attacker rotating the suffix.
|
|
115
|
+
*
|
|
116
|
+
* Strict validation here is two-layered: a character-set pre-filter
|
|
117
|
+
* (`IP_LITERAL_CHARS`), then a round-trip through `convertIPv*ToBinary`
|
|
118
|
+
* to confirm the input parses cleanly. Either layer alone has holes;
|
|
119
|
+
* together they reject every input form we've seen Hono mis-handle.
|
|
120
|
+
*
|
|
121
|
+
* Used as the security primitive for any code path that takes an IP
|
|
122
|
+
* string from an untrusted source (XFF, query params) and uses it as a
|
|
123
|
+
* key (rate limiting, audit subject) or compares it against trusted
|
|
124
|
+
* proxies via CIDR (where the latent throw would otherwise bubble out).
|
|
125
|
+
*
|
|
126
|
+
* @returns the address family on success, `undefined` if the string is
|
|
127
|
+
* not a strictly-valid IP
|
|
128
|
+
*/
|
|
129
|
+
export const validate_ip_strict = (ip) => {
|
|
130
|
+
if (!IP_LITERAL_CHARS.test(ip))
|
|
131
|
+
return undefined;
|
|
132
|
+
const type = distinctRemoteAddr(ip);
|
|
133
|
+
if (!type)
|
|
134
|
+
return undefined;
|
|
135
|
+
try {
|
|
136
|
+
if (type === 'IPv4')
|
|
137
|
+
convertIPv4ToBinary(ip);
|
|
138
|
+
else
|
|
139
|
+
convertIPv6ToBinary(ip);
|
|
140
|
+
return type;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
94
146
|
/**
|
|
95
147
|
* Check whether `ip` matches any entry in the trusted proxy list.
|
|
96
148
|
*
|
|
97
149
|
* Normalizes `ip` before matching (lowercase, IPv4-mapped IPv6 stripped).
|
|
150
|
+
* Uses `validate_ip_strict` to reject malformed input — without strict
|
|
151
|
+
* validation, Hono's lax `distinctRemoteAddr` would let an entry like
|
|
152
|
+
* `'203.0.113.1:8080'` (false-positive `'IPv6'`) reach
|
|
153
|
+
* `convertIPv6ToBinary` in the CIDR-match branch and throw.
|
|
98
154
|
*/
|
|
99
155
|
export const is_trusted_ip = (ip, proxies) => {
|
|
100
156
|
const normalized = normalize_ip(ip);
|
|
101
|
-
const address_type =
|
|
157
|
+
const address_type = validate_ip_strict(normalized);
|
|
102
158
|
if (!address_type)
|
|
103
159
|
return false;
|
|
104
160
|
for (const proxy of proxies) {
|
|
@@ -121,22 +177,33 @@ export const is_trusted_ip = (ip, proxies) => {
|
|
|
121
177
|
}
|
|
122
178
|
return false;
|
|
123
179
|
};
|
|
124
|
-
// NOTE: some non-standard proxies include ports in XFF entries (e.g.
|
|
125
|
-
// 203.0.113.1:8080). The entry fails distinctRemoteAddr and is treated as
|
|
126
|
-
// untrusted (safe default), but rate limiting keys on the port-suffixed
|
|
127
|
-
// string instead of the bare IP. Low risk — nginx and cloud LBs don't
|
|
128
|
-
// include ports.
|
|
129
180
|
/**
|
|
130
181
|
* Resolve the real client IP from an `X-Forwarded-For` header value.
|
|
131
182
|
*
|
|
132
|
-
* Walks right-to-left, skipping trusted proxy entries
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
183
|
+
* Walks right-to-left, skipping trusted proxy entries AND any entry
|
|
184
|
+
* that fails strict IP validation (`validate_ip_strict`). The first
|
|
185
|
+
* untrusted, strictly-valid entry is the client IP. If every walked
|
|
186
|
+
* entry is trusted or malformed, returns the leftmost strictly-valid
|
|
187
|
+
* (trusted) entry (likely-misconfigured all-trusted case) or
|
|
188
|
+
* `undefined` (everything was malformed — middleware falls back to
|
|
189
|
+
* the connection IP). All entries are normalized before matching and
|
|
190
|
+
* in the returned value.
|
|
191
|
+
*
|
|
192
|
+
* Skipping malformed entries is the rate-limit-key fix for the
|
|
193
|
+
* "attacker controls XFF and the proxy passes it through" surface —
|
|
194
|
+
* without the skip, an attacker could rotate arbitrary strings (incl.
|
|
195
|
+
* `'attacker:controlled'`, which Hono's lax `distinctRemoteAddr`
|
|
196
|
+
* misclassifies as IPv6) as XFF values to get fresh per-IP rate-limit
|
|
197
|
+
* buckets. Tradeoff: legitimate non-standard proxies that include
|
|
198
|
+
* ports in XFF entries (e.g. `203.0.113.1:8080`) also fail strict
|
|
199
|
+
* validation, so those entries get skipped and the rate-limit bucket
|
|
200
|
+
* collapses to the proxy's connection IP (one bucket for everyone
|
|
201
|
+
* behind that proxy). Standard proxies (nginx, cloud LBs) don't
|
|
202
|
+
* include ports.
|
|
136
203
|
*
|
|
137
204
|
* @param forwarded_for - the `X-Forwarded-For` header value
|
|
138
205
|
* @param proxies - parsed trusted proxy entries
|
|
139
|
-
* @returns the normalized client IP, or `undefined` if the header is empty
|
|
206
|
+
* @returns the normalized client IP, or `undefined` if the header is empty / all entries malformed
|
|
140
207
|
*/
|
|
141
208
|
export const resolve_client_ip = (forwarded_for, proxies) => {
|
|
142
209
|
const entries = [];
|
|
@@ -147,15 +214,26 @@ export const resolve_client_ip = (forwarded_for, proxies) => {
|
|
|
147
214
|
}
|
|
148
215
|
if (entries.length === 0)
|
|
149
216
|
return undefined;
|
|
150
|
-
// Walk from right to left, skip trusted proxies
|
|
217
|
+
// Walk from right to left, skip trusted proxies and malformed entries.
|
|
218
|
+
// Returning a malformed entry as the client IP would let an attacker
|
|
219
|
+
// who controls XFF poison the per-IP rate-limit key.
|
|
151
220
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
152
221
|
const entry = entries[i];
|
|
222
|
+
if (!validate_ip_strict(entry))
|
|
223
|
+
continue;
|
|
153
224
|
if (!is_trusted_ip(entry, proxies)) {
|
|
154
225
|
return entry;
|
|
155
226
|
}
|
|
156
227
|
}
|
|
157
|
-
//
|
|
158
|
-
|
|
228
|
+
// Every entry was trusted or malformed. Prefer the leftmost
|
|
229
|
+
// strictly-valid (trusted) entry — the misconfiguration warn in
|
|
230
|
+
// the middleware fires on it. If none, fall through to undefined
|
|
231
|
+
// and let the middleware fall back to the connection IP.
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
if (validate_ip_strict(entry))
|
|
234
|
+
return entry;
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
159
237
|
};
|
|
160
238
|
/**
|
|
161
239
|
* Create a Hono middleware that resolves the client IP from trusted proxies.
|