@beechcms/core 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content/content-lock.repository.d.ts +63 -0
- package/dist/content/content-lock.repository.d.ts.map +1 -0
- package/dist/content/content-lock.repository.js +15 -0
- package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts +19 -0
- package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts.map +1 -0
- package/dist/dashboard-layout/kanban/kanban-position.repository.js +3 -0
- package/dist/engine/ddl/seed-ddl-destructive.d.ts +40 -0
- package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +1 -0
- package/dist/engine/ddl/seed-ddl-destructive.js +149 -0
- package/dist/engine/ddl.d.ts +216 -0
- package/dist/engine/ddl.d.ts.map +1 -0
- package/dist/engine/ddl.js +574 -0
- package/dist/engine/define-seed.d.ts +3 -0
- package/dist/engine/define-seed.d.ts.map +1 -0
- package/dist/engine/define-seed.js +5 -0
- package/dist/engine/introspection.d.ts +101 -0
- package/dist/engine/introspection.d.ts.map +1 -0
- package/dist/engine/introspection.js +127 -0
- package/dist/engine/policies.d.ts +58 -0
- package/dist/engine/policies.d.ts.map +1 -0
- package/dist/engine/policies.js +168 -0
- package/dist/engine/privacy.service.d.ts +65 -0
- package/dist/engine/privacy.service.d.ts.map +1 -0
- package/dist/engine/privacy.service.js +114 -0
- package/dist/engine/query/relations.d.ts +15 -0
- package/dist/engine/query/relations.d.ts.map +1 -1
- package/dist/engine/query.d.ts +23 -0
- package/dist/engine/query.d.ts.map +1 -0
- package/dist/engine/query.js +248 -0
- package/dist/engine/relations.d.ts +28 -0
- package/dist/engine/relations.d.ts.map +1 -0
- package/dist/engine/relations.js +25 -0
- package/dist/engine/schema-fingerprint.d.ts +71 -0
- package/dist/engine/schema-fingerprint.d.ts.map +1 -0
- package/dist/engine/schema-fingerprint.js +95 -0
- package/dist/engine/schema-mutator.d.ts +26 -0
- package/dist/engine/schema-mutator.d.ts.map +1 -0
- package/dist/engine/schema-mutator.js +3 -0
- package/dist/engine/seed-ddl-destructive.d.ts +40 -0
- package/dist/engine/seed-ddl-destructive.d.ts.map +1 -0
- package/dist/engine/seed-ddl-destructive.js +149 -0
- package/dist/engine/seed-ddl.d.ts +43 -0
- package/dist/engine/seed-ddl.d.ts.map +1 -0
- package/dist/engine/seed-ddl.js +115 -0
- package/dist/engine/seed-registry.d.ts +71 -0
- package/dist/engine/seed-registry.d.ts.map +1 -0
- package/dist/engine/seed-registry.js +167 -0
- package/dist/engine/seed-types-generator.d.ts +25 -0
- package/dist/engine/seed-types-generator.d.ts.map +1 -0
- package/dist/engine/seed-types-generator.js +117 -0
- package/dist/engine/seed-validation.d.ts +30 -0
- package/dist/engine/seed-validation.d.ts.map +1 -0
- package/dist/engine/seed-validation.js +329 -0
- package/dist/engine/seeds.d.ts +20 -0
- package/dist/engine/seeds.d.ts.map +1 -0
- package/dist/engine/seeds.js +21 -0
- package/dist/engine/serialize.d.ts +20 -0
- package/dist/engine/serialize.d.ts.map +1 -0
- package/dist/engine/serialize.js +160 -0
- package/dist/seed-types-generator.d.ts +10 -0
- package/dist/seed-types-generator.d.ts.map +1 -0
- package/dist/seed-types-generator.js +86 -0
- package/dist/webhooks/webhook-validation.d.ts +5 -1
- package/dist/webhooks/webhook-validation.d.ts.map +1 -1
- package/dist/webhooks/webhook-validation.js +84 -2
- package/package.json +2 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a branch represents a multiple asset list field.
|
|
3
|
+
*
|
|
4
|
+
* @param branch The branch definition.
|
|
5
|
+
* @returns True if it's an asset list, false otherwise.
|
|
6
|
+
*/
|
|
7
|
+
function isAssetListBranch(branch) {
|
|
8
|
+
return branch.type === 'file' && (branch.multiple === true || branch.format === 'asset-list');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes an unknown value to a valid HTTP URL string, or null.
|
|
12
|
+
*
|
|
13
|
+
* @param value The value to normalize.
|
|
14
|
+
* @returns The normalized URL string, or null if invalid.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeHttpUrl(value) {
|
|
17
|
+
if (typeof value !== 'string')
|
|
18
|
+
return null;
|
|
19
|
+
const cleaned = value.trim();
|
|
20
|
+
if (!cleaned)
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = new URL(cleaned);
|
|
24
|
+
return parsed.protocol.startsWith('http') ? cleaned : null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Safely parses a JSON string, falling back to the raw string on error.
|
|
32
|
+
*
|
|
33
|
+
* @param value The string to parse.
|
|
34
|
+
* @returns The parsed object, or the original string.
|
|
35
|
+
*/
|
|
36
|
+
function parseJsonSafe(value) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Normalizes raw asset list values to a flat list of valid URL strings.
|
|
46
|
+
*
|
|
47
|
+
* @param rawValue The raw value (string, array, or object) to normalize.
|
|
48
|
+
* @returns An array of unique normalized URL strings.
|
|
49
|
+
*/
|
|
50
|
+
function normalizeAssetListValue(rawValue) {
|
|
51
|
+
const input = typeof rawValue === 'string' ? parseJsonSafe(rawValue) : rawValue;
|
|
52
|
+
const values = Array.isArray(input) ? input : [input];
|
|
53
|
+
const normalized = [];
|
|
54
|
+
for (const item of values) {
|
|
55
|
+
if (item == null)
|
|
56
|
+
continue;
|
|
57
|
+
const direct = normalizeHttpUrl(item);
|
|
58
|
+
if (direct) {
|
|
59
|
+
normalized.push(direct);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (typeof item === 'object' && !Array.isArray(item)) {
|
|
63
|
+
const fromObj = normalizeHttpUrl(item.url);
|
|
64
|
+
if (fromObj)
|
|
65
|
+
normalized.push(fromObj);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...new Set(normalized)];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Serializes a value for writing to the DB.
|
|
72
|
+
* boolean → 0/1 | date → Unix timestamp | json/tags/richtext/repeater → JSON string
|
|
73
|
+
*
|
|
74
|
+
* @param branch The branch definition.
|
|
75
|
+
* @param value The value to serialize.
|
|
76
|
+
* @returns The serialized DB value (string, number, or null).
|
|
77
|
+
*/
|
|
78
|
+
export function serializeForDb(branch, value) {
|
|
79
|
+
if (value === null || value === undefined)
|
|
80
|
+
return null;
|
|
81
|
+
switch (branch.type) {
|
|
82
|
+
case 'boolean':
|
|
83
|
+
return value ? 1 : 0;
|
|
84
|
+
case 'json':
|
|
85
|
+
case 'tags':
|
|
86
|
+
case 'richtext':
|
|
87
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
88
|
+
case 'date': {
|
|
89
|
+
if (typeof value === 'number')
|
|
90
|
+
return value;
|
|
91
|
+
if (typeof value === 'string') {
|
|
92
|
+
const d = new Date(value);
|
|
93
|
+
if (Number.isNaN(d.getTime()))
|
|
94
|
+
return null;
|
|
95
|
+
if (branch.format === 'date') {
|
|
96
|
+
const midnight = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
97
|
+
return Math.floor(midnight / 1000);
|
|
98
|
+
}
|
|
99
|
+
return Math.floor(d.getTime() / 1000);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
case 'file':
|
|
104
|
+
if (isAssetListBranch(branch)) {
|
|
105
|
+
return Array.isArray(value) ? JSON.stringify(value) : typeof value === 'string' ? value : null;
|
|
106
|
+
}
|
|
107
|
+
return typeof value === 'string' ? value : null;
|
|
108
|
+
case 'repeater':
|
|
109
|
+
return JSON.stringify(Array.isArray(value) ? value : []);
|
|
110
|
+
default:
|
|
111
|
+
return typeof value === 'string' ? value : typeof value === 'number' ? value : null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Deserializes a value read from the DB to its API/JS representation.
|
|
116
|
+
* 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object/array
|
|
117
|
+
*
|
|
118
|
+
* @param branch The branch definition.
|
|
119
|
+
* @param value The raw database value.
|
|
120
|
+
* @returns The deserialized value.
|
|
121
|
+
*/
|
|
122
|
+
export function deserializeFromDb(branch, value) {
|
|
123
|
+
if (branch.type === 'repeater') {
|
|
124
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
125
|
+
return [];
|
|
126
|
+
const parsed = parseJsonSafe(value);
|
|
127
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
128
|
+
}
|
|
129
|
+
if (value === null || value === undefined)
|
|
130
|
+
return null;
|
|
131
|
+
switch (branch.type) {
|
|
132
|
+
case 'boolean':
|
|
133
|
+
return value === 1 || value === true;
|
|
134
|
+
case 'json':
|
|
135
|
+
case 'tags':
|
|
136
|
+
case 'richtext': {
|
|
137
|
+
if (typeof value === 'string') {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(value);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
case 'date': {
|
|
148
|
+
if (typeof value !== 'number')
|
|
149
|
+
return null;
|
|
150
|
+
const d = new Date(value * 1000);
|
|
151
|
+
return branch.format === 'date' ? d.toISOString().slice(0, 10) : d.toISOString();
|
|
152
|
+
}
|
|
153
|
+
case 'file':
|
|
154
|
+
if (isAssetListBranch(branch))
|
|
155
|
+
return normalizeAssetListValue(value);
|
|
156
|
+
return typeof value === 'string' ? value : null;
|
|
157
|
+
default:
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Seed, Branch } from './engine/types.js';
|
|
2
|
+
/** slug/alias → PascalCase identifier. 'blog-posts' → 'BlogPosts'. */
|
|
3
|
+
export declare function pascalCase(input: string): string;
|
|
4
|
+
/** Maps a single Branch to its TypeScript type expression (no optional marker). */
|
|
5
|
+
export declare function tsTypeForBranch(branch: Branch): string;
|
|
6
|
+
/** Emits one exported interface for a single Seed. */
|
|
7
|
+
export declare function interfaceForSeed(seed: Seed): string;
|
|
8
|
+
/** Pure entry point. Deterministic: sorts seeds by slug for stable diffs. */
|
|
9
|
+
export declare function generateSeedTypes(seeds: Seed[]): string;
|
|
10
|
+
//# sourceMappingURL=seed-types-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-types-generator.d.ts","sourceRoot":"","sources":["../src/seed-types-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAerD,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhD;AAMD,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAoCtD;AAWD,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAYnD;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CASvD"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
const HEADER = '// Questo file è generato automaticamente da BeechCMS CLI. Non modificarlo direttamente.\n';
|
|
4
|
+
/** System columns emitted by generateCreateTable — always present, never optional. */
|
|
5
|
+
const SYSTEM_FIELDS = ` id: string\n` +
|
|
6
|
+
` slug: string\n` +
|
|
7
|
+
` status: 'draft' | 'review' | 'published' | 'archived'\n`;
|
|
8
|
+
const SYSTEM_TIMESTAMPS = ` created_at: number\n` +
|
|
9
|
+
` updated_at: number\n`;
|
|
10
|
+
/** slug/alias → PascalCase identifier. 'blog-posts' → 'BlogPosts'. */
|
|
11
|
+
export function pascalCase(input) {
|
|
12
|
+
return input
|
|
13
|
+
.split(/[^A-Za-z0-9]+/)
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
16
|
+
.join('');
|
|
17
|
+
}
|
|
18
|
+
function literalUnion(options) {
|
|
19
|
+
return options.map(o => `'${o.replace(/'/g, "\\'")}'`).join(' | ');
|
|
20
|
+
}
|
|
21
|
+
/** Maps a single Branch to its TypeScript type expression (no optional marker). */
|
|
22
|
+
export function tsTypeForBranch(branch) {
|
|
23
|
+
switch (branch.type) {
|
|
24
|
+
case 'number':
|
|
25
|
+
case 'date':
|
|
26
|
+
return 'number';
|
|
27
|
+
case 'boolean':
|
|
28
|
+
return 'boolean';
|
|
29
|
+
case 'json':
|
|
30
|
+
return 'unknown';
|
|
31
|
+
case 'text':
|
|
32
|
+
case 'richtext':
|
|
33
|
+
case 'file': {
|
|
34
|
+
const base = branch.type === 'text' && branch.options?.length
|
|
35
|
+
? literalUnion(branch.options)
|
|
36
|
+
: 'string';
|
|
37
|
+
return branch.type === 'file' && branch.multiple ? `${base}[]` : base;
|
|
38
|
+
}
|
|
39
|
+
case 'tags': {
|
|
40
|
+
const base = branch.options?.length ? `(${literalUnion(branch.options)})` : 'string';
|
|
41
|
+
return `${base}[]`;
|
|
42
|
+
}
|
|
43
|
+
case 'relation':
|
|
44
|
+
return branch.multiple ? 'string[]' : 'string';
|
|
45
|
+
case 'repeater': {
|
|
46
|
+
const inner = (branch.fields ?? [])
|
|
47
|
+
.map(f => `${propName(f)}${optional(f)}: ${tsTypeForBranch(f)}`)
|
|
48
|
+
.join('; ');
|
|
49
|
+
return `Array<{ ${inner} }>`;
|
|
50
|
+
}
|
|
51
|
+
default: {
|
|
52
|
+
// Exhaustiveness guard — fails the build if BranchType gains a member.
|
|
53
|
+
const _never = branch.type;
|
|
54
|
+
return _never;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function optional(branch) {
|
|
59
|
+
return branch.requiredOnCreate ? '' : '?';
|
|
60
|
+
}
|
|
61
|
+
function propName(branch) {
|
|
62
|
+
// alias is a valid SQL column; quote only if not a plain identifier.
|
|
63
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(branch.alias) ? branch.alias : `'${branch.alias}'`;
|
|
64
|
+
}
|
|
65
|
+
/** Emits one exported interface for a single Seed. */
|
|
66
|
+
export function interfaceForSeed(seed) {
|
|
67
|
+
const name = pascalCase(seed.slug);
|
|
68
|
+
const props = seed.branches
|
|
69
|
+
.map(b => ` ${propName(b)}${optional(b)}: ${tsTypeForBranch(b)}`)
|
|
70
|
+
.join('\n');
|
|
71
|
+
return (`export interface ${name} {\n` +
|
|
72
|
+
SYSTEM_FIELDS +
|
|
73
|
+
(props ? props + '\n' : '') +
|
|
74
|
+
SYSTEM_TIMESTAMPS +
|
|
75
|
+
`}\n`);
|
|
76
|
+
}
|
|
77
|
+
/** Pure entry point. Deterministic: sorts seeds by slug for stable diffs. */
|
|
78
|
+
export function generateSeedTypes(seeds) {
|
|
79
|
+
const sorted = [...seeds].sort((a, b) => a.slug.localeCompare(b.slug));
|
|
80
|
+
const interfaces = sorted.map(interfaceForSeed).join('\n');
|
|
81
|
+
const registry = `export interface SeedRegistryTypes {\n` +
|
|
82
|
+
sorted.map(s => ` ${propName({ alias: s.slug })}: ${pascalCase(s.slug)}`).join('\n') +
|
|
83
|
+
(sorted.length ? '\n' : '') +
|
|
84
|
+
`}\n`;
|
|
85
|
+
return `${HEADER}\n${interfaces}\n${registry}`;
|
|
86
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"version": "0.9.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^26.5.1",
|
|
32
32
|
"@vitest/coverage-v8": "^5.0.0",
|
|
33
|
-
"vitest": "^5.0.
|
|
33
|
+
"vitest": "^5.0.5"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@tiptap/core": "^3.31.3",
|