@vivero/stoma 0.1.0-rc.10
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/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/adapters/bun.d.ts +9 -0
- package/dist/adapters/bun.js +8 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +49 -0
- package/dist/adapters/cloudflare.js +85 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/deno.d.ts +9 -0
- package/dist/adapters/deno.js +8 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/durable-object.d.ts +63 -0
- package/dist/adapters/durable-object.js +46 -0
- package/dist/adapters/durable-object.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +53 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory.d.ts +9 -0
- package/dist/adapters/memory.js +14 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/node.d.ts +9 -0
- package/dist/adapters/node.js +8 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/postgres.d.ts +109 -0
- package/dist/adapters/postgres.js +242 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/redis.d.ts +116 -0
- package/dist/adapters/redis.js +194 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/testing.d.ts +32 -0
- package/dist/adapters/testing.js +33 -0
- package/dist/adapters/testing.js.map +1 -0
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +21 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +48 -0
- package/dist/config/merge.js +83 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/schema.d.ts +254 -0
- package/dist/config/schema.js +109 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/errors.d.ts +66 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/gateway.d.ts +44 -0
- package/dist/core/gateway.js +400 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/health.d.ts +78 -0
- package/dist/core/health.js +65 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/pipeline.d.ts +62 -0
- package/dist/core/pipeline.js +214 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/protocol.d.ts +4 -0
- package/dist/core/protocol.js +1 -0
- package/dist/core/protocol.js.map +1 -0
- package/dist/core/scope.d.ts +67 -0
- package/dist/core/scope.js +44 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/core/types.d.ts +252 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/admin.d.ts +32 -0
- package/dist/observability/admin.js +85 -0
- package/dist/observability/admin.js.map +1 -0
- package/dist/observability/metrics.d.ts +78 -0
- package/dist/observability/metrics.js +107 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/tracing.d.ts +149 -0
- package/dist/observability/tracing.js +191 -0
- package/dist/observability/tracing.js.map +1 -0
- package/dist/policies/auth/api-key-auth.d.ts +64 -0
- package/dist/policies/auth/api-key-auth.js +93 -0
- package/dist/policies/auth/api-key-auth.js.map +1 -0
- package/dist/policies/auth/basic-auth.d.ts +33 -0
- package/dist/policies/auth/basic-auth.js +96 -0
- package/dist/policies/auth/basic-auth.js.map +1 -0
- package/dist/policies/auth/crypto.d.ts +29 -0
- package/dist/policies/auth/crypto.js +100 -0
- package/dist/policies/auth/crypto.js.map +1 -0
- package/dist/policies/auth/generate-http-signature.d.ts +30 -0
- package/dist/policies/auth/generate-http-signature.js +79 -0
- package/dist/policies/auth/generate-http-signature.js.map +1 -0
- package/dist/policies/auth/generate-jwt.d.ts +44 -0
- package/dist/policies/auth/generate-jwt.js +99 -0
- package/dist/policies/auth/generate-jwt.js.map +1 -0
- package/dist/policies/auth/http-signature-base.d.ts +55 -0
- package/dist/policies/auth/http-signature-base.js +140 -0
- package/dist/policies/auth/http-signature-base.js.map +1 -0
- package/dist/policies/auth/jws.d.ts +46 -0
- package/dist/policies/auth/jws.js +317 -0
- package/dist/policies/auth/jws.js.map +1 -0
- package/dist/policies/auth/jwt-auth.d.ts +64 -0
- package/dist/policies/auth/jwt-auth.js +266 -0
- package/dist/policies/auth/jwt-auth.js.map +1 -0
- package/dist/policies/auth/oauth2.d.ts +38 -0
- package/dist/policies/auth/oauth2.js +254 -0
- package/dist/policies/auth/oauth2.js.map +1 -0
- package/dist/policies/auth/rbac.d.ts +30 -0
- package/dist/policies/auth/rbac.js +115 -0
- package/dist/policies/auth/rbac.js.map +1 -0
- package/dist/policies/auth/verify-http-signature.d.ts +30 -0
- package/dist/policies/auth/verify-http-signature.js +147 -0
- package/dist/policies/auth/verify-http-signature.js.map +1 -0
- package/dist/policies/index.d.ts +51 -0
- package/dist/policies/index.js +109 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/mock.d.ts +60 -0
- package/dist/policies/mock.js +29 -0
- package/dist/policies/mock.js.map +1 -0
- package/dist/policies/observability/assign-metrics.d.ts +37 -0
- package/dist/policies/observability/assign-metrics.js +29 -0
- package/dist/policies/observability/assign-metrics.js.map +1 -0
- package/dist/policies/observability/metrics-reporter.d.ts +25 -0
- package/dist/policies/observability/metrics-reporter.js +62 -0
- package/dist/policies/observability/metrics-reporter.js.map +1 -0
- package/dist/policies/observability/request-log.d.ts +135 -0
- package/dist/policies/observability/request-log.js +134 -0
- package/dist/policies/observability/request-log.js.map +1 -0
- package/dist/policies/observability/server-timing.d.ts +35 -0
- package/dist/policies/observability/server-timing.js +89 -0
- package/dist/policies/observability/server-timing.js.map +1 -0
- package/dist/policies/proxy.d.ts +59 -0
- package/dist/policies/proxy.js +47 -0
- package/dist/policies/proxy.js.map +1 -0
- package/dist/policies/resilience/circuit-breaker.d.ts +4 -0
- package/dist/policies/resilience/circuit-breaker.js +280 -0
- package/dist/policies/resilience/circuit-breaker.js.map +1 -0
- package/dist/policies/resilience/latency-injection.d.ts +35 -0
- package/dist/policies/resilience/latency-injection.js +26 -0
- package/dist/policies/resilience/latency-injection.js.map +1 -0
- package/dist/policies/resilience/retry.d.ts +71 -0
- package/dist/policies/resilience/retry.js +79 -0
- package/dist/policies/resilience/retry.js.map +1 -0
- package/dist/policies/resilience/timeout.d.ts +32 -0
- package/dist/policies/resilience/timeout.js +46 -0
- package/dist/policies/resilience/timeout.js.map +1 -0
- package/dist/policies/sdk/define-policy.d.ts +176 -0
- package/dist/policies/sdk/define-policy.js +42 -0
- package/dist/policies/sdk/define-policy.js.map +1 -0
- package/dist/policies/sdk/helpers.d.ts +132 -0
- package/dist/policies/sdk/helpers.js +87 -0
- package/dist/policies/sdk/helpers.js.map +1 -0
- package/dist/policies/sdk/index.d.ts +10 -0
- package/dist/policies/sdk/index.js +35 -0
- package/dist/policies/sdk/index.js.map +1 -0
- package/dist/policies/sdk/priority.d.ts +44 -0
- package/dist/policies/sdk/priority.js +36 -0
- package/dist/policies/sdk/priority.js.map +1 -0
- package/dist/policies/sdk/testing.d.ts +53 -0
- package/dist/policies/sdk/testing.js +41 -0
- package/dist/policies/sdk/testing.js.map +1 -0
- package/dist/policies/sdk/trace.d.ts +73 -0
- package/dist/policies/sdk/trace.js +25 -0
- package/dist/policies/sdk/trace.js.map +1 -0
- package/dist/policies/traffic/cache.d.ts +4 -0
- package/dist/policies/traffic/cache.js +224 -0
- package/dist/policies/traffic/cache.js.map +1 -0
- package/dist/policies/traffic/dynamic-routing.d.ts +54 -0
- package/dist/policies/traffic/dynamic-routing.js +36 -0
- package/dist/policies/traffic/dynamic-routing.js.map +1 -0
- package/dist/policies/traffic/geo-ip-filter.d.ts +37 -0
- package/dist/policies/traffic/geo-ip-filter.js +74 -0
- package/dist/policies/traffic/geo-ip-filter.js.map +1 -0
- package/dist/policies/traffic/http-callout.d.ts +59 -0
- package/dist/policies/traffic/http-callout.js +69 -0
- package/dist/policies/traffic/http-callout.js.map +1 -0
- package/dist/policies/traffic/interrupt.d.ts +46 -0
- package/dist/policies/traffic/interrupt.js +38 -0
- package/dist/policies/traffic/interrupt.js.map +1 -0
- package/dist/policies/traffic/ip-filter.d.ts +47 -0
- package/dist/policies/traffic/ip-filter.js +57 -0
- package/dist/policies/traffic/ip-filter.js.map +1 -0
- package/dist/policies/traffic/json-threat-protection.d.ts +51 -0
- package/dist/policies/traffic/json-threat-protection.js +173 -0
- package/dist/policies/traffic/json-threat-protection.js.map +1 -0
- package/dist/policies/traffic/rate-limit.d.ts +4 -0
- package/dist/policies/traffic/rate-limit.js +145 -0
- package/dist/policies/traffic/rate-limit.js.map +1 -0
- package/dist/policies/traffic/regex-threat-protection.d.ts +54 -0
- package/dist/policies/traffic/regex-threat-protection.js +109 -0
- package/dist/policies/traffic/regex-threat-protection.js.map +1 -0
- package/dist/policies/traffic/request-limit.d.ts +27 -0
- package/dist/policies/traffic/request-limit.js +41 -0
- package/dist/policies/traffic/request-limit.js.map +1 -0
- package/dist/policies/traffic/resource-filter.d.ts +38 -0
- package/dist/policies/traffic/resource-filter.js +184 -0
- package/dist/policies/traffic/resource-filter.js.map +1 -0
- package/dist/policies/traffic/ssl-enforce.d.ts +27 -0
- package/dist/policies/traffic/ssl-enforce.js +38 -0
- package/dist/policies/traffic/ssl-enforce.js.map +1 -0
- package/dist/policies/traffic/traffic-shadow.d.ts +40 -0
- package/dist/policies/traffic/traffic-shadow.js +87 -0
- package/dist/policies/traffic/traffic-shadow.js.map +1 -0
- package/dist/policies/transform/assign-attributes.d.ts +33 -0
- package/dist/policies/transform/assign-attributes.js +38 -0
- package/dist/policies/transform/assign-attributes.js.map +1 -0
- package/dist/policies/transform/assign-content.d.ts +40 -0
- package/dist/policies/transform/assign-content.js +185 -0
- package/dist/policies/transform/assign-content.js.map +1 -0
- package/dist/policies/transform/cors.d.ts +57 -0
- package/dist/policies/transform/cors.js +23 -0
- package/dist/policies/transform/cors.js.map +1 -0
- package/dist/policies/transform/json-validation.d.ts +50 -0
- package/dist/policies/transform/json-validation.js +125 -0
- package/dist/policies/transform/json-validation.js.map +1 -0
- package/dist/policies/transform/override-method.d.ts +33 -0
- package/dist/policies/transform/override-method.js +48 -0
- package/dist/policies/transform/override-method.js.map +1 -0
- package/dist/policies/transform/request-validation.d.ts +59 -0
- package/dist/policies/transform/request-validation.js +121 -0
- package/dist/policies/transform/request-validation.js.map +1 -0
- package/dist/policies/transform/transform.d.ts +75 -0
- package/dist/policies/transform/transform.js +116 -0
- package/dist/policies/transform/transform.js.map +1 -0
- package/dist/policies/types.d.ts +4 -0
- package/dist/policies/types.js +1 -0
- package/dist/policies/types.js.map +1 -0
- package/dist/protocol-2fD3DJrL.d.ts +725 -0
- package/dist/utils/cidr.d.ts +58 -0
- package/dist/utils/cidr.js +107 -0
- package/dist/utils/cidr.js.map +1 -0
- package/dist/utils/debug.d.ts +1 -0
- package/dist/utils/debug.js +13 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/headers.d.ts +68 -0
- package/dist/utils/headers.js +25 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/ip.d.ts +64 -0
- package/dist/utils/ip.js +29 -0
- package/dist/utils/ip.js.map +1 -0
- package/dist/utils/redact.d.ts +30 -0
- package/dist/utils/redact.js +52 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/request-id.d.ts +11 -0
- package/dist/utils/request-id.js +7 -0
- package/dist/utils/request-id.js.map +1 -0
- package/dist/utils/timing-safe.d.ts +31 -0
- package/dist/utils/timing-safe.js +17 -0
- package/dist/utils/timing-safe.js.map +1 -0
- package/dist/utils/timing.d.ts +27 -0
- package/dist/utils/timing.js +12 -0
- package/dist/utils/timing.js.map +1 -0
- package/dist/utils/trace-context.d.ts +51 -0
- package/dist/utils/trace-context.js +37 -0
- package/dist/utils/trace-context.js.map +1 -0
- package/package.json +213 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPv4/IPv6 CIDR parsing and range matching utilities.
|
|
3
|
+
*
|
|
4
|
+
* Used by the {@link ipFilter} policy to check client IPs against
|
|
5
|
+
* allowlists and denylists. Supports both individual IPs and CIDR notation
|
|
6
|
+
* for IPv4 (`10.0.0.0/8`) and IPv6 (`2001:db8::/32`).
|
|
7
|
+
*
|
|
8
|
+
* IPv4 uses 32-bit unsigned integers for fast bitwise matching.
|
|
9
|
+
* IPv6 uses BigInt for 128-bit address arithmetic.
|
|
10
|
+
*
|
|
11
|
+
* @module cidr
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Parsed IPv4 CIDR range represented as 32-bit network address and mask.
|
|
15
|
+
*/
|
|
16
|
+
interface ParsedCIDRv4 {
|
|
17
|
+
readonly version: 4;
|
|
18
|
+
/** Network address as a 32-bit unsigned integer. */
|
|
19
|
+
readonly network: number;
|
|
20
|
+
/** Subnet mask as a 32-bit unsigned integer. */
|
|
21
|
+
readonly mask: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parsed IPv6 CIDR range represented as 128-bit BigInt network address and mask.
|
|
25
|
+
*/
|
|
26
|
+
interface ParsedCIDRv6 {
|
|
27
|
+
readonly version: 6;
|
|
28
|
+
/** Network address as a 128-bit unsigned BigInt. */
|
|
29
|
+
readonly network: bigint;
|
|
30
|
+
/** Subnet mask as a 128-bit unsigned BigInt. */
|
|
31
|
+
readonly mask: bigint;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Discriminated union of parsed IPv4 and IPv6 CIDR ranges.
|
|
35
|
+
* Use the `version` field to narrow the type.
|
|
36
|
+
*/
|
|
37
|
+
type ParsedCIDR = ParsedCIDRv4 | ParsedCIDRv6;
|
|
38
|
+
/**
|
|
39
|
+
* Parse a CIDR string (IPv4 or IPv6) into a network address and mask.
|
|
40
|
+
* Bare IPs without a prefix length are treated as `/32` (IPv4) or `/128` (IPv6).
|
|
41
|
+
*
|
|
42
|
+
* @param cidr - IP address or CIDR notation string (IPv4 or IPv6).
|
|
43
|
+
* @returns Parsed range, or `null` if the input is invalid.
|
|
44
|
+
*/
|
|
45
|
+
declare function parseCIDR(cidr: string): ParsedCIDR | null;
|
|
46
|
+
/**
|
|
47
|
+
* Check if an IP address (IPv4 or IPv6) falls within any of the parsed CIDR ranges.
|
|
48
|
+
*
|
|
49
|
+
* IPv4 addresses only match IPv4 ranges and vice versa - there is no
|
|
50
|
+
* cross-family matching.
|
|
51
|
+
*
|
|
52
|
+
* @param ip - IP address string.
|
|
53
|
+
* @param ranges - Pre-parsed CIDR ranges from {@link parseCIDR}.
|
|
54
|
+
* @returns `true` if the IP matches any range.
|
|
55
|
+
*/
|
|
56
|
+
declare function isInRange(ip: string, ranges: ParsedCIDR[]): boolean;
|
|
57
|
+
|
|
58
|
+
export { type ParsedCIDR, type ParsedCIDRv4, type ParsedCIDRv6, isInRange, parseCIDR };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
function ipv4ToInt(ip) {
|
|
2
|
+
const parts = ip.split(".");
|
|
3
|
+
if (parts.length !== 4) return -1;
|
|
4
|
+
let result = 0;
|
|
5
|
+
for (const part of parts) {
|
|
6
|
+
const n = Number(part);
|
|
7
|
+
if (Number.isNaN(n) || n < 0 || n > 255) return -1;
|
|
8
|
+
result = result << 8 | n;
|
|
9
|
+
}
|
|
10
|
+
return result >>> 0;
|
|
11
|
+
}
|
|
12
|
+
function parseIPv4CIDR(ip, prefix) {
|
|
13
|
+
const addr = ipv4ToInt(ip);
|
|
14
|
+
if (addr === -1) return null;
|
|
15
|
+
if (prefix < 0 || prefix > 32) return null;
|
|
16
|
+
const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
|
|
17
|
+
return { version: 4, network: (addr & mask) >>> 0, mask };
|
|
18
|
+
}
|
|
19
|
+
const IPV6_FULL_MASK = (1n << 128n) - 1n;
|
|
20
|
+
function ipv6ToBigInt(raw) {
|
|
21
|
+
let addr = raw;
|
|
22
|
+
const zoneIdx = addr.indexOf("%");
|
|
23
|
+
if (zoneIdx !== -1) {
|
|
24
|
+
addr = addr.slice(0, zoneIdx);
|
|
25
|
+
}
|
|
26
|
+
addr = addr.toLowerCase();
|
|
27
|
+
const doubleColonIdx = addr.indexOf("::");
|
|
28
|
+
let left;
|
|
29
|
+
let right;
|
|
30
|
+
if (doubleColonIdx !== -1) {
|
|
31
|
+
if (addr.indexOf("::", doubleColonIdx + 2) !== -1) return null;
|
|
32
|
+
const leftPart = addr.slice(0, doubleColonIdx);
|
|
33
|
+
const rightPart = addr.slice(doubleColonIdx + 2);
|
|
34
|
+
left = leftPart === "" ? [] : leftPart.split(":");
|
|
35
|
+
right = rightPart === "" ? [] : rightPart.split(":");
|
|
36
|
+
const totalGroups = left.length + right.length;
|
|
37
|
+
if (totalGroups > 8) return null;
|
|
38
|
+
const fillCount = 8 - totalGroups;
|
|
39
|
+
const groups2 = [...left, ...Array(fillCount).fill("0"), ...right];
|
|
40
|
+
return groupsToBI(groups2);
|
|
41
|
+
}
|
|
42
|
+
const groups = addr.split(":");
|
|
43
|
+
if (groups.length !== 8) return null;
|
|
44
|
+
return groupsToBI(groups);
|
|
45
|
+
}
|
|
46
|
+
function groupsToBI(groups) {
|
|
47
|
+
if (groups.length !== 8) return null;
|
|
48
|
+
let result = 0n;
|
|
49
|
+
for (const group of groups) {
|
|
50
|
+
if (group.length === 0 || group.length > 4) return null;
|
|
51
|
+
const val = Number.parseInt(group, 16);
|
|
52
|
+
if (Number.isNaN(val) || val < 0 || val > 65535) return null;
|
|
53
|
+
result = result << 16n | BigInt(val);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
function parseIPv6CIDR(ip, prefix) {
|
|
58
|
+
const addr = ipv6ToBigInt(ip);
|
|
59
|
+
if (addr === null) return null;
|
|
60
|
+
if (prefix < 0 || prefix > 128) return null;
|
|
61
|
+
const mask = prefix === 0 ? 0n : IPV6_FULL_MASK << BigInt(128 - prefix) & IPV6_FULL_MASK;
|
|
62
|
+
return { version: 6, network: addr & mask, mask };
|
|
63
|
+
}
|
|
64
|
+
function isIPv6(input) {
|
|
65
|
+
return input.includes(":");
|
|
66
|
+
}
|
|
67
|
+
function parseCIDR(cidr) {
|
|
68
|
+
const slash = cidr.indexOf("/");
|
|
69
|
+
if (slash === -1) {
|
|
70
|
+
if (isIPv6(cidr)) {
|
|
71
|
+
return parseIPv6CIDR(cidr, 128);
|
|
72
|
+
}
|
|
73
|
+
const addr = ipv4ToInt(cidr);
|
|
74
|
+
if (addr === -1) return null;
|
|
75
|
+
return { version: 4, network: addr, mask: 4294967295 >>> 0 };
|
|
76
|
+
}
|
|
77
|
+
const ipPart = cidr.slice(0, slash);
|
|
78
|
+
const bits = Number(cidr.slice(slash + 1));
|
|
79
|
+
if (Number.isNaN(bits)) return null;
|
|
80
|
+
if (isIPv6(ipPart)) {
|
|
81
|
+
return parseIPv6CIDR(ipPart, bits);
|
|
82
|
+
}
|
|
83
|
+
return parseIPv4CIDR(ipPart, bits);
|
|
84
|
+
}
|
|
85
|
+
function isInRange(ip, ranges) {
|
|
86
|
+
if (isIPv6(ip)) {
|
|
87
|
+
const addr2 = ipv6ToBigInt(ip);
|
|
88
|
+
if (addr2 === null) return false;
|
|
89
|
+
for (const range of ranges) {
|
|
90
|
+
if (range.version !== 6) continue;
|
|
91
|
+
if ((addr2 & range.mask) === range.network) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const addr = ipv4ToInt(ip);
|
|
96
|
+
if (addr === -1) return false;
|
|
97
|
+
for (const range of ranges) {
|
|
98
|
+
if (range.version !== 4) continue;
|
|
99
|
+
if ((addr & range.mask) >>> 0 === range.network) return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
isInRange,
|
|
105
|
+
parseCIDR
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=cidr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/cidr.ts"],"sourcesContent":["/**\n * IPv4/IPv6 CIDR parsing and range matching utilities.\n *\n * Used by the {@link ipFilter} policy to check client IPs against\n * allowlists and denylists. Supports both individual IPs and CIDR notation\n * for IPv4 (`10.0.0.0/8`) and IPv6 (`2001:db8::/32`).\n *\n * IPv4 uses 32-bit unsigned integers for fast bitwise matching.\n * IPv6 uses BigInt for 128-bit address arithmetic.\n *\n * @module cidr\n */\n\n// ── Discriminated union types ────────────────────────────────────────\n\n/**\n * Parsed IPv4 CIDR range represented as 32-bit network address and mask.\n */\nexport interface ParsedCIDRv4 {\n readonly version: 4;\n /** Network address as a 32-bit unsigned integer. */\n readonly network: number;\n /** Subnet mask as a 32-bit unsigned integer. */\n readonly mask: number;\n}\n\n/**\n * Parsed IPv6 CIDR range represented as 128-bit BigInt network address and mask.\n */\nexport interface ParsedCIDRv6 {\n readonly version: 6;\n /** Network address as a 128-bit unsigned BigInt. */\n readonly network: bigint;\n /** Subnet mask as a 128-bit unsigned BigInt. */\n readonly mask: bigint;\n}\n\n/**\n * Discriminated union of parsed IPv4 and IPv6 CIDR ranges.\n * Use the `version` field to narrow the type.\n */\nexport type ParsedCIDR = ParsedCIDRv4 | ParsedCIDRv6;\n\n// ── IPv4 internals ───────────────────────────────────────────────────\n\n/**\n * Parse an IPv4 address to a 32-bit unsigned integer.\n * Returns `-1` for anything that isn't a valid dotted-quad.\n */\nfunction ipv4ToInt(ip: string): number {\n const parts = ip.split(\".\");\n if (parts.length !== 4) return -1;\n let result = 0;\n for (const part of parts) {\n const n = Number(part);\n if (Number.isNaN(n) || n < 0 || n > 255) return -1;\n result = (result << 8) | n;\n }\n return result >>> 0; // unsigned\n}\n\n/**\n * Parse an IPv4 CIDR string into a `ParsedCIDRv4`.\n * Bare IPs without a prefix length are treated as `/32`.\n */\nfunction parseIPv4CIDR(ip: string, prefix: number): ParsedCIDRv4 | null {\n const addr = ipv4ToInt(ip);\n if (addr === -1) return null;\n if (prefix < 0 || prefix > 32) return null;\n\n const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;\n return { version: 4, network: (addr & mask) >>> 0, mask };\n}\n\n// ── IPv6 internals ───────────────────────────────────────────────────\n\n/** Full 128-bit mask for IPv6. */\nconst IPV6_FULL_MASK = (1n << 128n) - 1n;\n\n/**\n * Parse an IPv6 address string into a 128-bit BigInt.\n * Handles `::` zero compression, zone IDs (`%eth0`), and uppercase hex.\n * Returns `null` for invalid addresses.\n */\nfunction ipv6ToBigInt(raw: string): bigint | null {\n // Strip zone ID (e.g. \"fe80::1%eth0\" → \"fe80::1\")\n let addr = raw;\n const zoneIdx = addr.indexOf(\"%\");\n if (zoneIdx !== -1) {\n addr = addr.slice(0, zoneIdx);\n }\n\n addr = addr.toLowerCase();\n\n // Handle :: compression\n const doubleColonIdx = addr.indexOf(\"::\");\n let left: string[];\n let right: string[];\n\n if (doubleColonIdx !== -1) {\n // Reject multiple ::\n if (addr.indexOf(\"::\", doubleColonIdx + 2) !== -1) return null;\n\n const leftPart = addr.slice(0, doubleColonIdx);\n const rightPart = addr.slice(doubleColonIdx + 2);\n\n left = leftPart === \"\" ? [] : leftPart.split(\":\");\n right = rightPart === \"\" ? [] : rightPart.split(\":\");\n\n const totalGroups = left.length + right.length;\n if (totalGroups > 8) return null;\n\n // Fill middle with zeros\n const fillCount = 8 - totalGroups;\n const groups = [...left, ...Array(fillCount).fill(\"0\"), ...right];\n return groupsToBI(groups);\n }\n\n // No :: - must have exactly 8 groups\n const groups = addr.split(\":\");\n if (groups.length !== 8) return null;\n return groupsToBI(groups);\n}\n\n/**\n * Convert an array of exactly 8 hex group strings into a 128-bit BigInt.\n */\nfunction groupsToBI(groups: string[]): bigint | null {\n if (groups.length !== 8) return null;\n let result = 0n;\n for (const group of groups) {\n if (group.length === 0 || group.length > 4) return null;\n const val = Number.parseInt(group, 16);\n if (Number.isNaN(val) || val < 0 || val > 0xffff) return null;\n result = (result << 16n) | BigInt(val);\n }\n return result;\n}\n\n/**\n * Parse an IPv6 CIDR string into a `ParsedCIDRv6`.\n * Bare addresses without a prefix length are treated as `/128`.\n */\nfunction parseIPv6CIDR(ip: string, prefix: number): ParsedCIDRv6 | null {\n const addr = ipv6ToBigInt(ip);\n if (addr === null) return null;\n if (prefix < 0 || prefix > 128) return null;\n\n const mask =\n prefix === 0\n ? 0n\n : (IPV6_FULL_MASK << BigInt(128 - prefix)) & IPV6_FULL_MASK;\n return { version: 6, network: addr & mask, mask };\n}\n\n// ── Public API ───────────────────────────────────────────────────────\n\n/**\n * Detect whether a CIDR or IP string is IPv6.\n * Presence of `:` indicates IPv6 (IPv4 dotted-quad never contains colons).\n */\nfunction isIPv6(input: string): boolean {\n return input.includes(\":\");\n}\n\n/**\n * Parse a CIDR string (IPv4 or IPv6) into a network address and mask.\n * Bare IPs without a prefix length are treated as `/32` (IPv4) or `/128` (IPv6).\n *\n * @param cidr - IP address or CIDR notation string (IPv4 or IPv6).\n * @returns Parsed range, or `null` if the input is invalid.\n */\nexport function parseCIDR(cidr: string): ParsedCIDR | null {\n const slash = cidr.indexOf(\"/\");\n\n if (slash === -1) {\n // Bare IP - no prefix length\n if (isIPv6(cidr)) {\n return parseIPv6CIDR(cidr, 128);\n }\n const addr = ipv4ToInt(cidr);\n if (addr === -1) return null;\n return { version: 4, network: addr, mask: 0xffffffff >>> 0 };\n }\n\n const ipPart = cidr.slice(0, slash);\n const bits = Number(cidr.slice(slash + 1));\n if (Number.isNaN(bits)) return null;\n\n if (isIPv6(ipPart)) {\n return parseIPv6CIDR(ipPart, bits);\n }\n return parseIPv4CIDR(ipPart, bits);\n}\n\n/**\n * Check if an IP address (IPv4 or IPv6) falls within any of the parsed CIDR ranges.\n *\n * IPv4 addresses only match IPv4 ranges and vice versa - there is no\n * cross-family matching.\n *\n * @param ip - IP address string.\n * @param ranges - Pre-parsed CIDR ranges from {@link parseCIDR}.\n * @returns `true` if the IP matches any range.\n */\nexport function isInRange(ip: string, ranges: ParsedCIDR[]): boolean {\n if (isIPv6(ip)) {\n const addr = ipv6ToBigInt(ip);\n if (addr === null) return false;\n for (const range of ranges) {\n if (range.version !== 6) continue;\n if ((addr & range.mask) === range.network) return true;\n }\n return false;\n }\n\n // IPv4 path\n const addr = ipv4ToInt(ip);\n if (addr === -1) return false;\n for (const range of ranges) {\n if (range.version !== 4) continue;\n if ((addr & range.mask) >>> 0 === range.network) return true;\n }\n return false;\n}\n"],"mappings":"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","names":["groups","addr"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DebugLogger, createDebugFactory, createDebugger, matchNamespace, noopDebugLogger } from '@vivero/stoma-core';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/debug.ts"],"sourcesContent":["/**\n * Re-exports from `@vivero/stoma-core`.\n *\n * The debug module lives in `packages/core` and is shared across all Stoma\n * packages. This file exists so that gateway-internal imports\n * (`../utils/debug`) continue to resolve without a 12-file refactor.\n *\n * @module debug\n */\nexport {\n createDebugFactory,\n createDebugger,\n type DebugLogger,\n matchNamespace,\n noopDebugLogger,\n} from \"@vivero/stoma-core\";\n"],"mappings":"AASA;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,OACK;","names":[]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP header utilities for Stoma policies.
|
|
5
|
+
*
|
|
6
|
+
* Consolidates common header manipulation patterns to avoid duplication
|
|
7
|
+
* across policies (auth, proxy, transform, etc.).
|
|
8
|
+
*
|
|
9
|
+
* @module utils/headers
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sanitize a header value to prevent header injection attacks.
|
|
14
|
+
*
|
|
15
|
+
* Strips control characters (CR, LF, NUL) that could allow attackers
|
|
16
|
+
* to inject or manipulate HTTP headers.
|
|
17
|
+
*
|
|
18
|
+
* @param value - The header value to sanitize
|
|
19
|
+
* @returns The sanitized value
|
|
20
|
+
*/
|
|
21
|
+
declare function sanitizeHeaderValue(value: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Escape a string for safe inclusion in an HTTP header value.
|
|
24
|
+
*
|
|
25
|
+
* Adds backslash escaping for double quotes to prevent header injection.
|
|
26
|
+
*
|
|
27
|
+
* @param value - The value to escape
|
|
28
|
+
* @returns The escaped value
|
|
29
|
+
*/
|
|
30
|
+
declare function escapeHeaderValue(value: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Create a mutable clone of the request headers from a Hono context.
|
|
33
|
+
*
|
|
34
|
+
* The Workers runtime has immutable Request.headers. This function creates
|
|
35
|
+
* a mutable Headers object that can be modified and then applied back
|
|
36
|
+
* to the request.
|
|
37
|
+
*
|
|
38
|
+
* @param c - The Hono context
|
|
39
|
+
* @returns A new mutable Headers object cloned from the request
|
|
40
|
+
*/
|
|
41
|
+
declare function cloneRequestHeaders(c: Context): Headers;
|
|
42
|
+
/**
|
|
43
|
+
* Apply modified headers back to the request in a Hono context.
|
|
44
|
+
*
|
|
45
|
+
* @param c - The Hono context
|
|
46
|
+
* @param headers - The modified headers to apply
|
|
47
|
+
*/
|
|
48
|
+
declare function applyRequestHeaders(c: Context, headers: Headers): void;
|
|
49
|
+
/**
|
|
50
|
+
* Modify request headers with a callback function.
|
|
51
|
+
*
|
|
52
|
+
* This is a convenience wrapper that combines cloneRequestHeaders and
|
|
53
|
+
* applyRequestHeaders into a single operation.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* withModifiedHeaders(c, (headers) => {
|
|
58
|
+
* headers.set("X-Custom", "value");
|
|
59
|
+
* headers.delete("X-Removed");
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @param c - The Hono context
|
|
64
|
+
* @param mutator - Function to modify the headers
|
|
65
|
+
*/
|
|
66
|
+
declare function withModifiedHeaders(c: Context, mutator: (headers: Headers) => void): void;
|
|
67
|
+
|
|
68
|
+
export { applyRequestHeaders, cloneRequestHeaders, escapeHeaderValue, sanitizeHeaderValue, withModifiedHeaders };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function sanitizeHeaderValue(value) {
|
|
2
|
+
return value.replace(/[\r\n\0]/g, "");
|
|
3
|
+
}
|
|
4
|
+
function escapeHeaderValue(value) {
|
|
5
|
+
return value.replace(/"/g, '\\"');
|
|
6
|
+
}
|
|
7
|
+
function cloneRequestHeaders(c) {
|
|
8
|
+
return new Headers(c.req.raw.headers);
|
|
9
|
+
}
|
|
10
|
+
function applyRequestHeaders(c, headers) {
|
|
11
|
+
c.req.raw = new Request(c.req.raw, { headers });
|
|
12
|
+
}
|
|
13
|
+
function withModifiedHeaders(c, mutator) {
|
|
14
|
+
const headers = cloneRequestHeaders(c);
|
|
15
|
+
mutator(headers);
|
|
16
|
+
applyRequestHeaders(c, headers);
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
applyRequestHeaders,
|
|
20
|
+
cloneRequestHeaders,
|
|
21
|
+
escapeHeaderValue,
|
|
22
|
+
sanitizeHeaderValue,
|
|
23
|
+
withModifiedHeaders
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/headers.ts"],"sourcesContent":["/**\n * HTTP header utilities for Stoma policies.\n *\n * Consolidates common header manipulation patterns to avoid duplication\n * across policies (auth, proxy, transform, etc.).\n *\n * @module utils/headers\n */\nimport type { Context } from \"hono\";\n\n/**\n * Sanitize a header value to prevent header injection attacks.\n *\n * Strips control characters (CR, LF, NUL) that could allow attackers\n * to inject or manipulate HTTP headers.\n *\n * @param value - The header value to sanitize\n * @returns The sanitized value\n */\nexport function sanitizeHeaderValue(value: string): string {\n return value.replace(/[\\r\\n\\0]/g, \"\");\n}\n\n/**\n * Escape a string for safe inclusion in an HTTP header value.\n *\n * Adds backslash escaping for double quotes to prevent header injection.\n *\n * @param value - The value to escape\n * @returns The escaped value\n */\nexport function escapeHeaderValue(value: string): string {\n return value.replace(/\"/g, '\\\\\"');\n}\n\n/**\n * Create a mutable clone of the request headers from a Hono context.\n *\n * The Workers runtime has immutable Request.headers. This function creates\n * a mutable Headers object that can be modified and then applied back\n * to the request.\n *\n * @param c - The Hono context\n * @returns A new mutable Headers object cloned from the request\n */\nexport function cloneRequestHeaders(c: Context): Headers {\n return new Headers(c.req.raw.headers);\n}\n\n/**\n * Apply modified headers back to the request in a Hono context.\n *\n * @param c - The Hono context\n * @param headers - The modified headers to apply\n */\nexport function applyRequestHeaders(c: Context, headers: Headers): void {\n c.req.raw = new Request(c.req.raw, { headers });\n}\n\n/**\n * Modify request headers with a callback function.\n *\n * This is a convenience wrapper that combines cloneRequestHeaders and\n * applyRequestHeaders into a single operation.\n *\n * @example\n * ```ts\n * withModifiedHeaders(c, (headers) => {\n * headers.set(\"X-Custom\", \"value\");\n * headers.delete(\"X-Removed\");\n * });\n * ```\n *\n * @param c - The Hono context\n * @param mutator - Function to modify the headers\n */\nexport function withModifiedHeaders(\n c: Context,\n mutator: (headers: Headers) => void\n): void {\n const headers = cloneRequestHeaders(c);\n mutator(headers);\n applyRequestHeaders(c, headers);\n}\n"],"mappings":"AAmBO,SAAS,oBAAoB,OAAuB;AACzD,SAAO,MAAM,QAAQ,aAAa,EAAE;AACtC;AAUO,SAAS,kBAAkB,OAAuB;AACvD,SAAO,MAAM,QAAQ,MAAM,KAAK;AAClC;AAYO,SAAS,oBAAoB,GAAqB;AACvD,SAAO,IAAI,QAAQ,EAAE,IAAI,IAAI,OAAO;AACtC;AAQO,SAAS,oBAAoB,GAAY,SAAwB;AACtE,IAAE,IAAI,MAAM,IAAI,QAAQ,EAAE,IAAI,KAAK,EAAE,QAAQ,CAAC;AAChD;AAmBO,SAAS,oBACd,GACA,SACM;AACN,QAAM,UAAU,oBAAoB,CAAC;AACrC,UAAQ,OAAO;AACf,sBAAoB,GAAG,OAAO;AAChC;","names":[]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared client IP extraction utility.
|
|
3
|
+
*
|
|
4
|
+
* Centralises the IP header lookup logic used by rate limiting, IP filtering,
|
|
5
|
+
* and request logging. The header priority order is configurable - the first
|
|
6
|
+
* header that contains a value wins.
|
|
7
|
+
*
|
|
8
|
+
* @module ip
|
|
9
|
+
*/
|
|
10
|
+
/** Default ordered list of headers to inspect for the client IP. */
|
|
11
|
+
declare const DEFAULT_IP_HEADERS: string[];
|
|
12
|
+
interface ExtractClientIpOptions {
|
|
13
|
+
/** Ordered list of headers to inspect. Default: ["cf-connecting-ip", "x-forwarded-for"]. */
|
|
14
|
+
ipHeaders?: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* List of trusted proxy IP ranges (CIDR notation).
|
|
17
|
+
* When specified, X-Forwarded-For will only be trusted if the client IP
|
|
18
|
+
* (leftmost) is within one of these ranges.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Only trust X-Forwarded-For from Cloudflare IPs
|
|
22
|
+
* { ipHeaders: ["cf-connecting-ip", "x-forwarded-for"], trustedProxies: ["173.245.48.0/20"] }
|
|
23
|
+
*/
|
|
24
|
+
trustedProxies?: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* When true, use the rightmost IP from X-Forwarded-For instead of leftmost.
|
|
27
|
+
* The rightmost IP is the one added by the most recent trusted proxy.
|
|
28
|
+
* Default: false.
|
|
29
|
+
*/
|
|
30
|
+
useRightmostForwardedIp?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Fallback IP address used when no headers match (e.g., the socket remote
|
|
33
|
+
* address from the Node.js HTTP server). This is checked after all
|
|
34
|
+
* configured headers have been exhausted.
|
|
35
|
+
*/
|
|
36
|
+
fallbackAddress?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract the client IP address from request headers.
|
|
40
|
+
*
|
|
41
|
+
* Iterates through `ipHeaders` in order. For comma-separated headers like
|
|
42
|
+
* `X-Forwarded-For`, the behavior depends on options:
|
|
43
|
+
* - By default, returns the first (leftmost) value
|
|
44
|
+
* - With `useRightmostForwardedIp: true`, returns the last (rightmost) value
|
|
45
|
+
* - With `trustedProxies`, validates the leftmost IP against trusted ranges
|
|
46
|
+
*
|
|
47
|
+
* @security The `X-Forwarded-For` header is trivially spoofable by clients
|
|
48
|
+
* outside of trusted proxy infrastructure. An attacker can set arbitrary IP
|
|
49
|
+
* values to bypass IP-based allowlists, rate limits, or geo-restrictions.
|
|
50
|
+
*
|
|
51
|
+
* To mitigate:
|
|
52
|
+
* 1. Use `cf-connecting-ip` when behind Cloudflare (not spoofable by clients)
|
|
53
|
+
* 2. Configure `trustedProxies` to validate X-Forwarded-For IPs
|
|
54
|
+
* 3. Use `useRightmostForwardedIp: true` when behind a trusted proxy
|
|
55
|
+
*
|
|
56
|
+
* @param headers - An object with a `.get(name)` method (e.g. `Headers`, Hono `c.req`).
|
|
57
|
+
* @param options - Configuration options for IP extraction.
|
|
58
|
+
* @returns The extracted IP address, or `"unknown"` if none found.
|
|
59
|
+
*/
|
|
60
|
+
declare function extractClientIp(headers: {
|
|
61
|
+
get(name: string): string | null | undefined;
|
|
62
|
+
}, options?: ExtractClientIpOptions): string;
|
|
63
|
+
|
|
64
|
+
export { DEFAULT_IP_HEADERS, type ExtractClientIpOptions, extractClientIp };
|
package/dist/utils/ip.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isInRange, parseCIDR } from "./cidr";
|
|
2
|
+
const DEFAULT_IP_HEADERS = ["cf-connecting-ip", "x-forwarded-for"];
|
|
3
|
+
function extractClientIp(headers, options = {}) {
|
|
4
|
+
const {
|
|
5
|
+
ipHeaders = DEFAULT_IP_HEADERS,
|
|
6
|
+
trustedProxies,
|
|
7
|
+
useRightmostForwardedIp = false,
|
|
8
|
+
fallbackAddress
|
|
9
|
+
} = options;
|
|
10
|
+
const parsedTrustedProxies = trustedProxies ? trustedProxies.map((cidr) => parseCIDR(cidr)).filter((r) => r !== null) : null;
|
|
11
|
+
for (const header of ipHeaders) {
|
|
12
|
+
const value = headers.get(header);
|
|
13
|
+
if (!value) continue;
|
|
14
|
+
const ips = value.split(",").map((ip) => ip.trim());
|
|
15
|
+
const clientIp = useRightmostForwardedIp ? ips[ips.length - 1] : ips[0];
|
|
16
|
+
if (parsedTrustedProxies && header.toLowerCase() === "x-forwarded-for") {
|
|
17
|
+
if (!isInRange(clientIp, parsedTrustedProxies)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return clientIp;
|
|
22
|
+
}
|
|
23
|
+
return fallbackAddress ?? "unknown";
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_IP_HEADERS,
|
|
27
|
+
extractClientIp
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=ip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/ip.ts"],"sourcesContent":["/**\n * Shared client IP extraction utility.\n *\n * Centralises the IP header lookup logic used by rate limiting, IP filtering,\n * and request logging. The header priority order is configurable - the first\n * header that contains a value wins.\n *\n * @module ip\n */\n\nimport { isInRange, type ParsedCIDR, parseCIDR } from \"./cidr\";\n\n/** Default ordered list of headers to inspect for the client IP. */\nexport const DEFAULT_IP_HEADERS = [\"cf-connecting-ip\", \"x-forwarded-for\"];\n\nexport interface ExtractClientIpOptions {\n /** Ordered list of headers to inspect. Default: [\"cf-connecting-ip\", \"x-forwarded-for\"]. */\n ipHeaders?: readonly string[];\n /**\n * List of trusted proxy IP ranges (CIDR notation).\n * When specified, X-Forwarded-For will only be trusted if the client IP\n * (leftmost) is within one of these ranges.\n *\n * @example\n * // Only trust X-Forwarded-For from Cloudflare IPs\n * { ipHeaders: [\"cf-connecting-ip\", \"x-forwarded-for\"], trustedProxies: [\"173.245.48.0/20\"] }\n */\n trustedProxies?: readonly string[];\n /**\n * When true, use the rightmost IP from X-Forwarded-For instead of leftmost.\n * The rightmost IP is the one added by the most recent trusted proxy.\n * Default: false.\n */\n useRightmostForwardedIp?: boolean;\n /**\n * Fallback IP address used when no headers match (e.g., the socket remote\n * address from the Node.js HTTP server). This is checked after all\n * configured headers have been exhausted.\n */\n fallbackAddress?: string;\n}\n\n/**\n * Extract the client IP address from request headers.\n *\n * Iterates through `ipHeaders` in order. For comma-separated headers like\n * `X-Forwarded-For`, the behavior depends on options:\n * - By default, returns the first (leftmost) value\n * - With `useRightmostForwardedIp: true`, returns the last (rightmost) value\n * - With `trustedProxies`, validates the leftmost IP against trusted ranges\n *\n * @security The `X-Forwarded-For` header is trivially spoofable by clients\n * outside of trusted proxy infrastructure. An attacker can set arbitrary IP\n * values to bypass IP-based allowlists, rate limits, or geo-restrictions.\n *\n * To mitigate:\n * 1. Use `cf-connecting-ip` when behind Cloudflare (not spoofable by clients)\n * 2. Configure `trustedProxies` to validate X-Forwarded-For IPs\n * 3. Use `useRightmostForwardedIp: true` when behind a trusted proxy\n *\n * @param headers - An object with a `.get(name)` method (e.g. `Headers`, Hono `c.req`).\n * @param options - Configuration options for IP extraction.\n * @returns The extracted IP address, or `\"unknown\"` if none found.\n */\nexport function extractClientIp(\n headers: { get(name: string): string | null | undefined },\n options: ExtractClientIpOptions = {}\n): string {\n const {\n ipHeaders = DEFAULT_IP_HEADERS,\n trustedProxies,\n useRightmostForwardedIp = false,\n fallbackAddress,\n } = options;\n\n const parsedTrustedProxies: ParsedCIDR[] | null = trustedProxies\n ? trustedProxies\n .map((cidr) => parseCIDR(cidr))\n .filter((r): r is ParsedCIDR => r !== null)\n : null;\n\n for (const header of ipHeaders) {\n const value = headers.get(header);\n if (!value) continue;\n\n const ips = value.split(\",\").map((ip) => ip.trim());\n const clientIp = useRightmostForwardedIp ? ips[ips.length - 1] : ips[0];\n\n // If trustedProxies is configured, validate against it\n if (parsedTrustedProxies && header.toLowerCase() === \"x-forwarded-for\") {\n if (!isInRange(clientIp, parsedTrustedProxies)) {\n // Client IP is not from trusted proxy - don't trust this header\n continue;\n }\n }\n\n return clientIp;\n }\n return fallbackAddress ?? \"unknown\";\n}\n"],"mappings":"AAUA,SAAS,WAA4B,iBAAiB;AAG/C,MAAM,qBAAqB,CAAC,oBAAoB,iBAAiB;AAmDjE,SAAS,gBACd,SACA,UAAkC,CAAC,GAC3B;AACR,QAAM;AAAA,IACJ,YAAY;AAAA,IACZ;AAAA,IACA,0BAA0B;AAAA,IAC1B;AAAA,EACF,IAAI;AAEJ,QAAM,uBAA4C,iBAC9C,eACG,IAAI,CAAC,SAAS,UAAU,IAAI,CAAC,EAC7B,OAAO,CAAC,MAAuB,MAAM,IAAI,IAC5C;AAEJ,aAAW,UAAU,WAAW;AAC9B,UAAM,QAAQ,QAAQ,IAAI,MAAM;AAChC,QAAI,CAAC,MAAO;AAEZ,UAAM,MAAM,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;AAClD,UAAM,WAAW,0BAA0B,IAAI,IAAI,SAAS,CAAC,IAAI,IAAI,CAAC;AAGtE,QAAI,wBAAwB,OAAO,YAAY,MAAM,mBAAmB;AACtE,UAAI,CAAC,UAAU,UAAU,oBAAoB,GAAG;AAE9C;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACA,SAAO,mBAAmB;AAC5B;","names":[]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field redaction utility for safe logging of request/response bodies.
|
|
3
|
+
*
|
|
4
|
+
* Supports dot-notation paths (`auth.token`) and single-level wildcards
|
|
5
|
+
* (`*.password`) to match fields at any nesting level. Always deep-clones
|
|
6
|
+
* before mutating - the original object is never modified.
|
|
7
|
+
*
|
|
8
|
+
* @module redact
|
|
9
|
+
*/
|
|
10
|
+
/** Configuration for field redaction. */
|
|
11
|
+
interface RedactConfig {
|
|
12
|
+
/** JSON field paths to redact (e.g., `"password"`, `"*.secret"`, `"auth.token"`). */
|
|
13
|
+
paths: string[];
|
|
14
|
+
/** Replacement text. Default: `"[REDACTED]"`. */
|
|
15
|
+
replacement?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Redact sensitive fields from an object for safe logging.
|
|
19
|
+
*
|
|
20
|
+
* Deep-clones the input, then replaces values at the specified paths
|
|
21
|
+
* with the replacement string. Non-object/non-array input passes
|
|
22
|
+
* through unchanged.
|
|
23
|
+
*
|
|
24
|
+
* @param obj - The value to redact (typically a parsed JSON body).
|
|
25
|
+
* @param config - Paths to redact and optional replacement text.
|
|
26
|
+
* @returns A deep-cloned copy with specified fields redacted.
|
|
27
|
+
*/
|
|
28
|
+
declare function redactFields(obj: unknown, config: RedactConfig): unknown;
|
|
29
|
+
|
|
30
|
+
export { type RedactConfig, redactFields };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function redactFields(obj, config) {
|
|
2
|
+
if (!isPlainObject(obj) && !Array.isArray(obj)) {
|
|
3
|
+
return obj;
|
|
4
|
+
}
|
|
5
|
+
const replacement = config.replacement ?? "[REDACTED]";
|
|
6
|
+
const cloned = structuredClone(obj);
|
|
7
|
+
for (const path of config.paths) {
|
|
8
|
+
applyRedaction(cloned, path.split("."), 0, replacement);
|
|
9
|
+
}
|
|
10
|
+
return cloned;
|
|
11
|
+
}
|
|
12
|
+
function applyRedaction(obj, segments, depth, replacement) {
|
|
13
|
+
if (depth >= segments.length || obj == null) return;
|
|
14
|
+
const segment = segments[depth];
|
|
15
|
+
const isLast = depth === segments.length - 1;
|
|
16
|
+
if (Array.isArray(obj)) {
|
|
17
|
+
for (const item of obj) {
|
|
18
|
+
applyRedaction(item, segments, depth, replacement);
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!isPlainObject(obj)) return;
|
|
23
|
+
if (segment === "*") {
|
|
24
|
+
for (const key of Object.keys(obj)) {
|
|
25
|
+
if (isLast) {
|
|
26
|
+
obj[key] = replacement;
|
|
27
|
+
} else {
|
|
28
|
+
applyRedaction(
|
|
29
|
+
obj[key],
|
|
30
|
+
segments,
|
|
31
|
+
depth + 1,
|
|
32
|
+
replacement
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
const record = obj;
|
|
38
|
+
if (!(segment in record)) return;
|
|
39
|
+
if (isLast) {
|
|
40
|
+
record[segment] = replacement;
|
|
41
|
+
} else {
|
|
42
|
+
applyRedaction(record[segment], segments, depth + 1, replacement);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isPlainObject(val) {
|
|
47
|
+
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
redactFields
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/redact.ts"],"sourcesContent":["/**\n * Field redaction utility for safe logging of request/response bodies.\n *\n * Supports dot-notation paths (`auth.token`) and single-level wildcards\n * (`*.password`) to match fields at any nesting level. Always deep-clones\n * before mutating - the original object is never modified.\n *\n * @module redact\n */\n\n/** Configuration for field redaction. */\nexport interface RedactConfig {\n /** JSON field paths to redact (e.g., `\"password\"`, `\"*.secret\"`, `\"auth.token\"`). */\n paths: string[];\n /** Replacement text. Default: `\"[REDACTED]\"`. */\n replacement?: string;\n}\n\n/**\n * Redact sensitive fields from an object for safe logging.\n *\n * Deep-clones the input, then replaces values at the specified paths\n * with the replacement string. Non-object/non-array input passes\n * through unchanged.\n *\n * @param obj - The value to redact (typically a parsed JSON body).\n * @param config - Paths to redact and optional replacement text.\n * @returns A deep-cloned copy with specified fields redacted.\n */\nexport function redactFields(obj: unknown, config: RedactConfig): unknown {\n if (!isPlainObject(obj) && !Array.isArray(obj)) {\n return obj;\n }\n\n const replacement = config.replacement ?? \"[REDACTED]\";\n const cloned = structuredClone(obj);\n\n for (const path of config.paths) {\n applyRedaction(cloned, path.split(\".\"), 0, replacement);\n }\n\n return cloned;\n}\n\nfunction applyRedaction(\n obj: unknown,\n segments: string[],\n depth: number,\n replacement: string\n): void {\n if (depth >= segments.length || obj == null) return;\n\n const segment = segments[depth];\n const isLast = depth === segments.length - 1;\n\n if (Array.isArray(obj)) {\n for (const item of obj) {\n applyRedaction(item, segments, depth, replacement);\n }\n return;\n }\n\n if (!isPlainObject(obj)) return;\n\n if (segment === \"*\") {\n // Wildcard: apply to all keys at this level\n for (const key of Object.keys(obj)) {\n if (isLast) {\n (obj as Record<string, unknown>)[key] = replacement;\n } else {\n applyRedaction(\n (obj as Record<string, unknown>)[key],\n segments,\n depth + 1,\n replacement\n );\n }\n }\n } else {\n const record = obj as Record<string, unknown>;\n if (!(segment in record)) return;\n\n if (isLast) {\n record[segment] = replacement;\n } else {\n applyRedaction(record[segment], segments, depth + 1, replacement);\n }\n }\n}\n\nfunction isPlainObject(val: unknown): val is Record<string, unknown> {\n return typeof val === \"object\" && val !== null && !Array.isArray(val);\n}\n"],"mappings":"AA6BO,SAAS,aAAa,KAAc,QAA+B;AACxE,MAAI,CAAC,cAAc,GAAG,KAAK,CAAC,MAAM,QAAQ,GAAG,GAAG;AAC9C,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,OAAO,eAAe;AAC1C,QAAM,SAAS,gBAAgB,GAAG;AAElC,aAAW,QAAQ,OAAO,OAAO;AAC/B,mBAAe,QAAQ,KAAK,MAAM,GAAG,GAAG,GAAG,WAAW;AAAA,EACxD;AAEA,SAAO;AACT;AAEA,SAAS,eACP,KACA,UACA,OACA,aACM;AACN,MAAI,SAAS,SAAS,UAAU,OAAO,KAAM;AAE7C,QAAM,UAAU,SAAS,KAAK;AAC9B,QAAM,SAAS,UAAU,SAAS,SAAS;AAE3C,MAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,eAAW,QAAQ,KAAK;AACtB,qBAAe,MAAM,UAAU,OAAO,WAAW;AAAA,IACnD;AACA;AAAA,EACF;AAEA,MAAI,CAAC,cAAc,GAAG,EAAG;AAEzB,MAAI,YAAY,KAAK;AAEnB,eAAW,OAAO,OAAO,KAAK,GAAG,GAAG;AAClC,UAAI,QAAQ;AACV,QAAC,IAAgC,GAAG,IAAI;AAAA,MAC1C,OAAO;AACL;AAAA,UACG,IAAgC,GAAG;AAAA,UACpC;AAAA,UACA,QAAQ;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,OAAO;AACL,UAAM,SAAS;AACf,QAAI,EAAE,WAAW,QAAS;AAE1B,QAAI,QAAQ;AACV,aAAO,OAAO,IAAI;AAAA,IACpB,OAAO;AACL,qBAAe,OAAO,OAAO,GAAG,UAAU,QAAQ,GAAG,WAAW;AAAA,IAClE;AAAA,EACF;AACF;AAEA,SAAS,cAAc,KAA8C;AACnE,SAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC,MAAM,QAAQ,GAAG;AACtE;","names":[]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a unique request ID using the Web Crypto API.
|
|
3
|
+
*
|
|
4
|
+
* Returns a v4 UUID. Available in Cloudflare Workers, Deno, Node 19+,
|
|
5
|
+
* and modern browsers.
|
|
6
|
+
*
|
|
7
|
+
* @returns A v4 UUID string (e.g. `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"`).
|
|
8
|
+
*/
|
|
9
|
+
declare function generateRequestId(): string;
|
|
10
|
+
|
|
11
|
+
export { generateRequestId };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/request-id.ts"],"sourcesContent":["/**\n * Generate a unique request ID using the Web Crypto API.\n *\n * Returns a v4 UUID. Available in Cloudflare Workers, Deno, Node 19+,\n * and modern browsers.\n *\n * @returns A v4 UUID string (e.g. `\"a1b2c3d4-e5f6-7890-abcd-ef1234567890\"`).\n */\nexport function generateRequestId(): string {\n return crypto.randomUUID();\n}\n"],"mappings":"AAQO,SAAS,oBAA4B;AAC1C,SAAO,OAAO,WAAW;AAC3B;","names":[]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constant-time string comparison to prevent timing side-channel attacks.
|
|
3
|
+
*
|
|
4
|
+
* Use this when comparing secrets (API keys, tokens, HMAC digests) to
|
|
5
|
+
* prevent an attacker from inferring the correct value by measuring
|
|
6
|
+
* response time differences.
|
|
7
|
+
*
|
|
8
|
+
* @module timing-safe
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Compare two strings in constant time.
|
|
12
|
+
*
|
|
13
|
+
* Returns `true` if `a` and `b` are identical, `false` otherwise.
|
|
14
|
+
* The comparison always examines every byte of the longer string,
|
|
15
|
+
* preventing timing side-channels that leak prefix information.
|
|
16
|
+
*
|
|
17
|
+
* @param a - First string to compare.
|
|
18
|
+
* @param b - Second string to compare.
|
|
19
|
+
* @returns `true` if the strings are identical.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { timingSafeEqual } from "@vivero/stoma";
|
|
24
|
+
*
|
|
25
|
+
* // Use in API key validators to prevent timing attacks
|
|
26
|
+
* const isValid = timingSafeEqual(providedKey, storedKey);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare function timingSafeEqual(a: string, b: string): boolean;
|
|
30
|
+
|
|
31
|
+
export { timingSafeEqual };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function timingSafeEqual(a, b) {
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const bufA = encoder.encode(a);
|
|
4
|
+
const bufB = encoder.encode(b);
|
|
5
|
+
const maxLen = Math.max(bufA.length, bufB.length);
|
|
6
|
+
let mismatch = bufA.length !== bufB.length ? 1 : 0;
|
|
7
|
+
for (let i = 0; i < maxLen; i++) {
|
|
8
|
+
const byteA = i < bufA.length ? bufA[i] : 0;
|
|
9
|
+
const byteB = i < bufB.length ? bufB[i] : 0;
|
|
10
|
+
mismatch |= byteA ^ byteB;
|
|
11
|
+
}
|
|
12
|
+
return mismatch === 0;
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
timingSafeEqual
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=timing-safe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/timing-safe.ts"],"sourcesContent":["/**\n * Constant-time string comparison to prevent timing side-channel attacks.\n *\n * Use this when comparing secrets (API keys, tokens, HMAC digests) to\n * prevent an attacker from inferring the correct value by measuring\n * response time differences.\n *\n * @module timing-safe\n */\n\n/**\n * Compare two strings in constant time.\n *\n * Returns `true` if `a` and `b` are identical, `false` otherwise.\n * The comparison always examines every byte of the longer string,\n * preventing timing side-channels that leak prefix information.\n *\n * @param a - First string to compare.\n * @param b - Second string to compare.\n * @returns `true` if the strings are identical.\n *\n * @example\n * ```ts\n * import { timingSafeEqual } from \"@vivero/stoma\";\n *\n * // Use in API key validators to prevent timing attacks\n * const isValid = timingSafeEqual(providedKey, storedKey);\n * ```\n */\nexport function timingSafeEqual(a: string, b: string): boolean {\n const encoder = new TextEncoder();\n const bufA = encoder.encode(a);\n const bufB = encoder.encode(b);\n\n // Length mismatch - still compare to avoid early exit timing leak\n const maxLen = Math.max(bufA.length, bufB.length);\n let mismatch = bufA.length !== bufB.length ? 1 : 0;\n\n for (let i = 0; i < maxLen; i++) {\n // Use 0 as fallback for shorter buffer - still accumulates XOR\n const byteA = i < bufA.length ? bufA[i] : 0;\n const byteB = i < bufB.length ? bufB[i] : 0;\n mismatch |= byteA ^ byteB;\n }\n\n return mismatch === 0;\n}\n"],"mappings":"AA6BO,SAAS,gBAAgB,GAAW,GAAoB;AAC7D,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,CAAC;AAC7B,QAAM,OAAO,QAAQ,OAAO,CAAC;AAG7B,QAAM,SAAS,KAAK,IAAI,KAAK,QAAQ,KAAK,MAAM;AAChD,MAAI,WAAW,KAAK,WAAW,KAAK,SAAS,IAAI;AAEjD,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAE/B,UAAM,QAAQ,IAAI,KAAK,SAAS,KAAK,CAAC,IAAI;AAC1C,UAAM,QAAQ,IAAI,KAAK,SAAS,KAAK,CAAC,IAAI;AAC1C,gBAAY,QAAQ;AAAA,EACtB;AAEA,SAAO,aAAa;AACtB;","names":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared timing utilities.
|
|
3
|
+
*
|
|
4
|
+
* @module timing
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Convert inclusive (onion-model) timings to self-time and reverse to
|
|
8
|
+
* execution order.
|
|
9
|
+
*
|
|
10
|
+
* `policiesToMiddleware()` records inclusive wall-clock time for each
|
|
11
|
+
* policy - meaning an outer policy's duration includes all inner
|
|
12
|
+
* policies plus the upstream. The timings array is ordered
|
|
13
|
+
* innermost-first (the innermost middleware finishes first and pushes
|
|
14
|
+
* its timing before outer ones).
|
|
15
|
+
*
|
|
16
|
+
* Self-time is computed as:
|
|
17
|
+
* self[0] = inclusive[0] (innermost, includes upstream)
|
|
18
|
+
* self[i] = inclusive[i] - inclusive[i-1] for i > 0
|
|
19
|
+
*
|
|
20
|
+
* The result is reversed so entries are in execution order (outermost
|
|
21
|
+
* policy first), which is the natural reading order for a waterfall.
|
|
22
|
+
*/
|
|
23
|
+
declare function toSelfTimes<T extends {
|
|
24
|
+
durationMs: number;
|
|
25
|
+
}>(timings: T[]): T[];
|
|
26
|
+
|
|
27
|
+
export { toSelfTimes };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function toSelfTimes(timings) {
|
|
2
|
+
const selfTimes = timings.map((entry, i) => ({
|
|
3
|
+
...entry,
|
|
4
|
+
durationMs: i === 0 ? entry.durationMs : Math.max(0, entry.durationMs - timings[i - 1].durationMs)
|
|
5
|
+
}));
|
|
6
|
+
selfTimes.reverse();
|
|
7
|
+
return selfTimes;
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
toSelfTimes
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/timing.ts"],"sourcesContent":["/**\n * Shared timing utilities.\n *\n * @module timing\n */\n\n/**\n * Convert inclusive (onion-model) timings to self-time and reverse to\n * execution order.\n *\n * `policiesToMiddleware()` records inclusive wall-clock time for each\n * policy - meaning an outer policy's duration includes all inner\n * policies plus the upstream. The timings array is ordered\n * innermost-first (the innermost middleware finishes first and pushes\n * its timing before outer ones).\n *\n * Self-time is computed as:\n * self[0] = inclusive[0] (innermost, includes upstream)\n * self[i] = inclusive[i] - inclusive[i-1] for i > 0\n *\n * The result is reversed so entries are in execution order (outermost\n * policy first), which is the natural reading order for a waterfall.\n */\nexport function toSelfTimes<T extends { durationMs: number }>(\n timings: T[]\n): T[] {\n const selfTimes = timings.map((entry, i) => ({\n ...entry,\n durationMs:\n i === 0\n ? entry.durationMs\n : Math.max(0, entry.durationMs - timings[i - 1].durationMs),\n }));\n\n // Reverse: innermost-first → outermost-first (execution order)\n selfTimes.reverse();\n return selfTimes;\n}\n"],"mappings":"AAuBO,SAAS,YACd,SACK;AACL,QAAM,YAAY,QAAQ,IAAI,CAAC,OAAO,OAAO;AAAA,IAC3C,GAAG;AAAA,IACH,YACE,MAAM,IACF,MAAM,aACN,KAAK,IAAI,GAAG,MAAM,aAAa,QAAQ,IAAI,CAAC,EAAE,UAAU;AAAA,EAChE,EAAE;AAGF,YAAU,QAAQ;AAClB,SAAO;AACT;","names":[]}
|