@open-agent-toolkit/cli 0.2.30 → 0.2.31
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +41 -0
- package/assets/docs/contributing/code.md +19 -7
- package/assets/docs/contributing/explainer-kit-verification.md +9 -1
- package/assets/docs/contributing/skills.md +9 -0
- package/assets/docs/workflows/skills/explainer-kit-providers.md +16 -5
- package/assets/docs/workflows/skills/explainer-kit.md +98 -38
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +33 -8
- package/assets/skills/explainer-kit/briefs/project-recap.md +25 -7
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +72 -0
- package/assets/skills/explainer-kit/references/contracts.md +49 -17
- package/assets/skills/explainer-kit/references/destination-contract.md +141 -25
- package/assets/skills/explainer-kit/references/extension-contract.md +19 -10
- package/assets/skills/explainer-kit/references/visual-authoring.md +24 -0
- package/assets/skills/explainer-kit/references/visual-review.md +19 -5
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +241 -0
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +215 -0
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +34 -0
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -1
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +81 -0
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +66 -0
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +109 -3
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +550 -17
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +90 -8
- package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +5 -0
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +538 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +129 -11
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +54 -0
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +2 -1
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +139 -22
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +353 -0
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +237 -107
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +1 -0
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +157 -0
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +26 -6
- package/assets/skills/explainer-kit/scripts/run.mjs +1006 -144
- package/assets/skills/oat-explainer-kit/SKILL.md +16 -3
- package/assets/skills/oat-explainer-kit/references/config-contract.md +13 -8
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +50 -6
- package/assets/skills/oat-explainer-kit/references/migration.md +2 -1
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +11 -0
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +37 -15
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +83 -0
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +91 -0
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +66 -10
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +60 -21
- package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +52 -8
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +271 -36
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
- package/assets/skills/oat-project-complete/SKILL.md +19 -3
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +8 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +18 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +57 -7
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +29 -0
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-terminal-evidence.js +37 -0
- package/dist/config/oat-config.d.ts +2 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +4 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +1 -0
- package/package.json +2 -2
- /package/assets/skills/explainer-kit/recipes/{project-recap.json → project-recap.v1.json} +0 -0
- /package/assets/skills/explainer-kit/schemas/{publish-receipt.schema.json → publish-receipt.v1.schema.json} +0 -0
- /package/assets/skills/explainer-kit/schemas/{publish-request.schema.json → publish-request.v1.schema.json} +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const BUCKET_PATTERN = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
2
|
+
const IPV4_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
3
|
+
const RESERVED_BUCKET_PREFIXES = ['xn--', 'sthree-', 'amzn_s3_demo_'];
|
|
4
|
+
const RESERVED_BUCKET_SUFFIXES = [
|
|
5
|
+
'-s3alias',
|
|
6
|
+
'--ol-s3',
|
|
7
|
+
'.mrap',
|
|
8
|
+
'--x-s3',
|
|
9
|
+
'--table-s3',
|
|
10
|
+
];
|
|
11
|
+
const ENCODED_DOT_OR_SEPARATOR_PATTERN = /%(?:2e|2f|5c)/i;
|
|
12
|
+
|
|
13
|
+
export function normalizePublishRoots(s3Uri, publicBaseUrl) {
|
|
14
|
+
const s3 = parseS3Root(s3Uri);
|
|
15
|
+
const publicRoot = parsePublicRoot(publicBaseUrl);
|
|
16
|
+
const keyPrefix = s3.segments.join('/');
|
|
17
|
+
const pathname = publicRoot.segments.join('/');
|
|
18
|
+
// Deliberately NO relational rule between the two roots. The mapping from an
|
|
19
|
+
// S3 key to a public URL is underdetermined by these two strings: it lives in
|
|
20
|
+
// CDN configuration this tool cannot read. Both of these are legitimate:
|
|
21
|
+
//
|
|
22
|
+
// A s3://bucket/repositories/alpha + https://host/repositories/alpha
|
|
23
|
+
// B s3://bucket/explainers + https://host (CloudFront Origin Path)
|
|
24
|
+
//
|
|
25
|
+
// B is a confirmed real deployment shape, recorded in oat-explainer-kit's
|
|
26
|
+
// migration reference. Suffix-containment does not rescue the rule either --
|
|
27
|
+
// an empty public path is a suffix of everything, so B would pass vacuously
|
|
28
|
+
// while path-rewriting deployments still false-reject. Divergence is surfaced
|
|
29
|
+
// as a non-blocking warning by the connector instead; correctness of the
|
|
30
|
+
// advertised URL is established by verification, not by string shape.
|
|
31
|
+
return {
|
|
32
|
+
bucket: s3.bucket,
|
|
33
|
+
keyPrefix,
|
|
34
|
+
s3Uri: `s3://${s3.bucket}${keyPrefix ? `/${keyPrefix}` : ''}`,
|
|
35
|
+
publicBaseUrl: `https://${publicRoot.authority}${pathname ? `/${pathname}` : ''}`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const ROOT_DIVERGENCE_WARNING_ENV =
|
|
40
|
+
'EXPLAINER_KIT_SUPPRESS_ROOT_DIVERGENCE_WARNING';
|
|
41
|
+
|
|
42
|
+
export function rootDivergenceWarningSuppressed(env = process.env) {
|
|
43
|
+
return ['on', 'true', '1'].includes(
|
|
44
|
+
String(env[ROOT_DIVERGENCE_WARNING_ENV] ?? '').toLowerCase(),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Advisory only, never a gate. A mismatched key prefix and public path is
|
|
50
|
+
* usually a typo, but it is also exactly what a CloudFront Origin Path
|
|
51
|
+
* deployment looks like, so this can only ever be a warning. Returns null when
|
|
52
|
+
* the two roots address the same path.
|
|
53
|
+
*/
|
|
54
|
+
export function describeRootDivergence(roots) {
|
|
55
|
+
const keyPrefix = roots?.keyPrefix ?? '';
|
|
56
|
+
let pathname;
|
|
57
|
+
try {
|
|
58
|
+
pathname = new URL(roots?.publicBaseUrl).pathname.replace(/^\/|\/$/g, '');
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (keyPrefix === pathname) return null;
|
|
63
|
+
return (
|
|
64
|
+
`Publication roots address different paths: S3 key prefix "${keyPrefix}" ` +
|
|
65
|
+
`vs public path "${pathname}". This is expected for a CloudFront Origin ` +
|
|
66
|
+
`Path deployment; verify it is not a typo.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function composePublicationTarget(relativePath, roots) {
|
|
71
|
+
const normalized = normalizePublishRoots(roots?.s3Uri, roots?.publicBaseUrl);
|
|
72
|
+
const segments = parseRelativePath(relativePath);
|
|
73
|
+
const rawSuffix = segments.join('/');
|
|
74
|
+
const publicSuffix = segments
|
|
75
|
+
.map((part) => encodeURIComponent(part))
|
|
76
|
+
.join('/');
|
|
77
|
+
return {
|
|
78
|
+
s3Uri: `${normalized.s3Uri}/${rawSuffix}`,
|
|
79
|
+
publicUrl: `${normalized.publicBaseUrl}/${publicSuffix}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseS3Root(value) {
|
|
84
|
+
if (
|
|
85
|
+
typeof value !== 'string' ||
|
|
86
|
+
/\s|[\\?#]/.test(value) ||
|
|
87
|
+
hasUnsafeRootChar(value) ||
|
|
88
|
+
!value.startsWith('s3://')
|
|
89
|
+
) {
|
|
90
|
+
throw rootError('S3 root must be a credential-free s3 URI.');
|
|
91
|
+
}
|
|
92
|
+
const match = /^s3:\/\/([^/]+)(?:\/(.*))?$/.exec(value);
|
|
93
|
+
if (!match) {
|
|
94
|
+
throw rootError('S3 root must be a credential-free s3 URI.');
|
|
95
|
+
}
|
|
96
|
+
const [, bucket, rawPath = ''] = match;
|
|
97
|
+
if (!validBucket(bucket)) {
|
|
98
|
+
throw rootError('S3 root has an invalid bucket.');
|
|
99
|
+
}
|
|
100
|
+
return { bucket, segments: parseRootPath(rawPath) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parsePublicRoot(value) {
|
|
104
|
+
if (
|
|
105
|
+
typeof value !== 'string' ||
|
|
106
|
+
/\s|[?#]/.test(value) ||
|
|
107
|
+
hasUnsafeRootChar(value) ||
|
|
108
|
+
!value.startsWith('https://')
|
|
109
|
+
) {
|
|
110
|
+
throw rootError('Public root must be credential-free HTTPS.');
|
|
111
|
+
}
|
|
112
|
+
const match = /^https:\/\/([^/]+)(?:\/(.*))?$/.exec(value);
|
|
113
|
+
if (!match || match[1].includes('%') || match[1].includes('@')) {
|
|
114
|
+
throw rootError('Public root must be credential-free HTTPS.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = new URL(value);
|
|
120
|
+
} catch {
|
|
121
|
+
throw rootError('Public root must be credential-free HTTPS.');
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
parsed.protocol !== 'https:' ||
|
|
125
|
+
parsed.username ||
|
|
126
|
+
parsed.password ||
|
|
127
|
+
parsed.search ||
|
|
128
|
+
parsed.hash ||
|
|
129
|
+
!parsed.hostname
|
|
130
|
+
) {
|
|
131
|
+
throw rootError('Public root must be credential-free HTTPS.');
|
|
132
|
+
}
|
|
133
|
+
if (isPrivatePublicHost(parsed.hostname) && !privatePublicRootAllowed()) {
|
|
134
|
+
throw rootError(
|
|
135
|
+
'Public root must not address a loopback, link-local, or private network.',
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
authority: parsed.host,
|
|
140
|
+
segments: parseRootPath(match[2] ?? ''),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseRootPath(rawPath) {
|
|
145
|
+
if (rawPath === '') return [];
|
|
146
|
+
const withoutTrailingSlash = rawPath.endsWith('/')
|
|
147
|
+
? rawPath.slice(0, -1)
|
|
148
|
+
: rawPath;
|
|
149
|
+
if (withoutTrailingSlash === '' || withoutTrailingSlash.includes('//')) {
|
|
150
|
+
throw rootError('Publication root contains an unsafe path.');
|
|
151
|
+
}
|
|
152
|
+
const segments = withoutTrailingSlash.split('/');
|
|
153
|
+
if (segments.some((segment) => !validRootSegment(segment))) {
|
|
154
|
+
throw rootError('Publication root contains an unsafe path.');
|
|
155
|
+
}
|
|
156
|
+
return segments;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseRelativePath(value) {
|
|
160
|
+
if (
|
|
161
|
+
typeof value !== 'string' ||
|
|
162
|
+
value === '' ||
|
|
163
|
+
value.startsWith('/') ||
|
|
164
|
+
value.endsWith('/') ||
|
|
165
|
+
value.includes('//') ||
|
|
166
|
+
/[\\?#]/.test(value) ||
|
|
167
|
+
ENCODED_DOT_OR_SEPARATOR_PATTERN.test(value)
|
|
168
|
+
) {
|
|
169
|
+
throw rootError('Publication target contains an unsafe path.');
|
|
170
|
+
}
|
|
171
|
+
const segments = value.split('/');
|
|
172
|
+
if (segments.some((segment) => segment === '.' || segment === '..')) {
|
|
173
|
+
throw rootError('Publication target contains an unsafe path.');
|
|
174
|
+
}
|
|
175
|
+
return segments;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validRootSegment(segment) {
|
|
179
|
+
if (!segment || segment === '.' || segment === '..') return false;
|
|
180
|
+
let decoded;
|
|
181
|
+
try {
|
|
182
|
+
decoded = decodeURIComponent(segment);
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return (
|
|
187
|
+
decoded !== '.' &&
|
|
188
|
+
decoded !== '..' &&
|
|
189
|
+
!decoded.includes('/') &&
|
|
190
|
+
!decoded.includes('\\') &&
|
|
191
|
+
!ENCODED_DOT_OR_SEPARATOR_PATTERN.test(decoded)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validBucket(bucket) {
|
|
196
|
+
return (
|
|
197
|
+
BUCKET_PATTERN.test(bucket) &&
|
|
198
|
+
!bucket.includes('..') &&
|
|
199
|
+
!bucket.includes('.-') &&
|
|
200
|
+
!bucket.includes('-.') &&
|
|
201
|
+
!IPV4_PATTERN.test(bucket) &&
|
|
202
|
+
!RESERVED_BUCKET_PREFIXES.some((prefix) => bucket.startsWith(prefix)) &&
|
|
203
|
+
!RESERVED_BUCKET_SUFFIXES.some((suffix) => bucket.endsWith(suffix))
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// `\s` matches only space, tab, newline, CR, FF and VT, leaving the rest of the
|
|
208
|
+
// C0 range and DEL free to reach S3 object keys, composed public URLs, the
|
|
209
|
+
// catalog, receipts and terminal output. NUL additionally crashed `execFile`
|
|
210
|
+
// with an uncoded `ERR_INVALID_ARG_VALUE` rather than a clean `E_PUBLISH_ROOTS`.
|
|
211
|
+
// Backslash is screened here so both parsers stay symmetric: `parseS3Root`
|
|
212
|
+
// already rejected it, but `parsePublicRoot` silently dropped the segment.
|
|
213
|
+
// The C1 range `0x80`-`0x9f` is screened for the same reason as C0: it was
|
|
214
|
+
// retained verbatim in both normalized roots and flowed into the S3 key, the
|
|
215
|
+
// composed public URL, the catalog, the receipt and `aws` argv. `0x9b` is the
|
|
216
|
+
// 8-bit CSI, a functional terminal control introducer, so leaving it accepted
|
|
217
|
+
// contradicted this project's no-uncontrolled-bytes-in-output posture. No
|
|
218
|
+
// legitimate root contains a raw C1 byte; percent-encoded forms are unaffected
|
|
219
|
+
// because the screen runs on the raw string before any decoding.
|
|
220
|
+
//
|
|
221
|
+
// Expressed as a codepoint scan rather than a regex so the source carries no
|
|
222
|
+
// literal control bytes.
|
|
223
|
+
function hasUnsafeRootChar(value) {
|
|
224
|
+
for (const char of value) {
|
|
225
|
+
const code = char.codePointAt(0);
|
|
226
|
+
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || char === '\\') {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Operators publishing to a genuinely internal mirror may opt back in. The
|
|
235
|
+
* default is deny: public verification issues an outbound GET against whatever
|
|
236
|
+
* this root names, so an unconstrained root is an attacker-influenced request
|
|
237
|
+
* primitive aimed at internal addresses (including the `169.254.169.254` IMDS
|
|
238
|
+
* endpoint), and pass/fail timing discloses internal reachability.
|
|
239
|
+
*/
|
|
240
|
+
export const PRIVATE_PUBLIC_ROOT_ENV =
|
|
241
|
+
'EXPLAINER_KIT_ALLOW_PRIVATE_PUBLIC_ROOT';
|
|
242
|
+
|
|
243
|
+
export function privatePublicRootAllowed(env = process.env) {
|
|
244
|
+
return ['on', 'true', '1'].includes(
|
|
245
|
+
String(env[PRIVATE_PUBLIC_ROOT_ENV] ?? '').toLowerCase(),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Literal-address policy only. Resolving names would be both TOCTOU-prone and
|
|
251
|
+
* dependent on the resolver of whoever runs the publish, so a name that happens
|
|
252
|
+
* to resolve inward is deliberately out of scope here.
|
|
253
|
+
*/
|
|
254
|
+
export function isPrivatePublicHost(hostname) {
|
|
255
|
+
const host = String(hostname).toLowerCase();
|
|
256
|
+
if (host === 'localhost' || host.endsWith('.localhost')) return true;
|
|
257
|
+
|
|
258
|
+
if (host.startsWith('[') && host.endsWith(']')) {
|
|
259
|
+
return isPrivateIpv6(host.slice(1, -1));
|
|
260
|
+
}
|
|
261
|
+
if (host.includes(':')) return isPrivateIpv6(host);
|
|
262
|
+
if (IPV4_PATTERN.test(host)) return isPrivateIpv4(host);
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isPrivateIpv4(host) {
|
|
267
|
+
const octets = host.split('.').map(Number);
|
|
268
|
+
if (octets.some((octet) => !Number.isInteger(octet) || octet > 255)) {
|
|
269
|
+
// Not a valid dotted quad; leave it to the surrounding host checks.
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const [a, b] = octets;
|
|
273
|
+
return (
|
|
274
|
+
a === 0 || // 0.0.0.0/8 "this network"
|
|
275
|
+
a === 127 || // loopback
|
|
276
|
+
a === 10 || // RFC 1918
|
|
277
|
+
(a === 172 && b >= 16 && b <= 31) || // RFC 1918
|
|
278
|
+
(a === 192 && b === 168) || // RFC 1918
|
|
279
|
+
(a === 169 && b === 254) // link-local, incl. the IMDS address
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isPrivateIpv6(address) {
|
|
284
|
+
const groups = expandIpv6(address.split('%')[0].toLowerCase());
|
|
285
|
+
if (!groups) return false;
|
|
286
|
+
|
|
287
|
+
// IPv4-mapped (`::ffff:a.b.c.d`) must not launder a private v4 address. The
|
|
288
|
+
// WHATWG URL parser rewrites the dotted form to hex, so `[::ffff:127.0.0.1]`
|
|
289
|
+
// arrives here as `::ffff:7f00:1` and has to be recognised in that shape.
|
|
290
|
+
if (groups.slice(0, 5).every((group) => group === 0)) {
|
|
291
|
+
if (groups[5] === 0xffff || groups[5] === 0) {
|
|
292
|
+
const [, , , , , , seven, eight] = groups;
|
|
293
|
+
const asV4 = [seven >> 8, seven & 0xff, eight >> 8, eight & 0xff].join(
|
|
294
|
+
'.',
|
|
295
|
+
);
|
|
296
|
+
if (groups[5] === 0xffff) return isPrivateIpv4(asV4);
|
|
297
|
+
// `::` unspecified and `::1` loopback.
|
|
298
|
+
if (seven === 0 && (eight === 0 || eight === 1)) return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
(groups[0] >= 0xfe80 && groups[0] <= 0xfebf) || // fe80::/10 link-local
|
|
304
|
+
(groups[0] >= 0xfc00 && groups[0] <= 0xfdff) // fc00::/7 unique-local
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Expand an IPv6 literal to exactly eight numeric groups, or null. */
|
|
309
|
+
function expandIpv6(address) {
|
|
310
|
+
if (!address.includes(':')) return null;
|
|
311
|
+
const halves = address.split('::');
|
|
312
|
+
if (halves.length > 2) return null;
|
|
313
|
+
|
|
314
|
+
const parse = (part) =>
|
|
315
|
+
part === ''
|
|
316
|
+
? []
|
|
317
|
+
: part.split(':').map((group) => Number.parseInt(group, 16));
|
|
318
|
+
|
|
319
|
+
// A trailing dotted quad (`::ffff:1.2.3.4`) contributes two groups.
|
|
320
|
+
const dotted = /(\d{1,3}(?:\.\d{1,3}){3})$/.exec(address);
|
|
321
|
+
let tail = [];
|
|
322
|
+
let working = address;
|
|
323
|
+
if (dotted) {
|
|
324
|
+
const octets = dotted[1].split('.').map(Number);
|
|
325
|
+
if (octets.some((octet) => !Number.isInteger(octet) || octet > 255)) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
tail = [(octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]];
|
|
329
|
+
working = address.slice(0, address.length - dotted[1].length);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const [left, right] = working.split('::');
|
|
333
|
+
const head = parse(left.replace(/:$/, ''));
|
|
334
|
+
const rest =
|
|
335
|
+
right === undefined ? [] : parse(right.replace(/:$/, '').replace(/^:/, ''));
|
|
336
|
+
const explicit = [...head, ...rest, ...tail];
|
|
337
|
+
if (explicit.some((group) => Number.isNaN(group) || group > 0xffff)) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (right === undefined) return explicit.length === 8 ? explicit : null;
|
|
342
|
+
if (explicit.length > 8) return null;
|
|
343
|
+
return [
|
|
344
|
+
...head,
|
|
345
|
+
...Array.from({ length: 8 - explicit.length }, () => 0),
|
|
346
|
+
...rest,
|
|
347
|
+
...tail,
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function rootError(message) {
|
|
352
|
+
return Object.assign(new Error(message), { code: 'E_PUBLISH_ROOTS' });
|
|
353
|
+
}
|