@open-agent-toolkit/cli 0.2.73 → 0.2.75

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 (149) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +3 -56
  3. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  4. package/assets/docs/contributing/code.md +2 -2
  5. package/assets/docs/contributing/index.md +0 -1
  6. package/assets/docs/reference/cli-reference.md +1 -1
  7. package/assets/docs/reference/troubleshooting.md +17 -40
  8. package/assets/docs/workflows/projects/artifacts.md +16 -34
  9. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  10. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  11. package/assets/docs/workflows/skills/index.md +1 -2
  12. package/assets/public-package-versions.json +4 -4
  13. package/assets/skills/explainer-kit/SKILL.md +96 -171
  14. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  15. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  16. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  17. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  18. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  19. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  20. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  21. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  22. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  23. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  24. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  25. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  26. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  27. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  28. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  29. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  30. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  31. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  32. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  33. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  34. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  35. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  36. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  37. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  38. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  39. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  40. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  41. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  42. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  43. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  44. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  45. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  46. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  47. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  48. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  49. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  50. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  51. package/assets/skills/oat-project-document/SKILL.md +1 -1
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  53. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  54. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  55. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  57. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  58. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  59. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  60. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  62. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  63. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  64. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  65. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  66. package/dist/commands/config/index.d.ts.map +1 -1
  67. package/dist/commands/config/index.js +9 -113
  68. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  69. package/dist/commands/project/archive/archive-utils.js +32 -184
  70. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  71. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  72. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  73. package/dist/config/oat-config.d.ts +0 -11
  74. package/dist/config/oat-config.d.ts.map +1 -1
  75. package/dist/config/oat-config.js +0 -34
  76. package/dist/config/resolve.d.ts.map +1 -1
  77. package/dist/config/resolve.js +0 -8
  78. package/dist/release/public-package-contract.d.ts.map +1 -1
  79. package/dist/release/public-package-contract.js +2 -10
  80. package/dist/validation/project-state.d.ts.map +1 -1
  81. package/dist/validation/project-state.js +42 -3
  82. package/package.json +2 -2
  83. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  84. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  85. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  86. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  87. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  88. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  89. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  90. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  91. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  92. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  93. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  94. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  95. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  96. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  97. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  98. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  99. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  100. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  101. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  102. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  103. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  104. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  105. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  106. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  107. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  108. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  109. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  110. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  111. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  112. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  113. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  114. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  115. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  116. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  117. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  118. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  119. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  120. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  121. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  122. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  123. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  124. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  125. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  126. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  127. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  128. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  129. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  130. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  131. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  132. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  133. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  134. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  135. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  136. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  137. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  138. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  139. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  140. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  141. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  142. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  143. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  144. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  145. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  146. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  147. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  148. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  149. package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
@@ -1,759 +0,0 @@
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 {
16
- catalogFromManifest,
17
- initiativeCatalogPath,
18
- resolvePublicVerificationPolicy,
19
- serializeInitiativeCatalog,
20
- validateInitiativeCatalog,
21
- } from './catalog.mjs';
22
- import { validateContract } from './contracts.mjs';
23
- import { assertManifestPublishable } from './publication-policy.mjs';
24
- import {
25
- composePublicationTarget,
26
- describeRootDivergence,
27
- isPrivatePublicHost,
28
- normalizePublishRoots,
29
- privatePublicRootAllowed,
30
- rootDivergenceWarningSuppressed,
31
- } from './s3-roots.mjs';
32
-
33
- export {
34
- composePublicationTarget,
35
- normalizePublishRoots,
36
- } from './s3-roots.mjs';
37
-
38
- const execFile = promisify(execFileCallback);
39
- const SENTINEL_BODY = 'explainer-kit sentinel\n';
40
- const TRANSIENT_PATTERN =
41
- /(?:timeout|timed out|connection reset|temporar|throttl|slowdown|service unavailable|\b5\d\d\b)/i;
42
- const AUTH_PATTERN =
43
- /(?:credential|expired|sso session|access key|invalidclienttokenid|unrecognizedclient|token)/i;
44
- const NOT_FOUND_PATTERN = /(?:\b404\b|not found|nosuchkey)/i;
45
-
46
- export function createSentinelRelativePath(
47
- runId,
48
- randomBytes = nodeRandomBytes,
49
- ) {
50
- const safeRunId =
51
- String(runId)
52
- .toLowerCase()
53
- .replace(/[^a-z0-9]+/g, '-')
54
- .replace(/^-|-$/g, '') || 'run';
55
- const suffix = randomBytes(16).toString('hex');
56
- if (!/^[a-f0-9]{32}$/.test(suffix)) {
57
- throw publishError(
58
- 'E_PUBLISH_INPUT',
59
- 'Sentinel randomness must provide 128 bits.',
60
- );
61
- }
62
- return `.explainer-kit-sentinel/${safeRunId}-${suffix}.txt`;
63
- }
64
-
65
- export async function publishS3Static(request, dependencies = {}) {
66
- if (dependencies.approved !== true) {
67
- throw publishError(
68
- 'E_PUBLISH_APPROVAL',
69
- 'Publishing requires explicit human approval.',
70
- );
71
- }
72
-
73
- const requestValidation = validateContract('publish-request', request);
74
- if (!requestValidation.valid) {
75
- throw publishError(
76
- 'E_PUBLISH_INPUT',
77
- `Invalid publish request: ${requestValidation.errors[0].message}`,
78
- );
79
- }
80
- const roots = normalizePublishRoots(request.s3Uri, request.publicBaseUrl);
81
- const cwd = dependencies.cwd ?? process.cwd();
82
- const siteRoot = resolve(cwd, request.siteRoot);
83
- const manifestPath = resolve(cwd, request.manifestPath);
84
- const manifest = await readJson(manifestPath, 'manifest');
85
- const buildRecord =
86
- manifest.outcome === 'incomplete'
87
- ? await readJson(
88
- resolve(dirname(manifestPath), manifest.buildRecord?.path ?? ''),
89
- 'build record',
90
- )
91
- : undefined;
92
- assertManifestPublishable(manifest, { buildRecord });
93
- const manifestValidation = validateContract('manifest', manifest);
94
- if (!manifestValidation.valid) {
95
- throw publishError(
96
- 'E_PUBLISH_INPUT',
97
- `Invalid manifest: ${manifestValidation.errors[0].message}`,
98
- );
99
- }
100
- const artifacts = await collectArtifacts(manifest, siteRoot);
101
- const command = dependencies.command ?? defaultCommand;
102
- const httpGet = dependencies.httpGet ?? defaultHttpGet;
103
- const randomBytes = dependencies.randomBytes ?? nodeRandomBytes;
104
- const now = dependencies.now ?? (() => new Date().toISOString());
105
- const sleep = dependencies.sleep ?? defaultSleep;
106
- const awsOptions = {
107
- region: request.awsRegion,
108
- profile: request.awsProfile,
109
- };
110
- const sentinelRelativePath = createSentinelRelativePath(
111
- manifest.runId,
112
- randomBytes,
113
- );
114
- const sentinelTargetPath = composePublicationTarget(
115
- sentinelRelativePath,
116
- roots,
117
- );
118
- // Resolved before the catalog is built: the catalog is hashed and uploaded
119
- // before any public verification runs, so it must carry the policy up front.
120
- const publicAccess =
121
- request.schemaVersion === 'explainer-kit.publish-request/v1'
122
- ? 'public'
123
- : request.publicAccess;
124
- const verificationPolicy = resolvePublicVerificationPolicy(publicAccess);
125
-
126
- const divergence = describeRootDivergence(roots);
127
- if (divergence && !rootDivergenceWarningSuppressed()) {
128
- (dependencies.warn ?? ((message) => console.warn(message)))(
129
- `explainer-kit: ${divergence}`,
130
- );
131
- }
132
-
133
- const catalog = catalogFromManifest(manifest, roots.publicBaseUrl, {
134
- publicAccess,
135
- });
136
- const catalogValidation = validateInitiativeCatalog(
137
- catalog,
138
- manifest,
139
- roots.publicBaseUrl,
140
- { publicAccess },
141
- );
142
- if (!catalogValidation.valid) {
143
- throw publishError(
144
- 'E_PUBLISH_INPUT',
145
- `Invalid initiative catalog: ${catalogValidation.errors[0].message}`,
146
- );
147
- }
148
- const catalogBody = Buffer.from(serializeInitiativeCatalog(catalog));
149
- const catalogManifestPath = initiativeCatalogPath(manifest.slug);
150
- const catalogPublishPath = catalogManifestPath.slice('site/'.length);
151
- if (artifacts.some(({ publishPath }) => publishPath === catalogPublishPath)) {
152
- throw publishError(
153
- 'E_PUBLISH_INPUT',
154
- `Generated catalog path collides with a manifest artifact: ${catalogManifestPath}`,
155
- );
156
- }
157
- const sentinelDirectory = await mkdtemp(
158
- join(tmpdir(), 'explainer-sentinel-'),
159
- );
160
- const sentinelBodyPath = join(sentinelDirectory, 'sentinel.txt');
161
- const catalogBodyPath = join(sentinelDirectory, 'catalog.json');
162
- const catalogArtifact = {
163
- source: { kind: 'auxiliary', name: 'catalog' },
164
- manifestPath: catalogManifestPath,
165
- publishPath: catalogPublishPath,
166
- filePath: catalogBodyPath,
167
- hash: fileHash(catalogBody),
168
- contentType: 'application/json',
169
- cacheControl: 'public, max-age=300',
170
- };
171
- const publicationArtifacts = [...artifacts, catalogArtifact];
172
- let sentinelUploaded = false;
173
- let sentinelDeleted = false;
174
-
175
- try {
176
- await writeFile(sentinelBodyPath, SENTINEL_BODY, 'utf8');
177
- await writeFile(catalogBodyPath, catalogBody);
178
- await runAws(
179
- command,
180
- putObjectArgs({
181
- roots,
182
- relativePath: sentinelRelativePath,
183
- bodyPath: sentinelBodyPath,
184
- contentType: 'text/plain; charset=utf-8',
185
- cacheControl: 'no-store',
186
- hash: fileHash(Buffer.from(SENTINEL_BODY)),
187
- awsOptions,
188
- }),
189
- { sleep },
190
- );
191
- sentinelUploaded = true;
192
- const sentinelMetadata = await readObjectMetadata(
193
- command,
194
- headObjectArgs(roots, sentinelRelativePath, awsOptions),
195
- { sleep },
196
- );
197
- const sentinelObjectVerification = await verifyObjectBytes({
198
- command,
199
- metadata: sentinelMetadata,
200
- roots,
201
- relativePath: sentinelRelativePath,
202
- expectedHash: fileHash(Buffer.from(SENTINEL_BODY)),
203
- downloadPath: join(sentinelDirectory, 'sentinel-download.txt'),
204
- awsOptions,
205
- sleep,
206
- });
207
- let sentinelPublicVerification = {
208
- status: verificationPolicy.receiptSkipStatus,
209
- };
210
- if (verificationPolicy.verifyPublicly) {
211
- const sentinelResponse = await httpGet(sentinelTargetPath.publicUrl);
212
- if (
213
- sentinelResponse.status < 200 ||
214
- sentinelResponse.status > 299 ||
215
- !responseBytes(sentinelResponse.body).equals(Buffer.from(SENTINEL_BODY))
216
- ) {
217
- throw publishError(
218
- 'E_PUBLISH_ROOTS',
219
- 'Public root did not serve the uploaded sentinel.',
220
- );
221
- }
222
- sentinelPublicVerification = {
223
- status: 'verified',
224
- httpStatus: sentinelResponse.status,
225
- hash: fileHash(responseBytes(sentinelResponse.body)),
226
- };
227
- }
228
- await runAws(
229
- command,
230
- deleteObjectArgs(roots, sentinelRelativePath, awsOptions),
231
- { sleep },
232
- );
233
- sentinelDeleted = true;
234
-
235
- const receiptArtifacts = [];
236
- for (const artifact of publicationArtifacts) {
237
- const target = composePublicationTarget(artifact.publishPath, roots);
238
- const metadata = await readExistingMetadata(
239
- command,
240
- headObjectArgs(roots, artifact.publishPath, awsOptions),
241
- { sleep },
242
- );
243
- if (!matchesPublishedArtifact(metadata, artifact)) {
244
- await runAws(
245
- command,
246
- putObjectArgs({
247
- roots,
248
- relativePath: artifact.publishPath,
249
- bodyPath: artifact.filePath,
250
- contentType: artifact.contentType,
251
- cacheControl: artifact.cacheControl,
252
- hash: artifact.hash,
253
- awsOptions,
254
- }),
255
- { sleep },
256
- );
257
- }
258
- const uploadedMetadata = await readObjectMetadata(
259
- command,
260
- headObjectArgs(roots, artifact.publishPath, awsOptions),
261
- { sleep },
262
- );
263
- assertMetadata(uploadedMetadata, artifact);
264
- const objectVerification = await verifyObjectBytes({
265
- command,
266
- metadata: uploadedMetadata,
267
- roots,
268
- relativePath: artifact.publishPath,
269
- expectedHash: artifact.hash,
270
- downloadPath: join(
271
- sentinelDirectory,
272
- `object-${receiptArtifacts.length}.download`,
273
- ),
274
- awsOptions,
275
- sleep,
276
- });
277
- let publicVerification = {
278
- status: verificationPolicy.receiptSkipStatus,
279
- };
280
- if (verificationPolicy.verifyPublicly) {
281
- const response = await httpGet(target.publicUrl);
282
- if (response.status < 200 || response.status > 299) {
283
- throw publishError(
284
- 'E_PUBLISH_VERIFY',
285
- `Public verification failed for ${artifact.manifestPath}.`,
286
- );
287
- }
288
- const servedType = headerValue(response.headers, 'content-type');
289
- if (!contentTypesMatch(servedType, artifact.contentType)) {
290
- throw publishError(
291
- 'E_PUBLISH_VERIFY',
292
- `Public content type mismatch for ${artifact.manifestPath}.`,
293
- );
294
- }
295
- const publicHash = fileHash(responseBytes(response.body));
296
- if (publicHash !== artifact.hash) {
297
- throw publishError(
298
- 'E_PUBLISH_VERIFY',
299
- `Public bytes do not match the manifest for ${artifact.manifestPath}.`,
300
- );
301
- }
302
- publicVerification = {
303
- status: 'verified',
304
- httpStatus: response.status,
305
- hash: publicHash,
306
- };
307
- }
308
- receiptArtifacts.push({
309
- source: artifact.source,
310
- relativePath: artifact.manifestPath,
311
- hash: artifact.hash,
312
- s3Uri: target.s3Uri,
313
- publicUrl: target.publicUrl,
314
- contentType: artifact.contentType,
315
- objectVerification,
316
- publicVerification,
317
- });
318
- }
319
-
320
- const receipt = {
321
- schemaVersion: 'explainer-kit.publish-receipt/v2',
322
- provider: 's3-static',
323
- publishedAt: now(),
324
- publicAccess,
325
- // Durable trace for the anti-SSRF opt-in. Without it, a run that
326
- // published to an internal address with the control disabled is
327
- // indistinguishable in retained evidence from one that did not, because
328
- // the variable is read straight from the environment and enters no other
329
- // record. Emitted only in that exceptional case, so ordinary receipts are
330
- // byte-identical to before.
331
- ...(privatePublicRootAllowed() &&
332
- isPrivatePublicHost(new URL(roots.publicBaseUrl).hostname) && {
333
- publicRootPolicy: 'private-allowed',
334
- }),
335
- roots: { s3Uri: roots.s3Uri, publicBaseUrl: roots.publicBaseUrl },
336
- sentinel: {
337
- relativePath: sentinelRelativePath,
338
- objectVerification: sentinelObjectVerification,
339
- publicVerification: sentinelPublicVerification,
340
- deleted: sentinelDeleted,
341
- },
342
- artifacts: receiptArtifacts,
343
- };
344
- const receiptValidation = validateContract('publish-receipt', receipt, {
345
- manifest,
346
- catalogArtifact: {
347
- relativePath: catalogArtifact.manifestPath,
348
- hash: catalogArtifact.hash,
349
- },
350
- });
351
- if (!receiptValidation.valid) {
352
- throw publishError(
353
- 'E_PUBLISH_VERIFY',
354
- `Invalid publish receipt: ${receiptValidation.errors[0].message}`,
355
- );
356
- }
357
- const receiptPath =
358
- dependencies.receiptPath ??
359
- join(dirname(manifestPath), 'publish-receipt.json');
360
- await writeJsonAtomic(receiptPath, receipt);
361
- return receipt;
362
- } finally {
363
- if (sentinelUploaded && !sentinelDeleted) {
364
- try {
365
- await runAws(
366
- command,
367
- deleteObjectArgs(roots, sentinelRelativePath, awsOptions),
368
- { sleep },
369
- );
370
- } catch {
371
- // Preserve the original failure. The missing cleanup remains observable
372
- // because no successful receipt is emitted.
373
- }
374
- }
375
- await rm(sentinelDirectory, { recursive: true, force: true });
376
- }
377
- }
378
-
379
- async function collectArtifacts(manifest, siteRoot) {
380
- const canonicalSiteRoot = await realpath(siteRoot);
381
- const sitePrefix = `${basename(canonicalSiteRoot)}/`;
382
- const seen = new Set();
383
- const artifacts = [];
384
- for (const entry of manifest.artifacts) {
385
- if (entry.status !== 'built' || typeof entry.renderedPath !== 'string') {
386
- continue;
387
- }
388
- if (!entry.renderedPath.startsWith(sitePrefix)) {
389
- throw publishError(
390
- 'E_PUBLISH_INPUT',
391
- `Rendered path is outside the site root: ${entry.renderedPath}`,
392
- );
393
- }
394
- const publishPath = entry.renderedPath.slice(sitePrefix.length);
395
- if (
396
- !publishPath ||
397
- publishPath.startsWith('/') ||
398
- publishPath.includes('..')
399
- ) {
400
- throw publishError(
401
- 'E_PUBLISH_INPUT',
402
- `Unsafe publish path: ${entry.renderedPath}`,
403
- );
404
- }
405
- if (seen.has(publishPath)) {
406
- throw publishError(
407
- 'E_PUBLISH_INPUT',
408
- `Duplicate publish path: ${publishPath}`,
409
- );
410
- }
411
- seen.add(publishPath);
412
- const filePath = await realpath(join(canonicalSiteRoot, publishPath));
413
- if (
414
- filePath !== canonicalSiteRoot &&
415
- !filePath.startsWith(`${canonicalSiteRoot}${sep}`)
416
- ) {
417
- throw publishError(
418
- 'E_PUBLISH_INPUT',
419
- `Artifact escapes the site root: ${entry.renderedPath}`,
420
- );
421
- }
422
- const bytes = await readFile(filePath);
423
- const actualHash = fileHash(bytes);
424
- if (actualHash !== entry.hash) {
425
- throw publishError(
426
- 'E_PUBLISH_INPUT',
427
- `Artifact hash mismatch: ${entry.renderedPath}`,
428
- );
429
- }
430
- const contentType = contentTypeFor(publishPath, entry.mediaType);
431
- artifacts.push({
432
- source: { kind: 'manifest', artifactId: entry.id },
433
- manifestPath: entry.renderedPath,
434
- publishPath,
435
- filePath,
436
- hash: actualHash,
437
- contentType,
438
- cacheControl: 'public, max-age=300',
439
- });
440
- }
441
- return artifacts;
442
- }
443
-
444
- function putObjectArgs({
445
- roots,
446
- relativePath,
447
- bodyPath,
448
- contentType,
449
- cacheControl,
450
- hash,
451
- awsOptions,
452
- }) {
453
- return withAwsOptions(
454
- [
455
- 's3api',
456
- 'put-object',
457
- '--bucket',
458
- roots.bucket,
459
- '--key',
460
- objectKey(roots, relativePath),
461
- '--body',
462
- bodyPath,
463
- '--content-type',
464
- contentType,
465
- '--cache-control',
466
- cacheControl,
467
- '--metadata',
468
- `explainer-sha256=${hash.slice('sha256:'.length)}`,
469
- '--checksum-sha256',
470
- Buffer.from(hash.slice('sha256:'.length), 'hex').toString('base64'),
471
- '--no-cli-pager',
472
- ],
473
- awsOptions,
474
- );
475
- }
476
-
477
- function headObjectArgs(roots, relativePath, awsOptions) {
478
- return withAwsOptions(
479
- [
480
- 's3api',
481
- 'head-object',
482
- '--bucket',
483
- roots.bucket,
484
- '--key',
485
- objectKey(roots, relativePath),
486
- '--checksum-mode',
487
- 'ENABLED',
488
- '--output',
489
- 'json',
490
- '--no-cli-pager',
491
- ],
492
- awsOptions,
493
- );
494
- }
495
-
496
- function getObjectArgs(roots, relativePath, downloadPath, awsOptions) {
497
- return withAwsOptions(
498
- [
499
- 's3api',
500
- 'get-object',
501
- '--bucket',
502
- roots.bucket,
503
- '--key',
504
- objectKey(roots, relativePath),
505
- '--checksum-mode',
506
- 'ENABLED',
507
- downloadPath,
508
- '--no-cli-pager',
509
- ],
510
- awsOptions,
511
- );
512
- }
513
-
514
- function deleteObjectArgs(roots, relativePath, awsOptions) {
515
- return withAwsOptions(
516
- [
517
- 's3api',
518
- 'delete-object',
519
- '--bucket',
520
- roots.bucket,
521
- '--key',
522
- objectKey(roots, relativePath),
523
- '--no-cli-pager',
524
- ],
525
- awsOptions,
526
- );
527
- }
528
-
529
- function withAwsOptions(args, { region, profile }) {
530
- const result = [...args, '--region', region];
531
- if (profile) {
532
- result.push('--profile', profile);
533
- }
534
- return result;
535
- }
536
-
537
- async function runAws(
538
- command,
539
- args,
540
- { sleep = defaultSleep, attempts = 3, allowNotFound = false } = {},
541
- ) {
542
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
543
- try {
544
- return await command('aws', args);
545
- } catch (caught) {
546
- const diagnostic = `${caught?.message ?? ''}\n${caught?.stderr ?? ''}`;
547
- if (AUTH_PATTERN.test(diagnostic)) {
548
- throw publishError(
549
- 'E_PUBLISH_AUTH',
550
- 'AWS authentication failed; refresh the configured credential chain and retry.',
551
- );
552
- }
553
- if (allowNotFound && NOT_FOUND_PATTERN.test(diagnostic)) {
554
- return null;
555
- }
556
- if (!TRANSIENT_PATTERN.test(diagnostic) || attempt === attempts) {
557
- throw publishError('E_PUBLISH_AWS', 'AWS object operation failed.');
558
- }
559
- await sleep(50 * 2 ** (attempt - 1));
560
- }
561
- }
562
- }
563
-
564
- async function readExistingMetadata(command, args, { sleep }) {
565
- const result = await runAws(command, args, {
566
- sleep,
567
- allowNotFound: true,
568
- });
569
- if (result === null) return null;
570
- try {
571
- return JSON.parse(result.stdout || '{}');
572
- } catch {
573
- throw publishError(
574
- 'E_PUBLISH_VERIFY',
575
- 'AWS returned invalid object metadata.',
576
- );
577
- }
578
- }
579
-
580
- async function readObjectMetadata(command, args, { sleep }) {
581
- const result = await runAws(command, args, { sleep });
582
- return parseAwsJson(result.stdout);
583
- }
584
-
585
- function matchesPublishedArtifact(metadata, artifact) {
586
- return (
587
- metadata?.Metadata?.['explainer-sha256'] ===
588
- artifact.hash.slice('sha256:'.length) &&
589
- metadata.ContentType === artifact.contentType &&
590
- metadata.CacheControl === artifact.cacheControl
591
- );
592
- }
593
-
594
- function assertMetadata(metadata, artifact) {
595
- if (!matchesPublishedArtifact(metadata, artifact)) {
596
- throw publishError(
597
- 'E_PUBLISH_VERIFY',
598
- `Uploaded metadata mismatch for ${artifact.manifestPath}.`,
599
- );
600
- }
601
- }
602
-
603
- async function verifyObjectBytes({
604
- command,
605
- metadata,
606
- roots,
607
- relativePath,
608
- expectedHash,
609
- downloadPath,
610
- awsOptions,
611
- sleep,
612
- }) {
613
- const expectedBase64 = Buffer.from(
614
- expectedHash.slice('sha256:'.length),
615
- 'hex',
616
- ).toString('base64');
617
- if (typeof metadata.ChecksumSHA256 === 'string') {
618
- if (metadata.ChecksumSHA256 !== expectedBase64) {
619
- throw publishError(
620
- 'E_PUBLISH_VERIFY',
621
- `Authenticated object checksum mismatch for ${relativePath}.`,
622
- );
623
- }
624
- return {
625
- status: 'verified',
626
- method: 'service-checksum',
627
- hash: expectedHash,
628
- };
629
- }
630
-
631
- try {
632
- await runAws(
633
- command,
634
- getObjectArgs(roots, relativePath, downloadPath, awsOptions),
635
- { sleep },
636
- );
637
- } catch {
638
- throw publishError(
639
- 'E_PUBLISH_VERIFY',
640
- `Authenticated object-byte verification is unavailable for ${relativePath}.`,
641
- );
642
- }
643
- if (fileHash(await readFile(downloadPath)) !== expectedHash) {
644
- throw publishError(
645
- 'E_PUBLISH_VERIFY',
646
- `Authenticated object bytes do not match ${relativePath}.`,
647
- );
648
- }
649
- return {
650
- status: 'verified',
651
- method: 'authenticated-download',
652
- hash: expectedHash,
653
- };
654
- }
655
-
656
- function parseAwsJson(stdout) {
657
- try {
658
- return JSON.parse(stdout || '{}');
659
- } catch {
660
- throw publishError(
661
- 'E_PUBLISH_VERIFY',
662
- 'AWS returned invalid object metadata.',
663
- );
664
- }
665
- }
666
-
667
- function contentTypeFor(path, declared) {
668
- const extension = path.slice(path.lastIndexOf('.')).toLowerCase();
669
- const known = {
670
- '.css': 'text/css; charset=utf-8',
671
- '.html': 'text/html; charset=utf-8',
672
- '.js': 'text/javascript; charset=utf-8',
673
- '.json': 'application/json',
674
- '.svg': 'image/svg+xml',
675
- '.txt': 'text/plain; charset=utf-8',
676
- };
677
- return known[extension] ?? declared ?? 'application/octet-stream';
678
- }
679
-
680
- function contentTypesMatch(served, expected) {
681
- if (!served) {
682
- return false;
683
- }
684
- return (
685
- served.toLowerCase().split(';')[0].trim() ===
686
- expected.toLowerCase().split(';')[0].trim()
687
- );
688
- }
689
-
690
- function objectKey(roots, relativePath) {
691
- return roots.keyPrefix ? `${roots.keyPrefix}/${relativePath}` : relativePath;
692
- }
693
-
694
- function fileHash(bytes) {
695
- return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
696
- }
697
-
698
- function headerValue(headers, name) {
699
- if (typeof headers?.get === 'function') {
700
- return headers.get(name);
701
- }
702
- return headers?.[name] ?? headers?.[name.toLowerCase()];
703
- }
704
-
705
- async function readJson(path, label) {
706
- try {
707
- return JSON.parse(await readFile(path, 'utf8'));
708
- } catch {
709
- throw publishError('E_PUBLISH_INPUT', `Could not read ${label} JSON.`);
710
- }
711
- }
712
-
713
- async function writeJsonAtomic(path, value) {
714
- const temporary = `${path}.tmp-${process.pid}-${nodeRandomBytes(6).toString('hex')}`;
715
- await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
716
- flag: 'wx',
717
- });
718
- await rename(temporary, path);
719
- }
720
-
721
- async function defaultCommand(file, args) {
722
- return execFile(file, args, { maxBuffer: 1024 * 1024 });
723
- }
724
-
725
- export async function defaultHttpGet(url, { fetchImpl = fetch } = {}) {
726
- // A canonical public artifact URL is uploaded to a known key and should never
727
- // legitimately redirect. Following redirects turned public verification into
728
- // an outbound GET primitive that a third party could bounce inward, so a
729
- // redirect is a verification failure rather than something to chase.
730
- const response = await fetchImpl(url, {
731
- redirect: 'error',
732
- signal: AbortSignal.timeout(15_000),
733
- });
734
- return {
735
- status: response.status,
736
- headers: response.headers,
737
- body: Buffer.from(await response.arrayBuffer()),
738
- };
739
- }
740
-
741
- function responseBytes(body) {
742
- if (Buffer.isBuffer(body)) return body;
743
- if (body instanceof Uint8Array) {
744
- return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
745
- }
746
- if (typeof body === 'string') return Buffer.from(body);
747
- throw publishError(
748
- 'E_PUBLISH_VERIFY',
749
- 'Public verification returned an unreadable response body.',
750
- );
751
- }
752
-
753
- function defaultSleep(milliseconds) {
754
- return new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds));
755
- }
756
-
757
- function publishError(code, message) {
758
- return Object.assign(new Error(message), { code });
759
- }