@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
package/dist/hono_context.js
CHANGED
|
@@ -11,11 +11,47 @@
|
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import { CREDENTIAL_TYPE_API_TOKEN, CREDENTIAL_TYPE_DAEMON_TOKEN, CREDENTIAL_TYPE_SESSION, } from './auth/credential_type_schema.js';
|
|
15
|
+
/**
|
|
16
|
+
* The credential types that can authenticate a request — the closed set
|
|
17
|
+
* of fuz_app builtins. The open registry on top
|
|
18
|
+
* (`create_credential_type_schema(consumer_types)`) is consulted at
|
|
19
|
+
* registry time by `create_role_schema` for `RoleSpec.required_credential_types`
|
|
20
|
+
* validation; the wire-validated `CredentialType` enum here stays
|
|
21
|
+
* narrow because middleware only ever sets one of the three builtins.
|
|
22
|
+
*/
|
|
23
|
+
export const CREDENTIAL_TYPES = [
|
|
24
|
+
CREDENTIAL_TYPE_SESSION,
|
|
25
|
+
CREDENTIAL_TYPE_API_TOKEN,
|
|
26
|
+
CREDENTIAL_TYPE_DAEMON_TOKEN,
|
|
27
|
+
];
|
|
16
28
|
/** Credential type — how a request was authenticated. */
|
|
17
29
|
export const CredentialType = z.enum(CREDENTIAL_TYPES);
|
|
18
30
|
/** Hono context variable name for the credential type. */
|
|
19
31
|
export const CREDENTIAL_TYPE_KEY = 'credential_type';
|
|
20
32
|
/** Hono context variable name for the authenticated API token id. */
|
|
21
33
|
export const AUTH_API_TOKEN_ID_KEY = 'auth_api_token_id';
|
|
34
|
+
/**
|
|
35
|
+
* Hono context variable name for the authenticated account id.
|
|
36
|
+
*
|
|
37
|
+
* Set by the auth middleware (session, bearer, or daemon token) on a valid
|
|
38
|
+
* credential. `null` for unauthenticated requests. The route-spec wrapper /
|
|
39
|
+
* RPC dispatcher's authorization phase reads this when resolving the acting
|
|
40
|
+
* actor; account-grain auth guards (`require_auth`) and account-grain handlers
|
|
41
|
+
* read it directly.
|
|
42
|
+
*/
|
|
43
|
+
export const ACCOUNT_ID_KEY = 'auth_account_id';
|
|
44
|
+
/**
|
|
45
|
+
* Hono context variable name for the test-harness pre-baked context flag.
|
|
46
|
+
*
|
|
47
|
+
* Test harnesses (`create_test_app_from_specs`, `create_fake_hono_context`,
|
|
48
|
+
* the WS round-trip `connect()` helper, plus per-test middleware that
|
|
49
|
+
* pre-populates `REQUEST_CONTEXT_KEY`) set this to `true` so
|
|
50
|
+
* `apply_authorization_phase` skips its DB-backed actor resolution and
|
|
51
|
+
* trusts the supplied `RequestContext`. Production middleware never sets
|
|
52
|
+
* this key — only test code does. The flag is the explicit escape hatch
|
|
53
|
+
* that replaced the implicit "is `REQUEST_CONTEXT_KEY` already set?" probe,
|
|
54
|
+
* so that future production code consulting `REQUEST_CONTEXT_KEY` cannot
|
|
55
|
+
* silently bypass the live build.
|
|
56
|
+
*/
|
|
57
|
+
export const TEST_CONTEXT_PRESET_KEY = 'test_context_preset';
|
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
|
|
|
@@ -91,27 +104,74 @@ wrapper). See `../auth/signup_routes.ts`.
|
|
|
91
104
|
|
|
92
105
|
`apply_route_specs` assembles the following middleware chain per spec:
|
|
93
106
|
|
|
94
|
-
1. **
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
(
|
|
107
|
+
1. **Params validation** — `spec.params` → `validated_params` context
|
|
108
|
+
var; mismatch returns 400 `ERROR_INVALID_ROUTE_PARAMS` with Zod
|
|
109
|
+
`issues`
|
|
110
|
+
2. **Query validation** — `spec.query` → `validated_query`; mismatch
|
|
111
|
+
returns 400 `ERROR_INVALID_QUERY_PARAMS`
|
|
112
|
+
3. **Pre-validation auth guards** — `require_auth` (401
|
|
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'`,
|
|
125
|
+
resolves the acting actor against `c.var.account_id` (set by the
|
|
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
|
|
131
|
+
`ERROR_ACTOR_REQUIRED` (with `available[]`) or
|
|
132
|
+
`ERROR_ACTOR_NOT_ON_ACCOUNT` (or 500 `ERROR_NO_ACTORS_ON_ACCOUNT`
|
|
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)`.
|
|
144
|
+
7. **Handler** — wrapped in transaction when `use_transaction` (see
|
|
145
|
+
above), receives `RouteContext`
|
|
146
|
+
8. **DEV-only output + error validation** — wraps the handler (see below)
|
|
147
|
+
9. **Error catch** — catches `ThrownJsonrpcError` → maps to HTTP status +
|
|
148
|
+
the flat REST `ApiError` body (`{error: <reason>, message?, ...rest_data}`);
|
|
149
|
+
catches generic `Error` → 500 `{error: 'internal_error', message?}`
|
|
150
|
+
(message only included in DEV). The reason string comes from
|
|
151
|
+
`err.data.reason` when set (consumer-supplied canonical reason
|
|
152
|
+
override) or from `jsonrpc_error_code_to_name(err.code)` (e.g.
|
|
153
|
+
`-32003 → 'not_found'`). The flat shape matches what middleware
|
|
154
|
+
and direct handlers emit (`c.json({error: ERROR_FOO}, status)`,
|
|
155
|
+
`c.json(failure.body, status)` from the dispatcher's authorization
|
|
156
|
+
phase) — REST callers see one envelope across every emit site, while
|
|
157
|
+
the JSON-RPC dispatcher keeps its own `{jsonrpc, id, error: {code,
|
|
158
|
+
message, data}}` envelope on the RPC mount
|
|
159
|
+
|
|
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.
|
|
109
167
|
|
|
110
168
|
Duplicate `method path` pairs throw at registration.
|
|
111
169
|
|
|
112
|
-
Validated values are accessed via `get_route_input
|
|
113
|
-
`get_route_params
|
|
114
|
-
|
|
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.
|
|
115
175
|
|
|
116
176
|
### DEV-only output + error validation
|
|
117
177
|
|
|
@@ -147,16 +207,17 @@ Output Validation.
|
|
|
147
207
|
|
|
148
208
|
- `ERROR_*` snake*case string constants — single source of truth; use
|
|
149
209
|
`.literal(ERROR*\*)` in Zod schemas and inline checks in handlers
|
|
150
|
-
- `ApiError`, `ValidationError`, `PermissionError`,
|
|
151
|
-
`RateLimitError`, `PayloadTooLargeError`,
|
|
152
|
-
shapes
|
|
210
|
+
- `ApiError`, `ValidationError`, `PermissionError`,
|
|
211
|
+
`CredentialTypeRequiredError`, `RateLimitError`, `PayloadTooLargeError`,
|
|
212
|
+
`ForeignKeyError` — standard shapes
|
|
153
213
|
- `RouteErrorSchemas = Partial<Record<number, z.ZodType>>`
|
|
154
214
|
- `RateLimitKey = 'ip' | 'account' | 'both'`
|
|
155
215
|
|
|
156
216
|
All standard shapes use `z.looseObject` — intentional because multiple
|
|
157
217
|
producers (middleware + handler) can emit different extra fields at the
|
|
158
218
|
same status code. The `error` string literal is the contract; extra keys
|
|
159
|
-
(`
|
|
219
|
+
(`required_roles`, `required_credential_types`, `retry_after`, `detail`)
|
|
220
|
+
are diagnostic.
|
|
160
221
|
|
|
161
222
|
Pair every schema with the `z.infer` type export (`export type ApiError = z.infer<typeof ApiError>`).
|
|
162
223
|
|
|
@@ -165,37 +226,71 @@ Pair every schema with the `z.infer` type export (`export type ApiError = z.infe
|
|
|
165
226
|
`merge_error_schemas(spec, middleware_errors?)` (in `schema_helpers.ts`)
|
|
166
227
|
merges three layers, later overrides earlier at the same status code:
|
|
167
228
|
|
|
168
|
-
1. **Derived** — from `derive_error_schemas(auth, has_input
|
|
229
|
+
1. **Derived** — from `derive_error_schemas({auth, has_input?, has_params?, has_query?, rate_limit?})`:
|
|
169
230
|
- `has_input || has_params || has_query` → 400 `ValidationError`
|
|
170
|
-
- `auth.
|
|
171
|
-
- `auth.
|
|
172
|
-
- `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])`)
|
|
173
238
|
- `rate_limit` → 429 `RateLimitError`
|
|
239
|
+
- `auth.actor !== 'none'` → widens 400 to a union with `ActorRequiredError` /
|
|
240
|
+
`ActorNotOnAccountError` and adds 500 union of `NoActorsOnAccountError`
|
|
241
|
+
/ `AccountVanishedError`. Mirrors what the dispatcher's authorization
|
|
242
|
+
phase actually emits on routes whose input declares `acting?: ActingActor`
|
|
243
|
+
(per registry-time invariant 2) — so DEV-mode error-schema validation in
|
|
244
|
+
`wrap_output_validation` doesn't reject the auth phase's body.
|
|
174
245
|
2. **Middleware** — from `MiddlewareSpec.errors` that apply to the route's
|
|
175
246
|
path (via `middleware_applies`)
|
|
176
247
|
3. **Explicit** — `RouteSpec.errors` — always wins
|
|
177
248
|
|
|
178
249
|
Routes typically only need `errors` for handler-specific codes (404, 409, 422).
|
|
179
250
|
|
|
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.
|
|
275
|
+
|
|
180
276
|
### `ERROR_*` constants by category
|
|
181
277
|
|
|
182
278
|
- **Validation**: `ERROR_INVALID_REQUEST_BODY`, `ERROR_INVALID_JSON_BODY`,
|
|
183
279
|
`ERROR_INVALID_ROUTE_PARAMS`, `ERROR_INVALID_QUERY_PARAMS`
|
|
184
280
|
- **Auth**: `ERROR_AUTHENTICATION_REQUIRED`, `ERROR_INSUFFICIENT_PERMISSIONS`,
|
|
185
|
-
`
|
|
186
|
-
`ERROR_PAYLOAD_TOO_LARGE`
|
|
281
|
+
`ERROR_CREDENTIAL_TYPE_REQUIRED`, `ERROR_RATE_LIMIT_EXCEEDED`,
|
|
282
|
+
`ERROR_INVALID_CREDENTIALS`, `ERROR_PAYLOAD_TOO_LARGE`
|
|
187
283
|
- **Origin + bearer**: `ERROR_FORBIDDEN_ORIGIN`, `ERROR_FORBIDDEN_REFERER`,
|
|
188
284
|
`ERROR_BEARER_REJECTED_BROWSER`, `ERROR_INVALID_TOKEN`, `ERROR_ACCOUNT_NOT_FOUND`
|
|
189
|
-
- **Keeper/daemon**: `
|
|
190
|
-
`
|
|
191
|
-
`ERROR_KEEPER_ACCOUNT_NOT_FOUND`
|
|
285
|
+
- **Keeper/daemon**: `ERROR_INVALID_DAEMON_TOKEN`,
|
|
286
|
+
`ERROR_KEEPER_ACCOUNT_NOT_CONFIGURED`, `ERROR_KEEPER_ACCOUNT_NOT_FOUND`
|
|
192
287
|
- **Bootstrap**: `ERROR_ALREADY_BOOTSTRAPPED`, `ERROR_TOKEN_FILE_MISSING`,
|
|
193
288
|
`ERROR_BOOTSTRAP_NOT_CONFIGURED`
|
|
194
289
|
- **Signup/invites**: `ERROR_NO_MATCHING_INVITE`, `ERROR_SIGNUP_CONFLICT`,
|
|
195
290
|
`ERROR_INVITE_NOT_FOUND`, `ERROR_INVITE_MISSING_IDENTIFIER`,
|
|
196
291
|
`ERROR_INVITE_DUPLICATE`, `ERROR_INVITE_ACCOUNT_EXISTS_USERNAME`,
|
|
197
292
|
`ERROR_INVITE_ACCOUNT_EXISTS_EMAIL`
|
|
198
|
-
- **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `
|
|
293
|
+
- **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_ROLE_GRANT_NOT_FOUND`,
|
|
199
294
|
`ERROR_INVALID_EVENT_TYPE`
|
|
200
295
|
- **DB browser**: `ERROR_FOREIGN_KEY_VIOLATION`, `ERROR_TABLE_NOT_FOUND`,
|
|
201
296
|
`ERROR_TABLE_NO_PRIMARY_KEY`, `ERROR_ROW_NOT_FOUND`
|
|
@@ -225,7 +320,7 @@ Key helpers:
|
|
|
225
320
|
`'*'`, exact, `'/api/*'` prefix (handles `prefix.slice(0, -1)` so
|
|
226
321
|
`/api/*` also matches the bare `/api`)
|
|
227
322
|
- `merge_error_schemas(spec, middleware_errors?)` — three-layer merge
|
|
228
|
-
described above
|
|
323
|
+
described above.
|
|
229
324
|
|
|
230
325
|
## Surface Generation
|
|
231
326
|
|
|
@@ -263,8 +358,7 @@ Key helpers:
|
|
|
263
358
|
— `description`, `sensitivity`, and probes `safeParse(undefined)` to
|
|
264
359
|
detect `optional` + `has_default`
|
|
265
360
|
- `events_to_surface(event_specs)` — SSE events surface as `{method, description, channel, params_schema}`
|
|
266
|
-
- RPC methods surface
|
|
267
|
-
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.
|
|
268
362
|
|
|
269
363
|
`create_app_surface_spec(options)` = `generate_app_surface(options)` plus
|
|
270
364
|
the source specs, for tests that need to iterate over raw specs.
|
|
@@ -278,11 +372,21 @@ No side effects, no state — filters and groupings over `AppSurface`:
|
|
|
278
372
|
- `filter_routes_by_prefix(prefix)` / `filter_routes_with_input` /
|
|
279
373
|
`filter_routes_with_params` / `filter_routes_with_query` /
|
|
280
374
|
`filter_mutation_routes` / `filter_rate_limited_routes`
|
|
281
|
-
- `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.
|
|
282
376
|
- `format_route_key(route)` → `'METHOD /path'`
|
|
283
377
|
- `surface_auth_summary(surface)` — counts per auth type, roles broken
|
|
284
378
|
out by name
|
|
285
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
|
+
|
|
286
390
|
Consumer code (tests, attack-surface helpers, `SurfaceExplorer.svelte`)
|
|
287
391
|
should reach for these rather than inlining `.filter` chains.
|
|
288
392
|
|
|
@@ -320,11 +424,27 @@ Must run **before** auth and rate-limiting middleware. See the root
|
|
|
320
424
|
- `parse_proxy_entry(entry)` — accepts `'127.0.0.1'`, `'::1'`,
|
|
321
425
|
`'10.0.0.0/8'`, `'fe80::/10'`. Throws on invalid IPs, NaN/negative/
|
|
322
426
|
over-range prefix, non-network-aligned CIDRs, or bad input
|
|
323
|
-
-
|
|
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
|
|
324
439
|
mismatched address families for CIDR matches
|
|
325
440
|
- `resolve_client_ip(forwarded_for, proxies)` — walks **right-to-left**,
|
|
326
|
-
skipping trusted entries
|
|
327
|
-
|
|
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
|
|
328
448
|
- `create_proxy_middleware(options)` + `create_proxy_middleware_spec(options)` —
|
|
329
449
|
three-branch logic:
|
|
330
450
|
1. No XFF → use connection IP directly
|
|
@@ -335,10 +455,11 @@ Must run **before** auth and rate-limiting middleware. See the root
|
|
|
335
455
|
- `get_client_ip(c)` — returns `'unknown'` when the proxy middleware
|
|
336
456
|
hasn't run
|
|
337
457
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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.
|
|
342
463
|
|
|
343
464
|
### Origin/Referer allowlist — `origin.ts`
|
|
344
465
|
|
|
@@ -490,31 +611,87 @@ Converters:
|
|
|
490
611
|
|
|
491
612
|
## Pending Effects
|
|
492
613
|
|
|
493
|
-
|
|
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.
|
|
614
|
+
Two queues, one timing contract each:
|
|
497
615
|
|
|
498
616
|
```typescript
|
|
499
|
-
interface
|
|
617
|
+
interface EmitAfterCommitContext {
|
|
500
618
|
log: Logger;
|
|
501
|
-
|
|
619
|
+
post_commit_effects: Array<() => void | Promise<void>>;
|
|
502
620
|
}
|
|
503
621
|
|
|
504
|
-
|
|
622
|
+
// `RouteContext` and `ActionContext` carry both:
|
|
623
|
+
// pending_effects: Array<Promise<void>>
|
|
624
|
+
// post_commit_effects: Array<() => void | Promise<void>>
|
|
505
625
|
```
|
|
506
626
|
|
|
507
|
-
|
|
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
|
+
```
|
|
508
665
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
-
|
|
516
|
-
|
|
517
|
-
|
|
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/`.
|
|
518
695
|
|
|
519
696
|
WS sends are **not** wrapped by `create_validated_broadcaster` (that only
|
|
520
697
|
guards SSE `broadcast(channel, data)`). Zod input schemas on
|
|
@@ -555,7 +732,7 @@ a generic table browser; the factory is domain-agnostic.
|
|
|
555
732
|
or table missing (`ERROR_TABLE_NOT_FOUND`), 409 on FK violation (pg
|
|
556
733
|
error code `23503`)
|
|
557
734
|
|
|
558
|
-
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
|
|
559
736
|
`VALID_SQL_IDENTIFIER` regex, and every table name gets
|
|
560
737
|
`assert_valid_sql_identifier()` before string-interpolating into SQL —
|
|
561
738
|
the identifier validation is the only reason the interpolation is safe.
|