@open-agent-toolkit/cli 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/assets/docs/cli-utilities/configuration.md +40 -0
  2. package/assets/docs/cli-utilities/tool-packs.md +31 -0
  3. package/assets/docs/workflows/projects/artifacts.md +27 -0
  4. package/assets/docs/workflows/skills/explainer-kit.md +125 -0
  5. package/assets/docs/workflows/skills/index.md +4 -0
  6. package/assets/migration/pjm-restructure.md +12 -0
  7. package/assets/public-package-versions.json +4 -4
  8. package/assets/skills/explainer-kit/SKILL.md +95 -0
  9. package/assets/skills/explainer-kit/examples/project-explainer/content.md +31 -0
  10. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +22 -0
  11. package/assets/skills/explainer-kit/examples/project-recap/content.md +34 -0
  12. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +22 -0
  13. package/assets/skills/explainer-kit/examples/theme-bundle.json +87 -0
  14. package/assets/skills/explainer-kit/palettes/ember.json +37 -0
  15. package/assets/skills/explainer-kit/palettes/forest.json +37 -0
  16. package/assets/skills/explainer-kit/palettes/neutral.json +37 -0
  17. package/assets/skills/explainer-kit/palettes/ocean.json +37 -0
  18. package/assets/skills/explainer-kit/palettes/violet.json +37 -0
  19. package/assets/skills/explainer-kit/profiles/clean.json +42 -0
  20. package/assets/skills/explainer-kit/profiles/editorial.json +42 -0
  21. package/assets/skills/explainer-kit/profiles/technical.json +39 -0
  22. package/assets/skills/explainer-kit/recipes/engineer-tour.json +33 -0
  23. package/assets/skills/explainer-kit/recipes/project-explainer.json +33 -0
  24. package/assets/skills/explainer-kit/recipes/project-recap.json +34 -0
  25. package/assets/skills/explainer-kit/references/contracts.md +101 -0
  26. package/assets/skills/explainer-kit/references/destination-contract.md +94 -0
  27. package/assets/skills/explainer-kit/references/extension-contract.md +71 -0
  28. package/assets/skills/explainer-kit/references/fact-base-contract.md +119 -0
  29. package/assets/skills/explainer-kit/schemas/build-record.schema.json +92 -0
  30. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +53 -0
  31. package/assets/skills/explainer-kit/schemas/fact-base.schema.json +129 -0
  32. package/assets/skills/explainer-kit/schemas/manifest.schema.json +193 -0
  33. package/assets/skills/explainer-kit/schemas/publish-receipt.schema.json +97 -0
  34. package/assets/skills/explainer-kit/schemas/publish-request.schema.json +32 -0
  35. package/assets/skills/explainer-kit/schemas/run-request.schema.json +132 -0
  36. package/assets/skills/explainer-kit/schemas/theme.schema.json +217 -0
  37. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +180 -0
  38. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +680 -0
  39. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +538 -0
  40. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +494 -0
  41. package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +218 -0
  42. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +512 -0
  43. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +289 -0
  44. package/assets/skills/explainer-kit/scripts/lib/records.mjs +231 -0
  45. package/assets/skills/explainer-kit/scripts/lib/render.mjs +485 -0
  46. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +587 -0
  47. package/assets/skills/explainer-kit/scripts/lib/safe-paths.mjs +96 -0
  48. package/assets/skills/explainer-kit/scripts/lib/theme.mjs +332 -0
  49. package/assets/skills/explainer-kit/scripts/publish.mjs +76 -0
  50. package/assets/skills/explainer-kit/scripts/record-durability.mjs +64 -0
  51. package/assets/skills/explainer-kit/scripts/render-qa.mjs +273 -0
  52. package/assets/skills/explainer-kit/scripts/run.mjs +778 -0
  53. package/assets/skills/explainer-kit/scripts/validate.mjs +59 -0
  54. package/assets/skills/explainer-kit/templates/deck-shell.html +233 -0
  55. package/assets/skills/explainer-kit/templates/diagram-shell.html +219 -0
  56. package/assets/skills/explainer-kit/templates/engineer-tour.html +273 -0
  57. package/assets/skills/explainer-kit/templates/house-style.html +218 -0
  58. package/assets/skills/oat-explainer-kit/SKILL.md +92 -0
  59. package/assets/skills/oat-explainer-kit/references/config-contract.md +67 -0
  60. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +143 -0
  61. package/assets/skills/oat-explainer-kit/references/migration.md +143 -0
  62. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +103 -0
  63. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +127 -0
  64. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +392 -0
  65. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +130 -0
  66. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +293 -0
  67. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +209 -0
  68. package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +166 -0
  69. package/assets/skills/oat-explainer-kit/scripts/run.mjs +284 -0
  70. package/assets/skills/oat-project-autonomous/SKILL.md +22 -1
  71. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +5 -1
  72. package/assets/skills/oat-project-complete/SKILL.md +166 -28
  73. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +5 -1
  74. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  75. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +17 -0
  76. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +5 -1
  77. package/assets/skills/oat-project-plan/SKILL.md +30 -1
  78. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +5 -1
  79. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +5 -1
  80. package/assets/skills/oat-project-summary/SKILL.md +20 -1
  81. package/assets/skills/oat-wave-execute/SKILL.md +1 -1
  82. package/dist/commands/config/index.d.ts.map +1 -1
  83. package/dist/commands/config/index.js +232 -1
  84. package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
  85. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  86. package/dist/commands/init/tools/shared/skill-manifest.js +2 -0
  87. package/dist/commands/project/archive/archive-utils.d.ts +12 -0
  88. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  89. package/dist/commands/project/archive/archive-utils.js +320 -8
  90. package/dist/commands/project/archive/index.d.ts.map +1 -1
  91. package/dist/commands/project/archive/index.js +1 -0
  92. package/dist/commands/project/archive/push-runner.d.ts +1 -0
  93. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  94. package/dist/commands/project/archive/push-runner.js +9 -2
  95. package/dist/config/oat-config.d.ts +26 -0
  96. package/dist/config/oat-config.d.ts.map +1 -1
  97. package/dist/config/oat-config.js +88 -0
  98. package/dist/config/resolve.d.ts.map +1 -1
  99. package/dist/config/resolve.js +18 -0
  100. package/dist/validation/project-state.d.ts +3 -0
  101. package/dist/validation/project-state.d.ts.map +1 -1
  102. package/dist/validation/project-state.js +91 -0
  103. package/package.json +2 -2
  104. /package/assets/skills/{oat-wave-execute/assets/program-recap.recipe.json → explainer-kit/recipes/program-recap.json} +0 -0
@@ -0,0 +1,587 @@
1
+ import { execFile as execFileCallback } from 'node:child_process';
2
+ import { createHash, randomBytes as nodeRandomBytes } from 'node:crypto';
3
+ import {
4
+ mkdtemp,
5
+ readFile,
6
+ realpath,
7
+ rename,
8
+ rm,
9
+ writeFile,
10
+ } from 'node:fs/promises';
11
+ import { tmpdir } from 'node:os';
12
+ import { basename, dirname, join, resolve, sep } from 'node:path';
13
+ import { promisify } from 'node:util';
14
+
15
+ import { validateContract } from './contracts.mjs';
16
+
17
+ const execFile = promisify(execFileCallback);
18
+ const SENTINEL_BODY = 'explainer-kit sentinel\n';
19
+ const TRANSIENT_PATTERN =
20
+ /(?:timeout|timed out|connection reset|temporar|throttl|slowdown|service unavailable|\b5\d\d\b)/i;
21
+ const AUTH_PATTERN =
22
+ /(?:credential|expired|sso session|access key|invalidclienttokenid|unrecognizedclient|token)/i;
23
+ const NOT_FOUND_PATTERN = /(?:\b404\b|not found|nosuchkey)/i;
24
+
25
+ export function normalizePublishRoots(s3Uri, publicBaseUrl) {
26
+ if (typeof s3Uri !== 'string' || !s3Uri.startsWith('s3://')) {
27
+ throw publishError('E_PUBLISH_ROOTS', 'S3 root must use s3://.');
28
+ }
29
+ const withoutScheme = s3Uri.slice('s3://'.length).replace(/\/+$/, '');
30
+ const slash = withoutScheme.indexOf('/');
31
+ const bucket = slash === -1 ? withoutScheme : withoutScheme.slice(0, slash);
32
+ const keyPrefix =
33
+ slash === -1
34
+ ? ''
35
+ : withoutScheme
36
+ .slice(slash + 1)
37
+ .split('/')
38
+ .filter(Boolean)
39
+ .join('/');
40
+ if (!bucket || /\s/.test(bucket)) {
41
+ throw publishError('E_PUBLISH_ROOTS', 'S3 root has an invalid bucket.');
42
+ }
43
+
44
+ let publicUrl;
45
+ try {
46
+ publicUrl = new URL(publicBaseUrl);
47
+ } catch {
48
+ throw publishError('E_PUBLISH_ROOTS', 'Public root must be a valid URL.');
49
+ }
50
+ if (
51
+ publicUrl.protocol !== 'https:' ||
52
+ publicUrl.username ||
53
+ publicUrl.password ||
54
+ publicUrl.search ||
55
+ publicUrl.hash
56
+ ) {
57
+ throw publishError(
58
+ 'E_PUBLISH_ROOTS',
59
+ 'Public root must be credential-free HTTPS without query or fragment.',
60
+ );
61
+ }
62
+ publicUrl.pathname = publicUrl.pathname.replace(/\/+$/, '');
63
+ const normalizedPublic = publicUrl.toString().replace(/\/$/, '');
64
+ const normalizedS3 = `s3://${bucket}${keyPrefix ? `/${keyPrefix}` : ''}`;
65
+ return {
66
+ bucket,
67
+ keyPrefix,
68
+ s3Uri: normalizedS3,
69
+ publicBaseUrl: normalizedPublic,
70
+ };
71
+ }
72
+
73
+ export function createSentinelRelativePath(
74
+ runId,
75
+ randomBytes = nodeRandomBytes,
76
+ ) {
77
+ const safeRunId =
78
+ String(runId)
79
+ .toLowerCase()
80
+ .replace(/[^a-z0-9]+/g, '-')
81
+ .replace(/^-|-$/g, '') || 'run';
82
+ const suffix = randomBytes(16).toString('hex');
83
+ if (!/^[a-f0-9]{32}$/.test(suffix)) {
84
+ throw publishError(
85
+ 'E_PUBLISH_INPUT',
86
+ 'Sentinel randomness must provide 128 bits.',
87
+ );
88
+ }
89
+ return `.explainer-kit-sentinel/${safeRunId}-${suffix}.txt`;
90
+ }
91
+
92
+ export async function publishS3Static(request, dependencies = {}) {
93
+ if (dependencies.approved !== true) {
94
+ throw publishError(
95
+ 'E_PUBLISH_APPROVAL',
96
+ 'Publishing requires explicit human approval.',
97
+ );
98
+ }
99
+
100
+ const requestValidation = validateContract('publish-request', request);
101
+ if (!requestValidation.valid) {
102
+ throw publishError(
103
+ 'E_PUBLISH_INPUT',
104
+ `Invalid publish request: ${requestValidation.errors[0].message}`,
105
+ );
106
+ }
107
+ const roots = normalizePublishRoots(request.s3Uri, request.publicBaseUrl);
108
+ const cwd = dependencies.cwd ?? process.cwd();
109
+ const siteRoot = resolve(cwd, request.siteRoot);
110
+ const manifestPath = resolve(cwd, request.manifestPath);
111
+ const manifest = await readJson(manifestPath, 'manifest');
112
+ const manifestValidation = validateContract('manifest', manifest);
113
+ if (!manifestValidation.valid) {
114
+ throw publishError(
115
+ 'E_PUBLISH_INPUT',
116
+ `Invalid manifest: ${manifestValidation.errors[0].message}`,
117
+ );
118
+ }
119
+ const artifacts = await collectArtifacts(manifest, siteRoot);
120
+ const command = dependencies.command ?? defaultCommand;
121
+ const httpGet = dependencies.httpGet ?? defaultHttpGet;
122
+ const randomBytes = dependencies.randomBytes ?? nodeRandomBytes;
123
+ const now = dependencies.now ?? (() => new Date().toISOString());
124
+ const sleep = dependencies.sleep ?? defaultSleep;
125
+ const awsOptions = {
126
+ region: request.awsRegion,
127
+ profile: request.awsProfile,
128
+ };
129
+ const sentinelRelativePath = createSentinelRelativePath(
130
+ manifest.runId,
131
+ randomBytes,
132
+ );
133
+ const sentinelTargetPath = targetPath(sentinelRelativePath, roots);
134
+ const sentinelDirectory = await mkdtemp(
135
+ join(tmpdir(), 'explainer-sentinel-'),
136
+ );
137
+ const sentinelBodyPath = join(sentinelDirectory, 'sentinel.txt');
138
+ let sentinelUploaded = false;
139
+ let sentinelDeleted = false;
140
+
141
+ try {
142
+ await writeFile(sentinelBodyPath, SENTINEL_BODY, 'utf8');
143
+ await runAws(
144
+ command,
145
+ putObjectArgs({
146
+ roots,
147
+ relativePath: sentinelRelativePath,
148
+ bodyPath: sentinelBodyPath,
149
+ contentType: 'text/plain; charset=utf-8',
150
+ cacheControl: 'no-store',
151
+ hash: fileHash(Buffer.from(SENTINEL_BODY)),
152
+ awsOptions,
153
+ }),
154
+ { sleep },
155
+ );
156
+ sentinelUploaded = true;
157
+ await runAws(
158
+ command,
159
+ headObjectArgs(roots, sentinelRelativePath, awsOptions),
160
+ { sleep },
161
+ );
162
+ const sentinelResponse = await httpGet(sentinelTargetPath.publicUrl);
163
+ if (
164
+ sentinelResponse.status < 200 ||
165
+ sentinelResponse.status > 299 ||
166
+ !responseBytes(sentinelResponse.body).equals(Buffer.from(SENTINEL_BODY))
167
+ ) {
168
+ throw publishError(
169
+ 'E_PUBLISH_ROOTS',
170
+ 'Public root did not serve the uploaded sentinel.',
171
+ );
172
+ }
173
+ await runAws(
174
+ command,
175
+ deleteObjectArgs(roots, sentinelRelativePath, awsOptions),
176
+ { sleep },
177
+ );
178
+ sentinelDeleted = true;
179
+
180
+ const receiptArtifacts = [];
181
+ for (const artifact of artifacts) {
182
+ const target = targetPath(artifact.publishPath, roots);
183
+ const metadata = await readExistingMetadata(
184
+ command,
185
+ headObjectArgs(roots, artifact.publishPath, awsOptions),
186
+ { sleep },
187
+ );
188
+ if (!matchesPublishedArtifact(metadata, artifact)) {
189
+ await runAws(
190
+ command,
191
+ putObjectArgs({
192
+ roots,
193
+ relativePath: artifact.publishPath,
194
+ bodyPath: artifact.filePath,
195
+ contentType: artifact.contentType,
196
+ cacheControl: artifact.cacheControl,
197
+ hash: artifact.hash,
198
+ awsOptions,
199
+ }),
200
+ { sleep },
201
+ );
202
+ }
203
+ const uploadedMetadata = await runAws(
204
+ command,
205
+ headObjectArgs(roots, artifact.publishPath, awsOptions),
206
+ { sleep },
207
+ );
208
+ assertMetadata(uploadedMetadata.stdout, artifact);
209
+ const response = await httpGet(target.publicUrl);
210
+ if (response.status < 200 || response.status > 299) {
211
+ throw publishError(
212
+ 'E_PUBLISH_VERIFY',
213
+ `Public verification failed for ${artifact.manifestPath}.`,
214
+ );
215
+ }
216
+ const servedType = headerValue(response.headers, 'content-type');
217
+ if (!contentTypesMatch(servedType, artifact.contentType)) {
218
+ throw publishError(
219
+ 'E_PUBLISH_VERIFY',
220
+ `Public content type mismatch for ${artifact.manifestPath}.`,
221
+ );
222
+ }
223
+ if (fileHash(responseBytes(response.body)) !== artifact.hash) {
224
+ throw publishError(
225
+ 'E_PUBLISH_VERIFY',
226
+ `Public bytes do not match the manifest for ${artifact.manifestPath}.`,
227
+ );
228
+ }
229
+ receiptArtifacts.push({
230
+ relativePath: artifact.manifestPath,
231
+ hash: artifact.hash,
232
+ s3Uri: target.s3Uri,
233
+ publicUrl: target.publicUrl,
234
+ httpStatus: response.status,
235
+ contentType: artifact.contentType,
236
+ });
237
+ }
238
+
239
+ const receipt = {
240
+ schemaVersion: 'explainer-kit.publish-receipt/v1',
241
+ provider: 's3-static',
242
+ publishedAt: now(),
243
+ roots: { s3Uri: roots.s3Uri, publicBaseUrl: roots.publicBaseUrl },
244
+ sentinel: {
245
+ relativePath: sentinelRelativePath,
246
+ uploadVerified: true,
247
+ publicVerified: true,
248
+ deleted: sentinelDeleted,
249
+ },
250
+ artifacts: receiptArtifacts,
251
+ };
252
+ const receiptValidation = validateContract('publish-receipt', receipt, {
253
+ manifest,
254
+ });
255
+ if (!receiptValidation.valid) {
256
+ throw publishError(
257
+ 'E_PUBLISH_VERIFY',
258
+ `Invalid publish receipt: ${receiptValidation.errors[0].message}`,
259
+ );
260
+ }
261
+ const receiptPath =
262
+ dependencies.receiptPath ??
263
+ join(dirname(manifestPath), 'publish-receipt.json');
264
+ await writeJsonAtomic(receiptPath, receipt);
265
+ return receipt;
266
+ } finally {
267
+ if (sentinelUploaded && !sentinelDeleted) {
268
+ try {
269
+ await runAws(
270
+ command,
271
+ deleteObjectArgs(roots, sentinelRelativePath, awsOptions),
272
+ { sleep },
273
+ );
274
+ } catch {
275
+ // Preserve the original failure. The missing cleanup remains observable
276
+ // because no successful receipt is emitted.
277
+ }
278
+ }
279
+ await rm(sentinelDirectory, { recursive: true, force: true });
280
+ }
281
+ }
282
+
283
+ async function collectArtifacts(manifest, siteRoot) {
284
+ const canonicalSiteRoot = await realpath(siteRoot);
285
+ const sitePrefix = `${basename(canonicalSiteRoot)}/`;
286
+ const seen = new Set();
287
+ const artifacts = [];
288
+ for (const entry of manifest.artifacts) {
289
+ if (entry.status !== 'built' || typeof entry.renderedPath !== 'string') {
290
+ continue;
291
+ }
292
+ if (!entry.renderedPath.startsWith(sitePrefix)) {
293
+ throw publishError(
294
+ 'E_PUBLISH_INPUT',
295
+ `Rendered path is outside the site root: ${entry.renderedPath}`,
296
+ );
297
+ }
298
+ const publishPath = entry.renderedPath.slice(sitePrefix.length);
299
+ if (
300
+ !publishPath ||
301
+ publishPath.startsWith('/') ||
302
+ publishPath.includes('..')
303
+ ) {
304
+ throw publishError(
305
+ 'E_PUBLISH_INPUT',
306
+ `Unsafe publish path: ${entry.renderedPath}`,
307
+ );
308
+ }
309
+ if (seen.has(publishPath)) {
310
+ throw publishError(
311
+ 'E_PUBLISH_INPUT',
312
+ `Duplicate publish path: ${publishPath}`,
313
+ );
314
+ }
315
+ seen.add(publishPath);
316
+ const filePath = await realpath(join(canonicalSiteRoot, publishPath));
317
+ if (
318
+ filePath !== canonicalSiteRoot &&
319
+ !filePath.startsWith(`${canonicalSiteRoot}${sep}`)
320
+ ) {
321
+ throw publishError(
322
+ 'E_PUBLISH_INPUT',
323
+ `Artifact escapes the site root: ${entry.renderedPath}`,
324
+ );
325
+ }
326
+ const bytes = await readFile(filePath);
327
+ const actualHash = fileHash(bytes);
328
+ if (actualHash !== entry.hash) {
329
+ throw publishError(
330
+ 'E_PUBLISH_INPUT',
331
+ `Artifact hash mismatch: ${entry.renderedPath}`,
332
+ );
333
+ }
334
+ const contentType = contentTypeFor(publishPath, entry.mediaType);
335
+ artifacts.push({
336
+ manifestPath: entry.renderedPath,
337
+ publishPath,
338
+ filePath,
339
+ hash: actualHash,
340
+ contentType,
341
+ cacheControl: 'public, max-age=300',
342
+ });
343
+ }
344
+ return artifacts;
345
+ }
346
+
347
+ function putObjectArgs({
348
+ roots,
349
+ relativePath,
350
+ bodyPath,
351
+ contentType,
352
+ cacheControl,
353
+ hash,
354
+ awsOptions,
355
+ }) {
356
+ return withAwsOptions(
357
+ [
358
+ 's3api',
359
+ 'put-object',
360
+ '--bucket',
361
+ roots.bucket,
362
+ '--key',
363
+ objectKey(roots, relativePath),
364
+ '--body',
365
+ bodyPath,
366
+ '--content-type',
367
+ contentType,
368
+ '--cache-control',
369
+ cacheControl,
370
+ '--metadata',
371
+ `explainer-sha256=${hash.slice('sha256:'.length)}`,
372
+ '--no-cli-pager',
373
+ ],
374
+ awsOptions,
375
+ );
376
+ }
377
+
378
+ function headObjectArgs(roots, relativePath, awsOptions) {
379
+ return withAwsOptions(
380
+ [
381
+ 's3api',
382
+ 'head-object',
383
+ '--bucket',
384
+ roots.bucket,
385
+ '--key',
386
+ objectKey(roots, relativePath),
387
+ '--output',
388
+ 'json',
389
+ '--no-cli-pager',
390
+ ],
391
+ awsOptions,
392
+ );
393
+ }
394
+
395
+ function deleteObjectArgs(roots, relativePath, awsOptions) {
396
+ return withAwsOptions(
397
+ [
398
+ 's3api',
399
+ 'delete-object',
400
+ '--bucket',
401
+ roots.bucket,
402
+ '--key',
403
+ objectKey(roots, relativePath),
404
+ '--no-cli-pager',
405
+ ],
406
+ awsOptions,
407
+ );
408
+ }
409
+
410
+ function withAwsOptions(args, { region, profile }) {
411
+ const result = [...args, '--region', region];
412
+ if (profile) {
413
+ result.push('--profile', profile);
414
+ }
415
+ return result;
416
+ }
417
+
418
+ async function runAws(
419
+ command,
420
+ args,
421
+ { sleep = defaultSleep, attempts = 3, allowNotFound = false } = {},
422
+ ) {
423
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
424
+ try {
425
+ return await command('aws', args);
426
+ } catch (caught) {
427
+ const diagnostic = `${caught?.message ?? ''}\n${caught?.stderr ?? ''}`;
428
+ if (AUTH_PATTERN.test(diagnostic)) {
429
+ throw publishError(
430
+ 'E_PUBLISH_AUTH',
431
+ 'AWS authentication failed; refresh the configured credential chain and retry.',
432
+ );
433
+ }
434
+ if (allowNotFound && NOT_FOUND_PATTERN.test(diagnostic)) {
435
+ return null;
436
+ }
437
+ if (!TRANSIENT_PATTERN.test(diagnostic) || attempt === attempts) {
438
+ throw publishError('E_PUBLISH_AWS', 'AWS object operation failed.');
439
+ }
440
+ await sleep(50 * 2 ** (attempt - 1));
441
+ }
442
+ }
443
+ }
444
+
445
+ async function readExistingMetadata(command, args, { sleep }) {
446
+ const result = await runAws(command, args, {
447
+ sleep,
448
+ allowNotFound: true,
449
+ });
450
+ if (result === null) return null;
451
+ try {
452
+ return JSON.parse(result.stdout || '{}');
453
+ } catch {
454
+ throw publishError(
455
+ 'E_PUBLISH_VERIFY',
456
+ 'AWS returned invalid object metadata.',
457
+ );
458
+ }
459
+ }
460
+
461
+ function matchesPublishedArtifact(metadata, artifact) {
462
+ return (
463
+ metadata?.Metadata?.['explainer-sha256'] ===
464
+ artifact.hash.slice('sha256:'.length) &&
465
+ metadata.ContentType === artifact.contentType &&
466
+ metadata.CacheControl === artifact.cacheControl
467
+ );
468
+ }
469
+
470
+ function assertMetadata(stdout, artifact) {
471
+ let metadata;
472
+ try {
473
+ metadata = JSON.parse(stdout || '{}');
474
+ } catch {
475
+ throw publishError(
476
+ 'E_PUBLISH_VERIFY',
477
+ 'AWS returned invalid object metadata.',
478
+ );
479
+ }
480
+ if (!matchesPublishedArtifact(metadata, artifact)) {
481
+ throw publishError(
482
+ 'E_PUBLISH_VERIFY',
483
+ `Uploaded metadata mismatch for ${artifact.manifestPath}.`,
484
+ );
485
+ }
486
+ }
487
+
488
+ function contentTypeFor(path, declared) {
489
+ const extension = path.slice(path.lastIndexOf('.')).toLowerCase();
490
+ const known = {
491
+ '.css': 'text/css; charset=utf-8',
492
+ '.html': 'text/html; charset=utf-8',
493
+ '.js': 'text/javascript; charset=utf-8',
494
+ '.json': 'application/json',
495
+ '.svg': 'image/svg+xml',
496
+ '.txt': 'text/plain; charset=utf-8',
497
+ };
498
+ return known[extension] ?? declared ?? 'application/octet-stream';
499
+ }
500
+
501
+ function contentTypesMatch(served, expected) {
502
+ if (!served) {
503
+ return false;
504
+ }
505
+ return (
506
+ served.toLowerCase().split(';')[0].trim() ===
507
+ expected.toLowerCase().split(';')[0].trim()
508
+ );
509
+ }
510
+
511
+ function objectKey(roots, relativePath) {
512
+ return roots.keyPrefix ? `${roots.keyPrefix}/${relativePath}` : relativePath;
513
+ }
514
+
515
+ function targetPath(relativePath, roots) {
516
+ const suffix = relativePath
517
+ .split('/')
518
+ .map((part) => encodeURIComponent(part))
519
+ .join('/');
520
+ return {
521
+ s3Uri: `${roots.s3Uri}/${relativePath}`,
522
+ publicUrl: `${roots.publicBaseUrl}/${suffix}`,
523
+ };
524
+ }
525
+
526
+ function fileHash(bytes) {
527
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
528
+ }
529
+
530
+ function headerValue(headers, name) {
531
+ if (typeof headers?.get === 'function') {
532
+ return headers.get(name);
533
+ }
534
+ return headers?.[name] ?? headers?.[name.toLowerCase()];
535
+ }
536
+
537
+ async function readJson(path, label) {
538
+ try {
539
+ return JSON.parse(await readFile(path, 'utf8'));
540
+ } catch {
541
+ throw publishError('E_PUBLISH_INPUT', `Could not read ${label} JSON.`);
542
+ }
543
+ }
544
+
545
+ async function writeJsonAtomic(path, value) {
546
+ const temporary = `${path}.tmp-${process.pid}-${nodeRandomBytes(6).toString('hex')}`;
547
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
548
+ flag: 'wx',
549
+ });
550
+ await rename(temporary, path);
551
+ }
552
+
553
+ async function defaultCommand(file, args) {
554
+ return execFile(file, args, { maxBuffer: 1024 * 1024 });
555
+ }
556
+
557
+ async function defaultHttpGet(url) {
558
+ const response = await fetch(url, {
559
+ redirect: 'follow',
560
+ signal: AbortSignal.timeout(15_000),
561
+ });
562
+ return {
563
+ status: response.status,
564
+ headers: response.headers,
565
+ body: Buffer.from(await response.arrayBuffer()),
566
+ };
567
+ }
568
+
569
+ function responseBytes(body) {
570
+ if (Buffer.isBuffer(body)) return body;
571
+ if (body instanceof Uint8Array) {
572
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
573
+ }
574
+ if (typeof body === 'string') return Buffer.from(body);
575
+ throw publishError(
576
+ 'E_PUBLISH_VERIFY',
577
+ 'Public verification returned an unreadable response body.',
578
+ );
579
+ }
580
+
581
+ function defaultSleep(milliseconds) {
582
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds));
583
+ }
584
+
585
+ function publishError(code, message) {
586
+ return Object.assign(new Error(message), { code });
587
+ }
@@ -0,0 +1,96 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+
4
+ export function validatePortablePath(candidate, { allowAbsolute = true } = {}) {
5
+ const path = typeof candidate === 'string' ? candidate : '';
6
+ if (
7
+ !path ||
8
+ (!allowAbsolute && isAbsolute(path)) ||
9
+ path.includes('\0') ||
10
+ path.includes('\\') ||
11
+ path.split('/').includes('..')
12
+ ) {
13
+ return invalid(
14
+ path,
15
+ 'unsafe-path',
16
+ allowAbsolute
17
+ ? 'Path must use POSIX separators and contain no NUL or traversal segments.'
18
+ : 'Path must be a non-empty relative POSIX path without traversal.',
19
+ );
20
+ }
21
+
22
+ return {
23
+ valid: true,
24
+ normalizedPath: `${isAbsolute(path) ? '/' : ''}${path
25
+ .split('/')
26
+ .filter(Boolean)
27
+ .join('/')}`,
28
+ errors: [],
29
+ };
30
+ }
31
+
32
+ export function validateSafeRelativePath(candidate) {
33
+ return validatePortablePath(candidate, { allowAbsolute: false });
34
+ }
35
+
36
+ export async function resolveRootConfinedPath(root, candidate) {
37
+ const lexical = validateSafeRelativePath(candidate);
38
+ if (!lexical.valid) {
39
+ return invalid(
40
+ typeof candidate === 'string' ? candidate : '',
41
+ 'path-traversal',
42
+ lexical.errors[0].message,
43
+ );
44
+ }
45
+ const path = lexical.normalizedPath;
46
+
47
+ let realRoot;
48
+ try {
49
+ realRoot = await realpath(resolve(root));
50
+ } catch {
51
+ return invalid(
52
+ path,
53
+ 'root-not-found',
54
+ 'The configured root does not exist.',
55
+ );
56
+ }
57
+
58
+ const lexicalTarget = resolve(realRoot, path);
59
+ if (!isWithin(realRoot, lexicalTarget)) {
60
+ return invalid(path, 'path-traversal', 'Path escapes the configured root.');
61
+ }
62
+
63
+ let realTarget;
64
+ try {
65
+ realTarget = await realpath(lexicalTarget);
66
+ } catch {
67
+ return invalid(path, 'path-not-found', 'The resolved path does not exist.');
68
+ }
69
+
70
+ if (!isWithin(realRoot, realTarget)) {
71
+ return invalid(
72
+ path,
73
+ 'symlink-escape',
74
+ 'The resolved path escapes the configured root through a symlink.',
75
+ );
76
+ }
77
+
78
+ return {
79
+ valid: true,
80
+ normalizedPath: path.split('/').filter(Boolean).join('/'),
81
+ absolutePath: realTarget,
82
+ errors: [],
83
+ };
84
+ }
85
+
86
+ function isWithin(root, target) {
87
+ const path = relative(root, target);
88
+ return path === '' || (!path.startsWith(`..${sep}`) && path !== '..');
89
+ }
90
+
91
+ function invalid(path, code, message) {
92
+ return {
93
+ valid: false,
94
+ errors: [{ path, code, message }],
95
+ };
96
+ }