@fuzdev/fuz_app 0.140.0 → 0.142.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 +72 -22
- package/dist/actions/action_rpc.d.ts +5 -1
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +9 -1
- package/dist/actions/connection_closer.d.ts +44 -4
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +33 -3
- package/dist/actions/perform_action.d.ts +10 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +23 -9
- package/dist/actions/register_action_ws.d.ts +42 -6
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +87 -19
- package/dist/actions/register_ws_endpoint.d.ts +15 -6
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +10 -4
- package/dist/actions/socket.svelte.d.ts +19 -4
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +31 -4
- package/dist/actions/transports.d.ts +30 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +30 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +24 -10
- package/dist/actions/transports_ws_backend.d.ts +51 -5
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +62 -9
- package/dist/actions/ws_endpoint_spec.d.ts +11 -10
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +44 -22
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +12 -1
- package/dist/auth/account_route_schema.d.ts +1 -0
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +12 -0
- package/dist/auth/account_routes.d.ts +7 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +240 -187
- package/dist/auth/admin_action_specs.d.ts +6 -1
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +6 -1
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +14 -2
- package/dist/auth/api_token.d.ts +20 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +21 -1
- package/dist/auth/api_token_queries.d.ts +22 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +11 -6
- package/dist/auth/audit_log_routes.d.ts +10 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +20 -0
- package/dist/auth/audit_log_schema.d.ts +30 -5
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +30 -0
- package/dist/auth/bearer_auth.d.ts +4 -2
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +19 -8
- package/dist/auth/bootstrap_route_schema.d.ts +1 -0
- package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +85 -76
- package/dist/auth/cell_action_specs.d.ts +7 -3
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +9 -5
- package/dist/auth/cell_actions.d.ts +3 -3
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +67 -38
- package/dist/auth/cell_authorize.d.ts +69 -8
- package/dist/auth/cell_authorize.d.ts.map +1 -1
- package/dist/auth/cell_authorize.js +89 -9
- package/dist/auth/cell_field_action_specs.d.ts +5 -4
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +7 -7
- package/dist/auth/cell_field_actions.d.ts +7 -5
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +24 -22
- package/dist/auth/cell_grant_action_specs.d.ts +6 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_grant_action_specs.js +6 -0
- package/dist/auth/cell_grant_actions.d.ts +3 -1
- package/dist/auth/cell_grant_actions.d.ts.map +1 -1
- package/dist/auth/cell_grant_actions.js +16 -2
- package/dist/auth/cell_item_action_specs.d.ts +5 -4
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +7 -7
- package/dist/auth/cell_item_actions.d.ts +6 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +23 -22
- package/dist/auth/cell_relation_list_limit.d.ts +20 -0
- package/dist/auth/cell_relation_list_limit.d.ts.map +1 -0
- package/dist/auth/cell_relation_list_limit.js +27 -0
- package/dist/auth/cell_relation_visibility.d.ts +4 -3
- package/dist/auth/cell_relation_visibility.d.ts.map +1 -1
- package/dist/auth/cell_relation_visibility.js +6 -5
- package/dist/auth/cell_steward.d.ts +45 -0
- package/dist/auth/cell_steward.d.ts.map +1 -0
- package/dist/auth/cell_steward.js +53 -0
- package/dist/auth/cleanup.d.ts +9 -9
- package/dist/auth/cleanup.js +9 -9
- package/dist/auth/credential_deadline.d.ts +95 -0
- package/dist/auth/credential_deadline.d.ts.map +1 -0
- package/dist/auth/credential_deadline.js +139 -0
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +4 -1
- package/dist/auth/middleware.d.ts +90 -12
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +115 -16
- package/dist/auth/request_context.d.ts +8 -2
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +20 -4
- package/dist/auth/resolved_auth.d.ts +13 -2
- package/dist/auth/resolved_auth.d.ts.map +1 -1
- package/dist/auth/resolved_auth.js +5 -4
- package/dist/auth/role_grant_offer_action_specs.d.ts +5 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +6 -1
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +23 -16
- package/dist/auth/role_grant_offer_queries.d.ts +8 -6
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +16 -8
- package/dist/auth/session_cookie.d.ts +4 -0
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +3 -2
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +4 -1
- package/dist/auth/session_queries.d.ts +19 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +8 -2
- package/dist/auth/signup_route_schema.d.ts.map +1 -1
- package/dist/auth/signup_route_schema.js +1 -0
- package/dist/auth/signup_routes.d.ts +4 -3
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +157 -134
- package/dist/auth/token_scope.d.ts +7 -1
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +8 -2
- package/dist/db/CLAUDE.md +32 -19
- package/dist/db/cell_ddl.d.ts +1 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +1 -2
- package/dist/db/cell_field_queries.d.ts +20 -18
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +28 -22
- package/dist/db/cell_item_queries.d.ts +20 -14
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +28 -18
- package/dist/db/cell_moderation.d.ts +67 -0
- package/dist/db/cell_moderation.d.ts.map +1 -0
- package/dist/db/cell_moderation.js +66 -0
- package/dist/db/cell_queries.d.ts +44 -14
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +68 -45
- package/dist/db/cell_view_sql.d.ts +102 -0
- package/dist/db/cell_view_sql.d.ts.map +1 -0
- package/dist/db/cell_view_sql.js +112 -0
- package/dist/db/db_pg.d.ts +1 -1
- package/dist/db/db_pg.js +1 -1
- package/dist/hono_context.d.ts +23 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +19 -0
- package/dist/http/CLAUDE.md +41 -15
- package/dist/http/auth_shape.d.ts +5 -2
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +5 -2
- package/dist/http/ip_canonical.d.ts +30 -3
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +50 -3
- package/dist/http/middleware_spec.d.ts +16 -0
- package/dist/http/middleware_spec.d.ts.map +1 -1
- package/dist/http/origin.d.ts +113 -18
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +247 -127
- package/dist/http/route_spec.d.ts +21 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/schema_helpers.d.ts +22 -0
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +24 -0
- package/dist/http/surface.d.ts +6 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +12 -5
- package/dist/primitive_schemas.d.ts +10 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +11 -2
- package/dist/rate_limiter.d.ts +179 -12
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +352 -42
- package/dist/realtime/sse_auth_guard.d.ts +22 -6
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +31 -12
- package/dist/realtime/subscriber_registry.d.ts +10 -1
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +10 -1
- package/dist/server/app_server.d.ts +20 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +8 -3
- package/dist/server/env.d.ts +2 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +7 -17
- package/dist/server/serve_fact_route.d.ts +8 -4
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +16 -6
- package/dist/testing/CLAUDE.md +164 -25
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -6
- package/dist/testing/connection_closer_helpers.d.ts +6 -1
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +4 -0
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +37 -2
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +14 -65
- package/dist/testing/cross_backend/capabilities.d.ts +26 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +9 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts +38 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +401 -5
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +145 -2
- package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +6 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +20 -19
- package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
- package/dist/testing/cross_backend/credential_deadline.js +185 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +13 -2
- package/dist/testing/cross_backend/expected_schema.json +0 -1
- package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +7 -1
- package/dist/testing/cross_backend/login_security.d.ts +16 -0
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +154 -0
- package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
- package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
- package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +6 -26
- package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
- package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_conformance.js +136 -0
- package/dist/testing/cross_backend/origin_conformance.json +1679 -0
- package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_scope.js +140 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +24 -2
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +64 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +20 -1
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +23 -1
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +20 -7
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +25 -21
- package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
- package/dist/testing/cross_backend/transport_composition.js +364 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +21 -1
- package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_role_gated.js +142 -0
- package/dist/testing/entities.d.ts +23 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +37 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +3 -1
- package/dist/testing/middleware.d.ts +10 -0
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +31 -7
- package/dist/testing/schema_introspect.js +1 -1
- package/dist/testing/schema_parity.d.ts +4 -3
- package/dist/testing/schema_parity.d.ts.map +1 -1
- package/dist/testing/schema_parity.js +4 -3
- package/dist/testing/schema_ready_fixture.d.ts +2 -4
- package/dist/testing/schema_ready_fixture.d.ts.map +1 -1
- package/dist/testing/schema_ready_fixture.js +2 -4
- package/dist/testing/stubs.d.ts +16 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +20 -7
- package/dist/testing/surface_invariants.js +2 -2
- package/dist/ui/auth_state.svelte.d.ts +10 -0
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +22 -4
- package/package.json +1 -1
- package/dist/db/cell_history_ddl.d.ts +0 -38
- package/dist/db/cell_history_ddl.d.ts.map +0 -1
- package/dist/db/cell_history_ddl.js +0 -61
|
@@ -27,9 +27,15 @@
|
|
|
27
27
|
* canonicalization because the dotted form is the only form the
|
|
28
28
|
* strip can recognize symmetrically.
|
|
29
29
|
*
|
|
30
|
-
* Mirrors `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Mirrors the Rust spine's `fuz_http::normalize_ip`, which uses the same
|
|
31
|
+
* parse-then-canonicalize-then-strip ordering for the same
|
|
32
|
+
* rate-limit-key-poisoning surface.
|
|
33
|
+
*
|
|
34
|
+
* `rate_limit_ip_key` is the bucket key an IP-keyed rate limiter uses: an
|
|
35
|
+
* IPv6 address collapses to its `/64`, the smallest block a host is commonly
|
|
36
|
+
* assigned, so rotating through one's own subnet buys no fresh budget. Only
|
|
37
|
+
* the limiter call sites apply it — the client IP stays the full address
|
|
38
|
+
* everywhere else (audit rows, token `last_used_ip`, `REMOTE_ADDR`).
|
|
33
39
|
*
|
|
34
40
|
* @module
|
|
35
41
|
*/
|
|
@@ -97,4 +103,25 @@ export declare const canonicalize_ip: (ip: string) => string;
|
|
|
97
103
|
* @throws {RangeError} when `bits` is negative or exceeds 128 bits
|
|
98
104
|
*/
|
|
99
105
|
export declare const ipv6_bigint_to_canonical: (bits: bigint) => string;
|
|
106
|
+
/**
|
|
107
|
+
* The bucket key an IP-keyed rate limiter uses for client IP `ip`.
|
|
108
|
+
*
|
|
109
|
+
* An IPv6 address keys its `/64`: the low 64 bits masked off, rendered in
|
|
110
|
+
* RFC 5952 canonical form with a `/64` suffix (`2001:db8:1:2::/64`; the
|
|
111
|
+
* all-zero prefix is `::/64`). A zone suffix (`%eth0`) is dropped first. An
|
|
112
|
+
* IPv4 address — an IPv4-mapped IPv6 one included — keys its canonical form,
|
|
113
|
+
* with no suffix. Anything that doesn't parse (`'unknown'` among them) is
|
|
114
|
+
* returned unchanged. Twin of the Rust spine's `fuz_http::rate_limit_ip_key`.
|
|
115
|
+
*
|
|
116
|
+
* For limiter keys only: audit rows and every other record of the client
|
|
117
|
+
* keep the full address.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* rate_limit_ip_key('2001:db8:1:2::1') // → '2001:db8:1:2::/64'
|
|
121
|
+
* rate_limit_ip_key('2001:db8:1:2:8000::') // → '2001:db8:1:2::/64'
|
|
122
|
+
* rate_limit_ip_key('fe80::1%eth0') // → 'fe80::/64'
|
|
123
|
+
* rate_limit_ip_key('203.0.113.7') // → '203.0.113.7'
|
|
124
|
+
* rate_limit_ip_key('unknown') // → 'unknown'
|
|
125
|
+
*/
|
|
126
|
+
export declare const rate_limit_ip_key: (ip: string) => string;
|
|
100
127
|
//# sourceMappingURL=ip_canonical.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ip_canonical.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/ip_canonical.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ip_canonical.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/ip_canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAIH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,QAAqB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAmC5C,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,wBAAwB,GAAI,MAAM,MAAM,KAAG,MAgEvD,CAAC;AAKF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iBAAiB,GAAI,IAAI,MAAM,KAAG,MAc9C,CAAC"}
|
|
@@ -27,9 +27,15 @@
|
|
|
27
27
|
* canonicalization because the dotted form is the only form the
|
|
28
28
|
* strip can recognize symmetrically.
|
|
29
29
|
*
|
|
30
|
-
* Mirrors `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Mirrors the Rust spine's `fuz_http::normalize_ip`, which uses the same
|
|
31
|
+
* parse-then-canonicalize-then-strip ordering for the same
|
|
32
|
+
* rate-limit-key-poisoning surface.
|
|
33
|
+
*
|
|
34
|
+
* `rate_limit_ip_key` is the bucket key an IP-keyed rate limiter uses: an
|
|
35
|
+
* IPv6 address collapses to its `/64`, the smallest block a host is commonly
|
|
36
|
+
* assigned, so rotating through one's own subnet buys no fresh budget. Only
|
|
37
|
+
* the limiter call sites apply it — the client IP stays the full address
|
|
38
|
+
* everywhere else (audit rows, token `last_used_ip`, `REMOTE_ADDR`).
|
|
33
39
|
*
|
|
34
40
|
* @module
|
|
35
41
|
*/
|
|
@@ -193,3 +199,44 @@ export const ipv6_bigint_to_canonical = (bits) => {
|
|
|
193
199
|
.join(':');
|
|
194
200
|
return before + '::' + after;
|
|
195
201
|
};
|
|
202
|
+
/** The high 64 bits of an IPv6 address — its `/64` network prefix. */
|
|
203
|
+
const IPV6_PREFIX_64_MASK = ((1n << 64n) - 1n) << 64n;
|
|
204
|
+
/**
|
|
205
|
+
* The bucket key an IP-keyed rate limiter uses for client IP `ip`.
|
|
206
|
+
*
|
|
207
|
+
* An IPv6 address keys its `/64`: the low 64 bits masked off, rendered in
|
|
208
|
+
* RFC 5952 canonical form with a `/64` suffix (`2001:db8:1:2::/64`; the
|
|
209
|
+
* all-zero prefix is `::/64`). A zone suffix (`%eth0`) is dropped first. An
|
|
210
|
+
* IPv4 address — an IPv4-mapped IPv6 one included — keys its canonical form,
|
|
211
|
+
* with no suffix. Anything that doesn't parse (`'unknown'` among them) is
|
|
212
|
+
* returned unchanged. Twin of the Rust spine's `fuz_http::rate_limit_ip_key`.
|
|
213
|
+
*
|
|
214
|
+
* For limiter keys only: audit rows and every other record of the client
|
|
215
|
+
* keep the full address.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* rate_limit_ip_key('2001:db8:1:2::1') // → '2001:db8:1:2::/64'
|
|
219
|
+
* rate_limit_ip_key('2001:db8:1:2:8000::') // → '2001:db8:1:2::/64'
|
|
220
|
+
* rate_limit_ip_key('fe80::1%eth0') // → 'fe80::/64'
|
|
221
|
+
* rate_limit_ip_key('203.0.113.7') // → '203.0.113.7'
|
|
222
|
+
* rate_limit_ip_key('unknown') // → 'unknown'
|
|
223
|
+
*/
|
|
224
|
+
export const rate_limit_ip_key = (ip) => {
|
|
225
|
+
const zone_at = ip.indexOf('%');
|
|
226
|
+
const canonical = canonicalize_ip(zone_at === -1 ? ip : ip.slice(0, zone_at));
|
|
227
|
+
if (!IP_LITERAL_CHARS.test(canonical))
|
|
228
|
+
return ip;
|
|
229
|
+
const family = distinctRemoteAddr(canonical);
|
|
230
|
+
if (family === 'IPv4')
|
|
231
|
+
return canonical;
|
|
232
|
+
if (family !== 'IPv6')
|
|
233
|
+
return ip;
|
|
234
|
+
let bits;
|
|
235
|
+
try {
|
|
236
|
+
bits = convertIPv6ToBinary(canonical);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return ip;
|
|
240
|
+
}
|
|
241
|
+
return `${ipv6_bigint_to_canonical(bits & IPV6_PREFIX_64_MASK)}/64`;
|
|
242
|
+
};
|
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { MiddlewareHandler } from 'hono';
|
|
10
10
|
import type { RouteErrorSchemas } from './error_schemas.js';
|
|
11
|
+
/**
|
|
12
|
+
* Paths a middleware layer explicitly does not run on, and why — the named
|
|
13
|
+
* exception to a layer that covers every path by default (the origin check).
|
|
14
|
+
*/
|
|
15
|
+
export interface MiddlewareCarveOut {
|
|
16
|
+
/** Hono-style pattern (`/hooks/*`, `/health`), matched like `MiddlewareSpec.path`. */
|
|
17
|
+
readonly path: string;
|
|
18
|
+
/** Why these paths are safe without the layer — required, so a carve-out is an argument, not an omission. */
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
}
|
|
11
21
|
/** A named middleware layer. */
|
|
12
22
|
export interface MiddlewareSpec {
|
|
13
23
|
name: string;
|
|
@@ -15,5 +25,11 @@ export interface MiddlewareSpec {
|
|
|
15
25
|
handler: MiddlewareHandler;
|
|
16
26
|
/** Error response schemas this middleware can produce, keyed by HTTP status code. */
|
|
17
27
|
errors?: RouteErrorSchemas;
|
|
28
|
+
/**
|
|
29
|
+
* Paths under `path` the layer does not run on. The handler enforces them
|
|
30
|
+
* itself (Hono mounts by `path` alone); the field is what keeps the
|
|
31
|
+
* generated surface from claiming the layer's errors on those routes.
|
|
32
|
+
*/
|
|
33
|
+
carve_outs?: ReadonlyArray<MiddlewareCarveOut>;
|
|
18
34
|
}
|
|
19
35
|
//# sourceMappingURL=middleware_spec.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/middleware_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,gCAAgC;AAChC,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,iBAAiB,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"middleware_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/middleware_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAClC,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6GAA6G;IAC7G,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,gCAAgC;AAChC,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,iBAAiB,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CAC/C"}
|
package/dist/http/origin.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Request source verification middleware for API protection.
|
|
3
3
|
*
|
|
4
|
-
* Verifies requests
|
|
4
|
+
* Verifies requests carry an `Origin` the server allows, or none.
|
|
5
5
|
* CSRF protection is `SameSite: strict` on session cookies
|
|
6
6
|
* (see `auth/session_middleware.ts`) plus the CORS simple-request refusal
|
|
7
7
|
* (`http/simple_request.ts`) — `SameSite` alone admits same-site pages, which
|
|
@@ -10,9 +10,18 @@
|
|
|
10
10
|
* from making requests as the user browses the web — and is the only gate on
|
|
11
11
|
* the WebSocket upgrade, which is never preflighted.
|
|
12
12
|
*
|
|
13
|
+
* The allowlist grammar is shared with the Rust spine entry for entry:
|
|
14
|
+
* `parse_allowed_origins` accepts and refuses the same entries, naming each
|
|
15
|
+
* refusal with the same `AllowedOriginsErrorKind` string, and an accepted list
|
|
16
|
+
* matches the same origins. The shared conformance table
|
|
17
|
+
* (`testing/cross_backend/origin_conformance.json`) pins both.
|
|
18
|
+
*
|
|
13
19
|
* @module
|
|
14
20
|
*/
|
|
21
|
+
import type { Result } from '@fuzdev/fuz_util/result.ts';
|
|
15
22
|
import type { Context, Handler } from 'hono';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import type { MiddlewareCarveOut } from './middleware_spec.js';
|
|
16
25
|
/**
|
|
17
26
|
* True when the request looks like it originated from a browser context — it
|
|
18
27
|
* carries an `Origin` or `Referer` header. Browsers attach these
|
|
@@ -31,29 +40,106 @@ import type { Context, Handler } from 'hono';
|
|
|
31
40
|
*/
|
|
32
41
|
export declare const is_browser_context: (c: Context) => boolean;
|
|
33
42
|
/**
|
|
34
|
-
*
|
|
43
|
+
* Why one allowlist entry was refused. The wire names both spines emit — the
|
|
44
|
+
* Rust spine's `OriginPatternError::kind` returns the same strings.
|
|
45
|
+
*/
|
|
46
|
+
export declare const OriginPatternErrorKind: z.ZodEnum<{
|
|
47
|
+
empty_entry: "empty_entry";
|
|
48
|
+
not_printable_ascii: "not_printable_ascii";
|
|
49
|
+
missing_scheme: "missing_scheme";
|
|
50
|
+
unsupported_scheme: "unsupported_scheme";
|
|
51
|
+
has_path: "has_path";
|
|
52
|
+
has_userinfo: "has_userinfo";
|
|
53
|
+
missing_host: "missing_host";
|
|
54
|
+
invalid_host_char: "invalid_host_char";
|
|
55
|
+
unbracketed_ipv6: "unbracketed_ipv6";
|
|
56
|
+
partial_wildcard: "partial_wildcard";
|
|
57
|
+
non_canonical_ipv4: "non_canonical_ipv4";
|
|
58
|
+
unclosed_ipv6: "unclosed_ipv6";
|
|
59
|
+
invalid_ipv6: "invalid_ipv6";
|
|
60
|
+
invalid_after_ipv6: "invalid_after_ipv6";
|
|
61
|
+
invalid_port: "invalid_port";
|
|
62
|
+
default_port: "default_port";
|
|
63
|
+
}>;
|
|
64
|
+
export type OriginPatternErrorKind = z.infer<typeof OriginPatternErrorKind>;
|
|
65
|
+
/** Why an allowlist value was refused: `no_origins` (a blank value) or an entry's kind. */
|
|
66
|
+
export declare const AllowedOriginsErrorKind: z.ZodEnum<{
|
|
67
|
+
empty_entry: "empty_entry";
|
|
68
|
+
not_printable_ascii: "not_printable_ascii";
|
|
69
|
+
missing_scheme: "missing_scheme";
|
|
70
|
+
unsupported_scheme: "unsupported_scheme";
|
|
71
|
+
has_path: "has_path";
|
|
72
|
+
has_userinfo: "has_userinfo";
|
|
73
|
+
missing_host: "missing_host";
|
|
74
|
+
invalid_host_char: "invalid_host_char";
|
|
75
|
+
unbracketed_ipv6: "unbracketed_ipv6";
|
|
76
|
+
partial_wildcard: "partial_wildcard";
|
|
77
|
+
non_canonical_ipv4: "non_canonical_ipv4";
|
|
78
|
+
unclosed_ipv6: "unclosed_ipv6";
|
|
79
|
+
invalid_ipv6: "invalid_ipv6";
|
|
80
|
+
invalid_after_ipv6: "invalid_after_ipv6";
|
|
81
|
+
invalid_port: "invalid_port";
|
|
82
|
+
default_port: "default_port";
|
|
83
|
+
no_origins: "no_origins";
|
|
84
|
+
}>;
|
|
85
|
+
export type AllowedOriginsErrorKind = z.infer<typeof AllowedOriginsErrorKind>;
|
|
86
|
+
/** The operator-facing reason for each refusal kind. */
|
|
87
|
+
export declare const ALLOWED_ORIGINS_ERROR_MESSAGES: Readonly<Record<AllowedOriginsErrorKind, string>>;
|
|
88
|
+
/** One reason `parse_allowed_origins` refused a value. */
|
|
89
|
+
export interface AllowedOriginsError {
|
|
90
|
+
readonly kind: AllowedOriginsErrorKind;
|
|
91
|
+
/** The refused entry's 1-based position; absent for `no_origins`. */
|
|
92
|
+
readonly index?: number;
|
|
93
|
+
/** The refused entry as given, trimmed; absent for `no_origins`. */
|
|
94
|
+
readonly entry?: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Render one refusal for a boot error — the entry in double quotes with every
|
|
98
|
+
* character but visible ASCII and the space escaped as `\uXXXX` (and `"` and
|
|
99
|
+
* `\` backslash-escaped), so an env value can't put a control, bidi, or other
|
|
100
|
+
* non-printing character in a log, nor close the quotes it is shown in.
|
|
101
|
+
*
|
|
102
|
+
* @param error - one of `parse_allowed_origins`'s errors
|
|
103
|
+
* @returns the reason as one line
|
|
104
|
+
*/
|
|
105
|
+
export declare const format_allowed_origins_error: (error: AllowedOriginsError) => string;
|
|
106
|
+
/**
|
|
107
|
+
* Parses an allowlist value (conventionally the `FUZ_ALLOWED_ORIGINS` env var)
|
|
108
|
+
* into regex matchers for request source verification.
|
|
35
109
|
* Origin allowlisting for locally-running services — not the CSRF layer
|
|
36
110
|
* (that's `SameSite: strict` on session cookies plus `http/simple_request.ts`).
|
|
37
111
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* -
|
|
42
|
-
* - Wildcard
|
|
43
|
-
*
|
|
112
|
+
* Entries are comma-separated and trimmed of ASCII whitespace. Each is written
|
|
113
|
+
* the way a browser writes an `Origin` — `scheme://host[:port]` — where a whole
|
|
114
|
+
* host label or the port may be `*`:
|
|
115
|
+
* - Exact origins: `https://api.fuz.dev`, `http://localhost:3000`
|
|
116
|
+
* - Wildcard labels: `https://*.fuz.dev` (each `*` is exactly one label, in any
|
|
117
|
+
* position — `api.fuz.dev` matches, `fuz.dev` and `a.b.fuz.dev` don't)
|
|
118
|
+
* - Wildcard ports: `http://localhost:*` (any port or none)
|
|
119
|
+
* - IPv6 addresses: `http://[::1]:3000`, `https://[2001:db8::1]` (exact)
|
|
44
120
|
* - Combined: `https://*.fuz.dev:*`
|
|
45
121
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
122
|
+
* Refused, nothing normalized: anything but printable ASCII (punycode an
|
|
123
|
+
* international hostname), a scheme other than http or https (`null`,
|
|
124
|
+
* `file://`, `chrome-extension://`), a path, query, fragment or trailing
|
|
125
|
+
* slash, userinfo, a character no hostname holds, a partial wildcard, an IPv4
|
|
126
|
+
* address not written as four canonical decimal numbers (`127.1`), an IPv6
|
|
127
|
+
* address not in the form a browser sends, a port outside `1..=65535` or with
|
|
128
|
+
* leading zeros, the scheme's default port, and an empty entry. Every refused
|
|
129
|
+
* entry is reported, not just the first.
|
|
50
130
|
*
|
|
51
|
-
* @
|
|
131
|
+
* @param raw - the comma-separated allowlist value
|
|
132
|
+
* @returns the matchers, or every refusal — `no_origins` when the value is blank
|
|
52
133
|
*/
|
|
53
|
-
export declare const parse_allowed_origins: (
|
|
134
|
+
export declare const parse_allowed_origins: (raw: string) => Result<{
|
|
135
|
+
origins: Array<RegExp>;
|
|
136
|
+
}, {
|
|
137
|
+
errors: Array<AllowedOriginsError>;
|
|
138
|
+
}>;
|
|
54
139
|
/**
|
|
55
|
-
* Tests if
|
|
56
|
-
* Pattern matching is case-insensitive
|
|
140
|
+
* Tests if an `Origin` value matches any of the allowed patterns.
|
|
141
|
+
* Pattern matching is case-insensitive (web hosts and schemes are). A value
|
|
142
|
+
* that isn't visible ASCII matches nothing — a browser never sends one.
|
|
57
143
|
*/
|
|
58
144
|
export declare const should_allow_origin: (origin: string, allowed_patterns: ReadonlyArray<RegExp>) => boolean;
|
|
59
145
|
/**
|
|
@@ -63,6 +149,9 @@ export declare const should_allow_origin: (origin: string, allowed_patterns: Rea
|
|
|
63
149
|
* plus `http/simple_request.ts`):
|
|
64
150
|
* - Checks the `Origin` header (if present) against the allowlist
|
|
65
151
|
* - Allows requests without an `Origin` header (direct access, curl, etc.)
|
|
152
|
+
* - Refuses an `Origin` that isn't visible ASCII, and a duplicated one — the
|
|
153
|
+
* runtimes join a repeated header into one value (`a, b`), which carries a
|
|
154
|
+
* space and so matches nothing
|
|
66
155
|
*
|
|
67
156
|
* Origin-only by design — Fetch spec mandates `Origin` on every unsafe
|
|
68
157
|
* method (POST / PUT / DELETE / PATCH) regardless of `Referrer-Policy`,
|
|
@@ -71,9 +160,15 @@ export declare const should_allow_origin: (origin: string, allowed_patterns: Rea
|
|
|
71
160
|
* attached session cookies, so CSRF isn't the relevant threat there —
|
|
72
161
|
* auth (bearer / daemon token) is the actual control. A Referer fallback
|
|
73
162
|
* would only widen the accepted-shape envelope without closing a real
|
|
74
|
-
* CSRF hole
|
|
163
|
+
* CSRF hole. The Rust spine's `origin_layer` is the twin.
|
|
164
|
+
*
|
|
165
|
+
* `create_app_server` mounts it on every path (`ORIGIN_MIDDLEWARE_PATH`); a
|
|
166
|
+
* request whose path a `carve_outs` entry matches passes unchecked — the named,
|
|
167
|
+
* explicit exception, each with its reason, as the Rust stack's
|
|
168
|
+
* `origin_carve_out` is.
|
|
75
169
|
*
|
|
76
170
|
* @param allowed_patterns - compiled regex patterns from `parse_allowed_origins`
|
|
171
|
+
* @param carve_outs - paths the check skips, matched like a middleware path (`middleware_applies`)
|
|
77
172
|
*/
|
|
78
|
-
export declare const verify_request_source: (allowed_patterns: ReadonlyArray<RegExp>) => Handler;
|
|
173
|
+
export declare const verify_request_source: (allowed_patterns: ReadonlyArray<RegExp>, carve_outs?: ReadonlyArray<MiddlewareCarveOut>) => Handler;
|
|
79
174
|
//# sourceMappingURL=origin.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"origin.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/origin.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"origin.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,GAAI,GAAG,OAAO,KAAG,OAC8B,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;EAiBjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,2FAA2F;AAC3F,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;EAA4D,CAAC;AACjG,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAE9E,wDAAwD;AACxD,eAAO,MAAM,8BAA8B,EAAE,QAAQ,CAAC,MAAM,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAwB5F,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAKD;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GAAI,OAAO,mBAAmB,KAAG,MAOzE,CAAC;AAwBF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,qBAAqB,GACjC,KAAK,MAAM,KACT,MAAM,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE,EAAE;IAAE,MAAM,EAAE,KAAK,CAAC,mBAAmB,CAAC,CAAA;CAAE,CAgB3E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,QAAQ,MAAM,EACd,kBAAkB,aAAa,CAAC,MAAM,CAAC,KACrC,OAAqF,CAAC;AAEzF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,qBAAqB,GAEhC,kBAAkB,aAAa,CAAC,MAAM,CAAC,EACvC,aAAY,aAAa,CAAC,kBAAkB,CAAM,KAChD,OAaF,CAAC"}
|