@beechcms/core 0.9.3 → 0.9.4

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.
@@ -1,3 +1,7 @@
1
- export declare const PRIVATE_HOST_REGEX: RegExp;
1
+ /**
2
+ * Whether a webhook target host is loopback, link-local, private or otherwise internal.
3
+ * Expects `URL.hostname`, which the WHATWG parser already normalises: IPv6 literals stay bracketed
4
+ * and IPv4-mapped tails are rewritten to hex, so the address is parsed, never string-matched.
5
+ */
2
6
  export declare function isPrivateHost(hostname: string): boolean;
3
7
  //# sourceMappingURL=webhook-validation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-validation.d.ts","sourceRoot":"","sources":["../../src/webhooks/webhook-validation.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,kBAAkB,QAC0H,CAAA;AAEzJ,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEvD"}
1
+ {"version":3,"file":"webhook-validation.d.ts","sourceRoot":"","sources":["../../src/webhooks/webhook-validation.ts"],"names":[],"mappings":"AA4EA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAYvD"}
@@ -1,6 +1,88 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Copyright (c) 2024–2026 Flavio De Musso
3
- export const PRIVATE_HOST_REGEX = /^(localhost|127\.\d+\.\d+\.\d+|::1|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|fc00:|fe80:|0\.0\.0\.0)/i;
3
+ const IPV4_PATTERN = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
4
+ const HEXTET_PATTERN = /^[0-9a-f]{1,4}$/;
5
+ function parseIPv4(text) {
6
+ const match = IPV4_PATTERN.exec(text);
7
+ if (!match)
8
+ return null;
9
+ const octets = match.slice(1).map(Number);
10
+ return octets.every((octet) => octet <= 255) ? octets : null;
11
+ }
12
+ // Expands an IPv6 literal (no brackets) into its eight 16-bit groups, or null when malformed.
13
+ function parseIPv6(text) {
14
+ let literal = text;
15
+ const lastColon = literal.lastIndexOf(':');
16
+ if (literal.includes('.')) {
17
+ const v4 = parseIPv4(literal.slice(lastColon + 1));
18
+ if (!v4)
19
+ return null;
20
+ const high = ((v4[0] << 8) | v4[1]).toString(16);
21
+ const low = ((v4[2] << 8) | v4[3]).toString(16);
22
+ literal = `${literal.slice(0, lastColon + 1)}${high}:${low}`;
23
+ }
24
+ const halves = literal.split('::');
25
+ if (halves.length > 2)
26
+ return null;
27
+ const split = (half) => (half === '' ? [] : half.split(':'));
28
+ const head = split(halves[0]);
29
+ const tail = halves.length === 2 ? split(halves[1]) : [];
30
+ const compressed = halves.length === 2;
31
+ if (compressed ? head.length + tail.length > 7 : head.length !== 8)
32
+ return null;
33
+ const groups = compressed
34
+ ? [...head, ...Array(8 - head.length - tail.length).fill('0'), ...tail]
35
+ : head;
36
+ if (!groups.every((group) => HEXTET_PATTERN.test(group)))
37
+ return null;
38
+ return groups.map((group) => Number.parseInt(group, 16));
39
+ }
40
+ function isPrivateIPv4([a, b]) {
41
+ return (a === 0 ||
42
+ a === 10 ||
43
+ (a === 100 && b >= 64 && b <= 127) ||
44
+ a === 127 ||
45
+ (a === 169 && b === 254) ||
46
+ (a === 172 && b >= 16 && b <= 31) ||
47
+ (a === 192 && b === 168));
48
+ }
49
+ // Groups g6 and g7 carry the embedded IPv4 address of the mapped, NAT64 and 6to4 forms.
50
+ function embeddedIPv4(high, low) {
51
+ return [high >> 8, high & 0xff, low >> 8, low & 0xff];
52
+ }
53
+ function isPrivateIPv6(groups) {
54
+ const [g0, g1, g2, g3, g4, g5, g6, g7] = groups;
55
+ const leadingZeros = (count) => groups.slice(0, count).every((group) => group === 0);
56
+ // ::/96 covers the unspecified address, loopback and the deprecated IPv4-compatible form.
57
+ if (leadingZeros(6))
58
+ return true;
59
+ // ::ffff:0:0/96 (IPv4-mapped) is the same host as the IPv4 address it wraps.
60
+ if (leadingZeros(5) && g5 === 0xffff)
61
+ return isPrivateIPv4(embeddedIPv4(g6, g7));
62
+ // 64:ff9b::/96 (NAT64) translates to the embedded IPv4 address.
63
+ if (g0 === 0x64 && g1 === 0xff9b && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0) {
64
+ return isPrivateIPv4(embeddedIPv4(g6, g7));
65
+ }
66
+ // 2002::/16 (6to4) embeds the IPv4 address in the next 32 bits.
67
+ if (g0 === 0x2002)
68
+ return isPrivateIPv4(embeddedIPv4(g1, g2));
69
+ // fc00::/7 unique local, fe80::/10 link-local.
70
+ return (g0 & 0xfe00) === 0xfc00 || (g0 & 0xffc0) === 0xfe80;
71
+ }
72
+ /**
73
+ * Whether a webhook target host is loopback, link-local, private or otherwise internal.
74
+ * Expects `URL.hostname`, which the WHATWG parser already normalises: IPv6 literals stay bracketed
75
+ * and IPv4-mapped tails are rewritten to hex, so the address is parsed, never string-matched.
76
+ */
4
77
  export function isPrivateHost(hostname) {
5
- return PRIVATE_HOST_REGEX.test(hostname);
78
+ const host = hostname.toLowerCase().replace(/\.$/, '');
79
+ if (host === 'localhost' || host.endsWith('.localhost'))
80
+ return true;
81
+ if (host.startsWith('[') && host.endsWith(']')) {
82
+ const groups = parseIPv6(host.slice(1, -1));
83
+ // An unparseable literal is rejected rather than trusted.
84
+ return groups === null || isPrivateIPv6(groups);
85
+ }
86
+ const octets = parseIPv4(host);
87
+ return octets !== null && isPrivateIPv4(octets);
6
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/core",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",