@fuzdev/fuz_app 0.63.0 → 0.65.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.
Files changed (181) hide show
  1. package/dist/actions/CLAUDE.md +525 -827
  2. package/dist/actions/broadcast_api.d.ts +1 -1
  3. package/dist/actions/broadcast_api.js +1 -1
  4. package/dist/actions/cancel.d.ts +2 -2
  5. package/dist/actions/cancel.js +3 -3
  6. package/dist/actions/connection_closer.d.ts +65 -0
  7. package/dist/actions/connection_closer.d.ts.map +1 -0
  8. package/dist/actions/connection_closer.js +38 -0
  9. package/dist/actions/register_action_ws.d.ts +2 -2
  10. package/dist/actions/register_action_ws.d.ts.map +1 -1
  11. package/dist/actions/register_action_ws.js +23 -2
  12. package/dist/actions/register_ws_endpoint.d.ts +12 -10
  13. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  14. package/dist/actions/register_ws_endpoint.js +5 -5
  15. package/dist/actions/transports_ws_auth_guard.d.ts +25 -10
  16. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  17. package/dist/actions/transports_ws_auth_guard.js +24 -9
  18. package/dist/actions/ws_endpoint_spec.d.ts +119 -0
  19. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
  20. package/dist/actions/ws_endpoint_spec.js +13 -0
  21. package/dist/auth/CLAUDE.md +592 -1808
  22. package/dist/auth/account_action_specs.d.ts +1 -1
  23. package/dist/auth/account_actions.d.ts +13 -0
  24. package/dist/auth/account_actions.d.ts.map +1 -1
  25. package/dist/auth/account_actions.js +31 -1
  26. package/dist/auth/account_routes.d.ts +12 -2
  27. package/dist/auth/account_routes.d.ts.map +1 -1
  28. package/dist/auth/account_routes.js +55 -8
  29. package/dist/auth/account_schema.d.ts +4 -4
  30. package/dist/auth/account_schema.d.ts.map +1 -1
  31. package/dist/auth/admin_action_specs.d.ts +8 -8
  32. package/dist/auth/admin_actions.d.ts +11 -0
  33. package/dist/auth/admin_actions.d.ts.map +1 -1
  34. package/dist/auth/admin_actions.js +25 -0
  35. package/dist/auth/api_token_queries.js +1 -1
  36. package/dist/auth/audit_emitter.d.ts +56 -12
  37. package/dist/auth/audit_emitter.d.ts.map +1 -1
  38. package/dist/auth/audit_emitter.js +38 -12
  39. package/dist/auth/audit_log_ddl.d.ts +1 -1
  40. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  41. package/dist/auth/audit_log_ddl.js +1 -1
  42. package/dist/auth/audit_log_schema.d.ts +5 -3
  43. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  44. package/dist/auth/audit_log_schema.js +5 -3
  45. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  46. package/dist/auth/bootstrap_account.js +1 -5
  47. package/dist/auth/bootstrap_routes.d.ts +8 -2
  48. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  49. package/dist/auth/bootstrap_routes.js +15 -11
  50. package/dist/auth/invite_schema.d.ts +2 -2
  51. package/dist/auth/keyring.d.ts +6 -6
  52. package/dist/auth/keyring.js +8 -8
  53. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  54. package/dist/auth/role_grant_offer_actions.js +4 -2
  55. package/dist/auth/signup_routes.d.ts +1 -1
  56. package/dist/auth/standard_rpc_actions.d.ts +1 -0
  57. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  58. package/dist/auth/standard_rpc_actions.js +1 -0
  59. package/dist/db/create_db.d.ts.map +1 -1
  60. package/dist/db/create_db.js +13 -0
  61. package/dist/dev/setup.d.ts +2 -2
  62. package/dist/dev/setup.js +3 -3
  63. package/dist/http/CLAUDE.md +225 -483
  64. package/dist/http/error_schemas.d.ts +0 -4
  65. package/dist/http/error_schemas.d.ts.map +1 -1
  66. package/dist/http/error_schemas.js +0 -4
  67. package/dist/http/ip_canonical.d.ts +100 -0
  68. package/dist/http/ip_canonical.d.ts.map +1 -0
  69. package/dist/http/ip_canonical.js +195 -0
  70. package/dist/http/origin.d.ts +14 -6
  71. package/dist/http/origin.d.ts.map +1 -1
  72. package/dist/http/origin.js +14 -32
  73. package/dist/http/pending_effects.d.ts +1 -1
  74. package/dist/http/pending_effects.js +1 -1
  75. package/dist/http/proxy.d.ts +13 -5
  76. package/dist/http/proxy.d.ts.map +1 -1
  77. package/dist/http/proxy.js +15 -23
  78. package/dist/http/surface.d.ts +50 -0
  79. package/dist/http/surface.d.ts.map +1 -1
  80. package/dist/http/surface.js +27 -1
  81. package/dist/primitive_schemas.d.ts +20 -4
  82. package/dist/primitive_schemas.d.ts.map +1 -1
  83. package/dist/primitive_schemas.js +25 -4
  84. package/dist/realtime/sse_auth_guard.d.ts +16 -4
  85. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  86. package/dist/realtime/sse_auth_guard.js +15 -3
  87. package/dist/runtime/mock.js +1 -1
  88. package/dist/server/app_backend.d.ts +66 -19
  89. package/dist/server/app_backend.d.ts.map +1 -1
  90. package/dist/server/app_backend.js +57 -34
  91. package/dist/server/app_server.d.ts +101 -10
  92. package/dist/server/app_server.d.ts.map +1 -1
  93. package/dist/server/app_server.js +105 -6
  94. package/dist/server/env.d.ts +7 -7
  95. package/dist/server/env.d.ts.map +1 -1
  96. package/dist/server/env.js +14 -14
  97. package/dist/server/startup.d.ts.map +1 -1
  98. package/dist/server/startup.js +12 -0
  99. package/dist/server/static.d.ts +4 -4
  100. package/dist/server/static.js +7 -7
  101. package/dist/testing/CLAUDE.md +269 -59
  102. package/dist/testing/admin_integration.d.ts +18 -23
  103. package/dist/testing/admin_integration.d.ts.map +1 -1
  104. package/dist/testing/admin_integration.js +159 -202
  105. package/dist/testing/adversarial_headers.d.ts +6 -0
  106. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  107. package/dist/testing/adversarial_headers.js +13 -5
  108. package/dist/testing/app_server.d.ts +148 -60
  109. package/dist/testing/app_server.d.ts.map +1 -1
  110. package/dist/testing/app_server.js +143 -54
  111. package/dist/testing/attack_surface.d.ts +8 -7
  112. package/dist/testing/attack_surface.d.ts.map +1 -1
  113. package/dist/testing/attack_surface.js +12 -8
  114. package/dist/testing/audit_completeness.d.ts +23 -22
  115. package/dist/testing/audit_completeness.d.ts.map +1 -1
  116. package/dist/testing/audit_completeness.js +199 -158
  117. package/dist/testing/audit_drift_guard.d.ts +116 -0
  118. package/dist/testing/audit_drift_guard.d.ts.map +1 -0
  119. package/dist/testing/audit_drift_guard.js +134 -0
  120. package/dist/testing/bootstrap_success.d.ts +28 -0
  121. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  122. package/dist/testing/bootstrap_success.js +144 -0
  123. package/dist/testing/connection_closer_helpers.d.ts +44 -0
  124. package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
  125. package/dist/testing/connection_closer_helpers.js +48 -0
  126. package/dist/testing/cross_backend/capabilities.d.ts +64 -0
  127. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/capabilities.js +47 -0
  129. package/dist/testing/cross_backend/setup.d.ts +215 -0
  130. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  131. package/dist/testing/cross_backend/setup.js +101 -0
  132. package/dist/testing/data_exposure.d.ts +14 -15
  133. package/dist/testing/data_exposure.d.ts.map +1 -1
  134. package/dist/testing/data_exposure.js +127 -146
  135. package/dist/testing/db_entities.d.ts +11 -1
  136. package/dist/testing/db_entities.d.ts.map +1 -1
  137. package/dist/testing/db_entities.js +13 -1
  138. package/dist/testing/integration.d.ts +35 -21
  139. package/dist/testing/integration.d.ts.map +1 -1
  140. package/dist/testing/integration.js +231 -293
  141. package/dist/testing/integration_helpers.d.ts +16 -6
  142. package/dist/testing/integration_helpers.d.ts.map +1 -1
  143. package/dist/testing/integration_helpers.js +7 -7
  144. package/dist/testing/mock_fs.d.ts.map +1 -1
  145. package/dist/testing/mock_fs.js +0 -2
  146. package/dist/testing/rate_limiting.d.ts.map +1 -1
  147. package/dist/testing/rate_limiting.js +13 -4
  148. package/dist/testing/role_grant_helpers.d.ts +31 -0
  149. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  150. package/dist/testing/role_grant_helpers.js +46 -0
  151. package/dist/testing/round_trip.d.ts +21 -16
  152. package/dist/testing/round_trip.d.ts.map +1 -1
  153. package/dist/testing/round_trip.js +65 -86
  154. package/dist/testing/rpc_helpers.d.ts +2 -1
  155. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  156. package/dist/testing/rpc_round_trip.d.ts +24 -21
  157. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  158. package/dist/testing/rpc_round_trip.js +91 -106
  159. package/dist/testing/schema_introspect.d.ts +106 -0
  160. package/dist/testing/schema_introspect.d.ts.map +1 -0
  161. package/dist/testing/schema_introspect.js +123 -0
  162. package/dist/testing/schema_parity.d.ts +144 -0
  163. package/dist/testing/schema_parity.d.ts.map +1 -0
  164. package/dist/testing/schema_parity.js +233 -0
  165. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  166. package/dist/testing/sse_round_trip.js +12 -6
  167. package/dist/testing/standard.d.ts +57 -25
  168. package/dist/testing/standard.d.ts.map +1 -1
  169. package/dist/testing/standard.js +62 -5
  170. package/dist/testing/stubs.d.ts +22 -3
  171. package/dist/testing/stubs.d.ts.map +1 -1
  172. package/dist/testing/stubs.js +28 -21
  173. package/dist/testing/surface_invariants.d.ts +66 -1
  174. package/dist/testing/surface_invariants.d.ts.map +1 -1
  175. package/dist/testing/surface_invariants.js +103 -1
  176. package/dist/testing/transports/surface_source.d.ts +51 -0
  177. package/dist/testing/transports/surface_source.d.ts.map +1 -0
  178. package/dist/testing/transports/surface_source.js +19 -0
  179. package/dist/ui/SurfaceExplorer.svelte +161 -2
  180. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  181. package/package.json +4 -4
@@ -43,8 +43,6 @@ export declare const ERROR_INVALID_CREDENTIALS: "invalid_credentials";
43
43
  export declare const ERROR_PAYLOAD_TOO_LARGE: "payload_too_large";
44
44
  /** Request origin not in allowlist. */
45
45
  export declare const ERROR_FORBIDDEN_ORIGIN: "forbidden_origin";
46
- /** Request referer not in allowlist. */
47
- export declare const ERROR_FORBIDDEN_REFERER: "forbidden_referer";
48
46
  /** Bearer token sent with Origin/Referer header (browser context). */
49
47
  export declare const ERROR_BEARER_REJECTED_BROWSER: "bearer_token_rejected_in_browser_context";
50
48
  /** Bearer token failed validation (missing, malformed, or revoked). */
@@ -94,8 +92,6 @@ export declare const ERROR_KEEPER_ACCOUNT_NOT_FOUND: "keeper_account_not_found";
94
92
  export declare const ERROR_ALREADY_BOOTSTRAPPED: "already_bootstrapped";
95
93
  /** Bootstrap token file not found on disk. */
96
94
  export declare const ERROR_TOKEN_FILE_MISSING: "token_file_missing";
97
- /** Bootstrap endpoint called but no token path configured. */
98
- export declare const ERROR_BOOTSTRAP_NOT_CONFIGURED: "bootstrap_not_configured";
99
95
  /** No unclaimed invite matches the signup credentials. */
100
96
  export declare const ERROR_NO_MATCHING_INVITE: "no_matching_invite";
101
97
  /** Signup conflict — username or email already taken (intentionally vague for enumeration prevention). */
@@ -1 +1 @@
1
- {"version":3,"file":"error_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/error_schemas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"error_schemas.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/error_schemas.ts"],"names":[],"mappings":"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"}
@@ -46,8 +46,6 @@ export const ERROR_PAYLOAD_TOO_LARGE = 'payload_too_large';
46
46
  // --- Origin & bearer token verification ---
47
47
  /** Request origin not in allowlist. */
48
48
  export const ERROR_FORBIDDEN_ORIGIN = 'forbidden_origin';
49
- /** Request referer not in allowlist. */
50
- export const ERROR_FORBIDDEN_REFERER = 'forbidden_referer';
51
49
  /** Bearer token sent with Origin/Referer header (browser context). */
52
50
  export const ERROR_BEARER_REJECTED_BROWSER = 'bearer_token_rejected_in_browser_context';
53
51
  /** Bearer token failed validation (missing, malformed, or revoked). */
@@ -99,8 +97,6 @@ export const ERROR_KEEPER_ACCOUNT_NOT_FOUND = 'keeper_account_not_found';
99
97
  export const ERROR_ALREADY_BOOTSTRAPPED = 'already_bootstrapped';
100
98
  /** Bootstrap token file not found on disk. */
101
99
  export const ERROR_TOKEN_FILE_MISSING = 'token_file_missing';
102
- /** Bootstrap endpoint called but no token path configured. */
103
- export const ERROR_BOOTSTRAP_NOT_CONFIGURED = 'bootstrap_not_configured';
104
100
  // --- Signup / Invites ---
105
101
  /** No unclaimed invite matches the signup credentials. */
106
102
  export const ERROR_NO_MATCHING_INVITE = 'no_matching_invite';
@@ -0,0 +1,100 @@
1
+ /**
2
+ * IP address canonicalization — collapse equivalent string forms into a
3
+ * single key per RFC 5952 (IPv6) plus the dotted form for IPv4-mapped
4
+ * IPv6 addresses.
5
+ *
6
+ * **Why this exists.** Without canonicalization, the four representations
7
+ * `::1`, `::01`, `::0001`, and `0:0:0:0:0:0:0:1` are the same IPv6 address
8
+ * but produce four distinct strings — so an attacker rotating
9
+ * equivalent forms behind a trusted-passthrough proxy could defeat
10
+ * per-IP rate limiting (each form gets a fresh bucket) and pollute
11
+ * `audit_log.ip` forensics. The collision can extend to IPv4-mapped
12
+ * IPv6 forms (`::ffff:127.0.0.1` vs `0:0:0:0:0:ffff:7f00:1` vs the
13
+ * bare `127.0.0.1`) — three keys for one address.
14
+ *
15
+ * Canonicalization runs through {@link canonicalize_ip} which:
16
+ *
17
+ * 1. Lowercases and char-set filters (`IP_LITERAL_CHARS`) — non-IP
18
+ * strings (`'unknown'`, `'attacker:controlled'`, `'::1\n'`) pass
19
+ * through unchanged so downstream strict validators can still
20
+ * reject them.
21
+ * 2. Parses via Hono's `convertIPv*ToBinary` family.
22
+ * 3. Re-emits the canonical RFC 5952 string (lowercase hex,
23
+ * longest-zero-run compressed, IPv4-mapped emitted in the dotted
24
+ * form mandated by RFC 5952 §5).
25
+ * 4. Strips the `::ffff:` prefix from dotted IPv4-mapped forms so the
26
+ * bucket collapses to plain IPv4 — the strip moves AFTER
27
+ * canonicalization because the dotted form is the only form the
28
+ * strip can recognize symmetrically.
29
+ *
30
+ * Mirrors `zzz_server::proxy::normalize_ip` (landed 2026-05-16) which
31
+ * uses the same parse-then-canonicalize-then-strip ordering for the
32
+ * same rate-limit-key-poisoning surface.
33
+ *
34
+ * @module
35
+ */
36
+ /**
37
+ * Allowed character set for a bare IP literal.
38
+ *
39
+ * Covers the union of IPv4 (digits + `.`), IPv6 (hex digits + `:`), and
40
+ * IPv4-mapped IPv6 forms (`::ffff:127.0.0.1`). Anything outside this
41
+ * set — brackets, whitespace, control bytes, letters g–z — disqualifies
42
+ * the input from parsing.
43
+ *
44
+ * Same regex `proxy.ts`'s `validate_ip_strict` uses; exported here so
45
+ * both modules can share one source of truth.
46
+ */
47
+ export declare const IP_LITERAL_CHARS: RegExp;
48
+ /**
49
+ * Canonicalize an IP address string.
50
+ *
51
+ * Returns the RFC 5952 canonical form for parseable IPv4 or IPv6
52
+ * input. Returns the input unchanged (only lowercased) when the input
53
+ * is non-IP (`'unknown'`), malformed (`'attacker:controlled'`,
54
+ * `'::1\n'`), or any string the strict char-set filter rejects.
55
+ *
56
+ * **Idempotent.** `canonicalize_ip(canonicalize_ip(x)) === canonicalize_ip(x)`
57
+ * for every input.
58
+ *
59
+ * **Order-safe for IPv4-mapped IPv6.** The `::ffff:` prefix strip
60
+ * runs AFTER the canonical emit because the canonical form of an
61
+ * IPv4-mapped IPv6 address is the dotted form (`::ffff:127.0.0.1`,
62
+ * not `::ffff:7f00:1`). Stripping before canonicalize would miss the
63
+ * full-hex form. Closes the
64
+ * `normalize_ipv4_mapped_collapse_is_order_safe` test from the Rust
65
+ * port.
66
+ *
67
+ * @example
68
+ * canonicalize_ip('::0001') // → '::1'
69
+ * canonicalize_ip('0:0:0:0:0:0:0:1') // → '::1'
70
+ * canonicalize_ip('2001:0DB8::0001') // → '2001:db8::1'
71
+ * canonicalize_ip('::ffff:127.0.0.1') // → '127.0.0.1'
72
+ * canonicalize_ip('0:0:0:0:0:ffff:7f00:1') // → '127.0.0.1'
73
+ * canonicalize_ip('::ffff:1') // → '::ffff:1' (NOT IPv4-mapped — group[5] is 0, not ffff)
74
+ * canonicalize_ip('127.0.0.1') // → '127.0.0.1'
75
+ * canonicalize_ip('not-an-ip') // → 'not-an-ip' (passes through)
76
+ * canonicalize_ip('::1\n') // → '::1\n' (fails char-set; passes through)
77
+ * canonicalize_ip('203.0.113.1:8080') // → '203.0.113.1:8080' (passes through; validate_ip_strict rejects)
78
+ */
79
+ export declare const canonicalize_ip: (ip: string) => string;
80
+ /**
81
+ * Convert a 128-bit IPv6 binary value into its RFC 5952 canonical string form.
82
+ *
83
+ * - IPv4-mapped (groups[0..5] = 0, groups[5] = 0xffff) emits the
84
+ * `::ffff:a.b.c.d` dotted form per RFC 5952 §5.
85
+ * - Otherwise: lowercase hex with no leading zeros per group (§4.1),
86
+ * the longest run of consecutive zero groups (≥ 2 groups) is
87
+ * replaced with `::` (§4.2.1, §4.2.3), and on equal-length runs the
88
+ * first one wins (§4.2.3). Single-zero groups stay as `0` (§4.2.2).
89
+ *
90
+ * Pure helper exported for the test suite to exercise the
91
+ * canonicalization invariants directly without a full
92
+ * `convertIPv6ToBinary` round-trip.
93
+ *
94
+ * @param bits - the 128-bit IPv6 value as `bigint`. Must satisfy `0n <= bits < 2n ** 128n`;
95
+ * throws `RangeError` otherwise. Silent truncation would mask caller bugs since the
96
+ * bit-extraction loop only consumes the low 128 bits.
97
+ * @throws {RangeError} when `bits` is negative or exceeds 128 bits
98
+ */
99
+ export declare const ipv6_bigint_to_canonical: (bits: bigint) => string;
100
+ //# sourceMappingURL=ip_canonical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ip_canonical.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/ip_canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;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"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * IP address canonicalization — collapse equivalent string forms into a
3
+ * single key per RFC 5952 (IPv6) plus the dotted form for IPv4-mapped
4
+ * IPv6 addresses.
5
+ *
6
+ * **Why this exists.** Without canonicalization, the four representations
7
+ * `::1`, `::01`, `::0001`, and `0:0:0:0:0:0:0:1` are the same IPv6 address
8
+ * but produce four distinct strings — so an attacker rotating
9
+ * equivalent forms behind a trusted-passthrough proxy could defeat
10
+ * per-IP rate limiting (each form gets a fresh bucket) and pollute
11
+ * `audit_log.ip` forensics. The collision can extend to IPv4-mapped
12
+ * IPv6 forms (`::ffff:127.0.0.1` vs `0:0:0:0:0:ffff:7f00:1` vs the
13
+ * bare `127.0.0.1`) — three keys for one address.
14
+ *
15
+ * Canonicalization runs through {@link canonicalize_ip} which:
16
+ *
17
+ * 1. Lowercases and char-set filters (`IP_LITERAL_CHARS`) — non-IP
18
+ * strings (`'unknown'`, `'attacker:controlled'`, `'::1\n'`) pass
19
+ * through unchanged so downstream strict validators can still
20
+ * reject them.
21
+ * 2. Parses via Hono's `convertIPv*ToBinary` family.
22
+ * 3. Re-emits the canonical RFC 5952 string (lowercase hex,
23
+ * longest-zero-run compressed, IPv4-mapped emitted in the dotted
24
+ * form mandated by RFC 5952 §5).
25
+ * 4. Strips the `::ffff:` prefix from dotted IPv4-mapped forms so the
26
+ * bucket collapses to plain IPv4 — the strip moves AFTER
27
+ * canonicalization because the dotted form is the only form the
28
+ * strip can recognize symmetrically.
29
+ *
30
+ * Mirrors `zzz_server::proxy::normalize_ip` (landed 2026-05-16) which
31
+ * uses the same parse-then-canonicalize-then-strip ordering for the
32
+ * same rate-limit-key-poisoning surface.
33
+ *
34
+ * @module
35
+ */
36
+ import { convertIPv6ToBinary, distinctRemoteAddr } from 'hono/utils/ipaddr';
37
+ /**
38
+ * Allowed character set for a bare IP literal.
39
+ *
40
+ * Covers the union of IPv4 (digits + `.`), IPv6 (hex digits + `:`), and
41
+ * IPv4-mapped IPv6 forms (`::ffff:127.0.0.1`). Anything outside this
42
+ * set — brackets, whitespace, control bytes, letters g–z — disqualifies
43
+ * the input from parsing.
44
+ *
45
+ * Same regex `proxy.ts`'s `validate_ip_strict` uses; exported here so
46
+ * both modules can share one source of truth.
47
+ */
48
+ export const IP_LITERAL_CHARS = /^[0-9a-fA-F.:]+$/;
49
+ /**
50
+ * Canonicalize an IP address string.
51
+ *
52
+ * Returns the RFC 5952 canonical form for parseable IPv4 or IPv6
53
+ * input. Returns the input unchanged (only lowercased) when the input
54
+ * is non-IP (`'unknown'`), malformed (`'attacker:controlled'`,
55
+ * `'::1\n'`), or any string the strict char-set filter rejects.
56
+ *
57
+ * **Idempotent.** `canonicalize_ip(canonicalize_ip(x)) === canonicalize_ip(x)`
58
+ * for every input.
59
+ *
60
+ * **Order-safe for IPv4-mapped IPv6.** The `::ffff:` prefix strip
61
+ * runs AFTER the canonical emit because the canonical form of an
62
+ * IPv4-mapped IPv6 address is the dotted form (`::ffff:127.0.0.1`,
63
+ * not `::ffff:7f00:1`). Stripping before canonicalize would miss the
64
+ * full-hex form. Closes the
65
+ * `normalize_ipv4_mapped_collapse_is_order_safe` test from the Rust
66
+ * port.
67
+ *
68
+ * @example
69
+ * canonicalize_ip('::0001') // → '::1'
70
+ * canonicalize_ip('0:0:0:0:0:0:0:1') // → '::1'
71
+ * canonicalize_ip('2001:0DB8::0001') // → '2001:db8::1'
72
+ * canonicalize_ip('::ffff:127.0.0.1') // → '127.0.0.1'
73
+ * canonicalize_ip('0:0:0:0:0:ffff:7f00:1') // → '127.0.0.1'
74
+ * canonicalize_ip('::ffff:1') // → '::ffff:1' (NOT IPv4-mapped — group[5] is 0, not ffff)
75
+ * canonicalize_ip('127.0.0.1') // → '127.0.0.1'
76
+ * canonicalize_ip('not-an-ip') // → 'not-an-ip' (passes through)
77
+ * canonicalize_ip('::1\n') // → '::1\n' (fails char-set; passes through)
78
+ * canonicalize_ip('203.0.113.1:8080') // → '203.0.113.1:8080' (passes through; validate_ip_strict rejects)
79
+ */
80
+ export const canonicalize_ip = (ip) => {
81
+ const lowered = ip.toLowerCase();
82
+ // Strict char-set filter — reject brackets, whitespace, control bytes,
83
+ // letters g-z before invoking the parser. Hono's `convertIPv6ToBinary`
84
+ // silently accepts `'::1\n'` and similar; canonicalizing those would
85
+ // erase the malformed form so downstream `validate_ip_strict` could no
86
+ // longer reject it. Pass-through preserves the original string.
87
+ if (!IP_LITERAL_CHARS.test(lowered))
88
+ return lowered;
89
+ const family = distinctRemoteAddr(lowered);
90
+ if (family === 'IPv4') {
91
+ // IPv4's dotted-decimal form is already canonical — no transform
92
+ // needed. Malformed forms (`999.999.999.999`) still pass through
93
+ // here; downstream `validate_ip_strict` rejects them via its own
94
+ // round-trip parse.
95
+ return lowered;
96
+ }
97
+ if (family === 'IPv6') {
98
+ try {
99
+ const bits = convertIPv6ToBinary(lowered);
100
+ const canonical = ipv6_bigint_to_canonical(bits);
101
+ // Strip `::ffff:` only when the canonical form is dotted
102
+ // IPv4-mapped (`::ffff:X.X.X.X`). Pure IPv6 values that happen
103
+ // to start with `::ffff:` (e.g. `::ffff:1` → `0:0:0:0:0:0:ffff:1`,
104
+ // where group[5] is 0 not 0xffff) emit without the dot and
105
+ // are preserved.
106
+ if (canonical.startsWith('::ffff:') && canonical.substring(7).includes('.')) {
107
+ return canonical.substring(7);
108
+ }
109
+ return canonical;
110
+ }
111
+ catch {
112
+ return lowered;
113
+ }
114
+ }
115
+ return lowered;
116
+ };
117
+ /**
118
+ * Convert a 128-bit IPv6 binary value into its RFC 5952 canonical string form.
119
+ *
120
+ * - IPv4-mapped (groups[0..5] = 0, groups[5] = 0xffff) emits the
121
+ * `::ffff:a.b.c.d` dotted form per RFC 5952 §5.
122
+ * - Otherwise: lowercase hex with no leading zeros per group (§4.1),
123
+ * the longest run of consecutive zero groups (≥ 2 groups) is
124
+ * replaced with `::` (§4.2.1, §4.2.3), and on equal-length runs the
125
+ * first one wins (§4.2.3). Single-zero groups stay as `0` (§4.2.2).
126
+ *
127
+ * Pure helper exported for the test suite to exercise the
128
+ * canonicalization invariants directly without a full
129
+ * `convertIPv6ToBinary` round-trip.
130
+ *
131
+ * @param bits - the 128-bit IPv6 value as `bigint`. Must satisfy `0n <= bits < 2n ** 128n`;
132
+ * throws `RangeError` otherwise. Silent truncation would mask caller bugs since the
133
+ * bit-extraction loop only consumes the low 128 bits.
134
+ * @throws {RangeError} when `bits` is negative or exceeds 128 bits
135
+ */
136
+ export const ipv6_bigint_to_canonical = (bits) => {
137
+ if (bits < 0n || bits >= 1n << 128n) {
138
+ throw new RangeError(`ipv6_bigint_to_canonical: bits out of [0, 2^128) range: ${bits}`);
139
+ }
140
+ // Split into 8 16-bit groups, big-endian (group[0] is the high-order group).
141
+ const groups = new Array(8);
142
+ let remaining = bits;
143
+ for (let i = 7; i >= 0; i--) {
144
+ groups[i] = Number(remaining & 0xffffn);
145
+ remaining >>= 16n;
146
+ }
147
+ // IPv4-mapped detection: leading 80 bits zero, next 16 bits 0xffff.
148
+ if (groups[0] === 0 &&
149
+ groups[1] === 0 &&
150
+ groups[2] === 0 &&
151
+ groups[3] === 0 &&
152
+ groups[4] === 0 &&
153
+ groups[5] === 0xffff) {
154
+ const high = groups[6];
155
+ const low = groups[7];
156
+ const a = (high >> 8) & 0xff;
157
+ const b = high & 0xff;
158
+ const c = (low >> 8) & 0xff;
159
+ const d = low & 0xff;
160
+ return `::ffff:${a}.${b}.${c}.${d}`;
161
+ }
162
+ // Find longest run of consecutive zero groups for `::` compression.
163
+ // RFC 5952 §4.2.1: only compress runs of two or more.
164
+ // RFC 5952 §4.2.3: on ties, compress the first run.
165
+ let best_start = -1;
166
+ let best_len = 0;
167
+ let cur_start = -1;
168
+ let cur_len = 0;
169
+ for (let i = 0; i < 8; i++) {
170
+ if (groups[i] === 0) {
171
+ if (cur_start === -1)
172
+ cur_start = i;
173
+ cur_len++;
174
+ if (cur_len > best_len) {
175
+ best_start = cur_start;
176
+ best_len = cur_len;
177
+ }
178
+ }
179
+ else {
180
+ cur_start = -1;
181
+ cur_len = 0;
182
+ }
183
+ }
184
+ const to_hex = (g) => g.toString(16);
185
+ // RFC 5952 §4.2.2 — never compress a single zero group.
186
+ if (best_len < 2) {
187
+ return groups.map(to_hex).join(':');
188
+ }
189
+ const before = groups.slice(0, best_start).map(to_hex).join(':');
190
+ const after = groups
191
+ .slice(best_start + best_len)
192
+ .map(to_hex)
193
+ .join(':');
194
+ return before + '::' + after;
195
+ };
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { Handler } from 'hono';
13
13
  /**
14
- * Parses ALLOWED_ORIGINS env var into regex matchers for request source verification.
14
+ * Parses FUZ_ALLOWED_ORIGINS env var into regex matchers for request source verification.
15
15
  * Origin allowlisting for locally-running services — not the CSRF layer
16
16
  * (that's `SameSite: strict` on session cookies).
17
17
  *
@@ -35,16 +35,24 @@ export declare const parse_allowed_origins: (env_value: string | undefined) => A
35
35
  * Tests if a request source (origin or referer) matches any of the allowed patterns.
36
36
  * Pattern matching is case-insensitive for domains (as per web standards).
37
37
  */
38
- export declare const should_allow_origin: (origin: string, allowed_patterns: Array<RegExp>) => boolean;
38
+ export declare const should_allow_origin: (origin: string, allowed_patterns: ReadonlyArray<RegExp>) => boolean;
39
39
  /**
40
40
  * Middleware that verifies the request source against an allowlist.
41
41
  *
42
42
  * Origin allowlisting (not the CSRF layer — that's `SameSite: strict` cookies):
43
- * - Checks the `Origin` header first (if present)
44
- * - Falls back to `Referer` header (if no `Origin`)
45
- * - Allows requests without `Origin`/`Referer` headers (direct access, curl, etc.)
43
+ * - Checks the `Origin` header (if present) against the allowlist
44
+ * - Allows requests without an `Origin` header (direct access, curl, etc.)
45
+ *
46
+ * Origin-only by design — Fetch spec mandates `Origin` on every unsafe
47
+ * method (POST / PUT / DELETE / PATCH) regardless of `Referrer-Policy`,
48
+ * so every real browser request on the state-changing surface carries it.
49
+ * Non-browser clients (curl, server-to-server, CLI) don't ship auto-
50
+ * attached session cookies, so CSRF isn't the relevant threat there —
51
+ * auth (bearer / daemon token) is the actual control. A Referer fallback
52
+ * would only widen the accepted-shape envelope without closing a real
53
+ * CSRF hole; mirrors `zzz_server::auth::is_request_origin_allowed`.
46
54
  *
47
55
  * @param allowed_patterns - compiled regex patterns from `parse_allowed_origins`
48
56
  */
49
- export declare const verify_request_source: (allowed_patterns: Array<RegExp>) => Handler;
57
+ export declare const verify_request_source: (allowed_patterns: ReadonlyArray<RegExp>) => Handler;
50
58
  //# 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;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,MAAM,CAAC;AAIlC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,KAAK,CAAC,MAAM,CAO5E,CAAC;AAEP;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,MAAM,EAAE,kBAAkB,KAAK,CAAC,MAAM,CAAC,KAAG,OACzC,CAAC;AAE9C;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GAChC,kBAAkB,KAAK,CAAC,MAAM,CAAC,KAAG,OA2BlC,CAAC"}
1
+ {"version":3,"file":"origin.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/http/origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,MAAM,CAAC;AAIlC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,GAAG,SAAS,KAAG,KAAK,CAAC,MAAM,CAO5E,CAAC;AAEP;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC/B,QAAQ,MAAM,EACd,kBAAkB,aAAa,CAAC,MAAM,CAAC,KACrC,OAAuD,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,qBAAqB,GAChC,kBAAkB,aAAa,CAAC,MAAM,CAAC,KAAG,OAgB1C,CAAC"}
@@ -10,9 +10,9 @@
10
10
  * @module
11
11
  */
12
12
  import { escape_regexp } from '@fuzdev/fuz_util/regexp.js';
13
- import { ERROR_FORBIDDEN_ORIGIN, ERROR_FORBIDDEN_REFERER } from './error_schemas.js';
13
+ import { ERROR_FORBIDDEN_ORIGIN } from './error_schemas.js';
14
14
  /**
15
- * Parses ALLOWED_ORIGINS env var into regex matchers for request source verification.
15
+ * Parses FUZ_ALLOWED_ORIGINS env var into regex matchers for request source verification.
16
16
  * Origin allowlisting for locally-running services — not the CSRF layer
17
17
  * (that's `SameSite: strict` on session cookies).
18
18
  *
@@ -47,9 +47,17 @@ export const should_allow_origin = (origin, allowed_patterns) => allowed_pattern
47
47
  * Middleware that verifies the request source against an allowlist.
48
48
  *
49
49
  * Origin allowlisting (not the CSRF layer — that's `SameSite: strict` cookies):
50
- * - Checks the `Origin` header first (if present)
51
- * - Falls back to `Referer` header (if no `Origin`)
52
- * - Allows requests without `Origin`/`Referer` headers (direct access, curl, etc.)
50
+ * - Checks the `Origin` header (if present) against the allowlist
51
+ * - Allows requests without an `Origin` header (direct access, curl, etc.)
52
+ *
53
+ * Origin-only by design — Fetch spec mandates `Origin` on every unsafe
54
+ * method (POST / PUT / DELETE / PATCH) regardless of `Referrer-Policy`,
55
+ * so every real browser request on the state-changing surface carries it.
56
+ * Non-browser clients (curl, server-to-server, CLI) don't ship auto-
57
+ * attached session cookies, so CSRF isn't the relevant threat there —
58
+ * auth (bearer / daemon token) is the actual control. A Referer fallback
59
+ * would only widen the accepted-shape envelope without closing a real
60
+ * CSRF hole; mirrors `zzz_server::auth::is_request_origin_allowed`.
53
61
  *
54
62
  * @param allowed_patterns - compiled regex patterns from `parse_allowed_origins`
55
63
  */
@@ -64,17 +72,7 @@ export const verify_request_source = (allowed_patterns) => (c, next) => {
64
72
  }
65
73
  return next();
66
74
  }
67
- // Check referer header (fallback for some requests like gets and navigation).
68
- // Same !== undefined check as origin.
69
- const referer = c.req.header('referer');
70
- if (referer !== undefined) {
71
- const referer_origin = extract_origin_from_referer(referer);
72
- if (!should_allow_origin(referer_origin, allowed_patterns)) {
73
- return c.json({ error: ERROR_FORBIDDEN_REFERER }, 403);
74
- }
75
- return next();
76
- }
77
- // No origin or referer - direct access (curl, CLI, etc.)
75
+ // No origin header - direct access (curl, CLI, etc.)
78
76
  // Allow through since token auth is the primary security control.
79
77
  return next();
80
78
  };
@@ -182,19 +180,3 @@ const origin_pattern_to_regexp = (pattern) => {
182
180
  // Case-insensitive matching (web standards specify domains are case-insensitive)
183
181
  return new RegExp(regex_pattern, 'i');
184
182
  };
185
- /**
186
- * Extracts the origin from a referer URL, removing the path, query string, and fragment.
187
- *
188
- * @param referer - the referer URL (e.g., `https://fuz.dev/path?query#hash`)
189
- * @returns the origin part (e.g., `https://fuz.dev`)
190
- */
191
- const extract_origin_from_referer = (referer) => {
192
- try {
193
- return new URL(referer).origin;
194
- }
195
- catch {
196
- // If URL parsing fails, return the original string
197
- // (it will likely fail pattern matching anyway)
198
- return referer;
199
- }
200
- };
@@ -47,7 +47,7 @@ export interface EmitAfterCommitContext {
47
47
  * middleware (in `server/app_server.ts` and the per-message WS dispatcher)
48
48
  * is the only site that ever invokes `fn`. This is load-bearing: a
49
49
  * previous implementation queued `Promise.resolve().then(fn)`, which
50
- * JavaScript's microtask scheduler drains before the wrapping
50
+ * JS's microtask scheduler drains before the wrapping
51
51
  * `await db.query('COMMIT')` resumes — `fn` fired mid-transaction and a
52
52
  * rollback would leak a notification for state that never landed.
53
53
  *
@@ -37,7 +37,7 @@
37
37
  * middleware (in `server/app_server.ts` and the per-message WS dispatcher)
38
38
  * is the only site that ever invokes `fn`. This is load-bearing: a
39
39
  * previous implementation queued `Promise.resolve().then(fn)`, which
40
- * JavaScript's microtask scheduler drains before the wrapping
40
+ * JS's microtask scheduler drains before the wrapping
41
41
  * `await db.query('COMMIT')` resumes — `fn` fired mid-transaction and a
42
42
  * rollback would leak a notification for state that never landed.
43
43
  *
@@ -13,11 +13,19 @@ import type { MiddlewareSpec } from './middleware_spec.js';
13
13
  /**
14
14
  * Normalize an IP address for consistent matching and storage.
15
15
  *
16
- * - Strips `::ffff:` prefix from IPv4-mapped IPv6 addresses
17
- * (e.g. `::ffff:127.0.0.1` `127.0.0.1`)
18
- * - Lowercases for case-insensitive IPv6 comparison
19
- * - Idempotent: calling twice produces the same result
20
- * - Safe on non-IP strings: `normalize_ip('unknown')` returns `'unknown'`
16
+ * Delegates to `canonicalize_ip` from `ip_canonical.ts` collapses
17
+ * RFC 5952-equivalent IPv6 forms (`::1`, `::0001`, `0:0:0:0:0:0:0:1`)
18
+ * into a single key, emits IPv4-mapped IPv6 in dotted form, and
19
+ * strips the `::ffff:` prefix from dotted IPv4-mapped values so the
20
+ * bucket collapses to plain IPv4.
21
+ *
22
+ * - Lowercases for case-insensitive IPv6 comparison.
23
+ * - Idempotent: calling twice produces the same result.
24
+ * - Safe on non-IP strings: `normalize_ip('unknown')` returns `'unknown'`.
25
+ * Malformed inputs (`'attacker:controlled'`, `'::1\n'`,
26
+ * `'203.0.113.1:8080'`) pass through unchanged so downstream
27
+ * `validate_ip_strict` can still reject them — canonicalization
28
+ * never erases the malformed-form signal.
21
29
  */
22
30
  export declare const normalize_ip: (ip: string) => string;
23
31
  /**
@@ -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;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"}
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;AAGzD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GAAI,IAAI,MAAM,KAAG,MAA6B,CAAC;AAExE;;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;AAiBF;;;;;;;;;;;;;;;;;;;;;;;;;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"}
@@ -8,24 +8,25 @@
8
8
  * @module
9
9
  */
10
10
  import { convertIPv4ToBinary, convertIPv6ToBinary, distinctRemoteAddr } from 'hono/utils/ipaddr';
11
+ import { canonicalize_ip, IP_LITERAL_CHARS } from './ip_canonical.js';
11
12
  /**
12
13
  * Normalize an IP address for consistent matching and storage.
13
14
  *
14
- * - Strips `::ffff:` prefix from IPv4-mapped IPv6 addresses
15
- * (e.g. `::ffff:127.0.0.1` `127.0.0.1`)
16
- * - Lowercases for case-insensitive IPv6 comparison
17
- * - Idempotent: calling twice produces the same result
18
- * - Safe on non-IP strings: `normalize_ip('unknown')` returns `'unknown'`
15
+ * Delegates to `canonicalize_ip` from `ip_canonical.ts` collapses
16
+ * RFC 5952-equivalent IPv6 forms (`::1`, `::0001`, `0:0:0:0:0:0:0:1`)
17
+ * into a single key, emits IPv4-mapped IPv6 in dotted form, and
18
+ * strips the `::ffff:` prefix from dotted IPv4-mapped values so the
19
+ * bucket collapses to plain IPv4.
20
+ *
21
+ * - Lowercases for case-insensitive IPv6 comparison.
22
+ * - Idempotent: calling twice produces the same result.
23
+ * - Safe on non-IP strings: `normalize_ip('unknown')` returns `'unknown'`.
24
+ * Malformed inputs (`'attacker:controlled'`, `'::1\n'`,
25
+ * `'203.0.113.1:8080'`) pass through unchanged so downstream
26
+ * `validate_ip_strict` can still reject them — canonicalization
27
+ * never erases the malformed-form signal.
19
28
  */
20
- export const normalize_ip = (ip) => {
21
- const lowered = ip.toLowerCase();
22
- // Strip ::ffff: prefix only when remainder contains a dot (IPv4-mapped IPv6).
23
- // This distinguishes ::ffff:127.0.0.1 (IPv4-mapped) from ::ffff:1 (pure IPv6).
24
- if (lowered.startsWith('::ffff:') && lowered.substring(7).includes('.')) {
25
- return lowered.substring(7);
26
- }
27
- return lowered;
28
- };
29
+ export const normalize_ip = (ip) => canonicalize_ip(ip);
29
30
  /**
30
31
  * Parse a trusted proxy entry string into a structured form.
31
32
  *
@@ -91,15 +92,6 @@ const cidr_contains = (ip_binary, network, prefix, total_bits) => {
91
92
  const shift = BigInt(total_bits - prefix);
92
93
  return ip_binary >> shift === network >> shift;
93
94
  };
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
95
  /**
104
96
  * Strict IP validity check.
105
97
  *