@dsh-enhanced/plugin-control-plane 0.1.31 → 0.1.34

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 (199) hide show
  1. package/README.md +352 -4
  2. package/bin/dsh-local-release-adapter.js +80 -16
  3. package/bin/dsh-npm-registry-adapter.js +613 -0
  4. package/bin/dsh-source-adoption-authority.js +12 -0
  5. package/bin/dsh-source-approval-authority.js +16 -0
  6. package/bin/dsh-source-release-authority.js +12 -0
  7. package/bin/dsh-systemd-host-attestor.js +707 -0
  8. package/bin/dsh-task-observation-authority.js +15 -0
  9. package/lib/adapter-process.d.ts +23 -0
  10. package/lib/adapter-process.d.ts.map +1 -0
  11. package/lib/adapter-process.js +305 -0
  12. package/lib/adapter-process.js.map +1 -0
  13. package/lib/adoption-coordinator.d.ts +63 -0
  14. package/lib/adoption-coordinator.d.ts.map +1 -0
  15. package/lib/adoption-coordinator.js +241 -0
  16. package/lib/adoption-coordinator.js.map +1 -0
  17. package/lib/adoption-handoff.d.ts +16 -0
  18. package/lib/adoption-handoff.d.ts.map +1 -0
  19. package/lib/adoption-handoff.js +11 -0
  20. package/lib/adoption-handoff.js.map +1 -0
  21. package/lib/attestation.d.ts.map +1 -1
  22. package/lib/attestation.js +49 -1
  23. package/lib/attestation.js.map +1 -1
  24. package/lib/catalog.d.ts.map +1 -1
  25. package/lib/catalog.js +29 -2
  26. package/lib/catalog.js.map +1 -1
  27. package/lib/cli.d.ts +33 -9
  28. package/lib/cli.d.ts.map +1 -1
  29. package/lib/cli.js +624 -178
  30. package/lib/cli.js.map +1 -1
  31. package/lib/deployment-readiness.d.ts +38 -0
  32. package/lib/deployment-readiness.d.ts.map +1 -0
  33. package/lib/deployment-readiness.js +154 -0
  34. package/lib/deployment-readiness.js.map +1 -0
  35. package/lib/effect-blocked-replay.d.ts +81 -0
  36. package/lib/effect-blocked-replay.d.ts.map +1 -0
  37. package/lib/effect-blocked-replay.js +259 -0
  38. package/lib/effect-blocked-replay.js.map +1 -0
  39. package/lib/errors.d.ts +6 -0
  40. package/lib/errors.d.ts.map +1 -0
  41. package/lib/errors.js +9 -0
  42. package/lib/errors.js.map +1 -0
  43. package/lib/foreground-deployment-runtime.d.ts +20 -0
  44. package/lib/foreground-deployment-runtime.d.ts.map +1 -0
  45. package/lib/foreground-deployment-runtime.js +79 -0
  46. package/lib/foreground-deployment-runtime.js.map +1 -0
  47. package/lib/foreground-deployment.d.ts +17 -0
  48. package/lib/foreground-deployment.d.ts.map +1 -0
  49. package/lib/foreground-deployment.js +43 -0
  50. package/lib/foreground-deployment.js.map +1 -0
  51. package/lib/host-attestor.d.ts +1 -1
  52. package/lib/host-attestor.d.ts.map +1 -1
  53. package/lib/host-attestor.js +51 -57
  54. package/lib/host-attestor.js.map +1 -1
  55. package/lib/index.d.ts +25 -2
  56. package/lib/index.d.ts.map +1 -1
  57. package/lib/index.js +11 -2
  58. package/lib/index.js.map +1 -1
  59. package/lib/npm-publish.d.ts +25 -0
  60. package/lib/npm-publish.d.ts.map +1 -0
  61. package/lib/npm-publish.js +280 -0
  62. package/lib/npm-publish.js.map +1 -0
  63. package/lib/owner-task-gap-types.d.ts +21 -0
  64. package/lib/owner-task-gap-types.d.ts.map +1 -0
  65. package/lib/owner-task-gap-types.js +2 -0
  66. package/lib/owner-task-gap-types.js.map +1 -0
  67. package/lib/owner-task-gaps.d.ts +24 -0
  68. package/lib/owner-task-gaps.d.ts.map +1 -0
  69. package/lib/owner-task-gaps.js +74 -0
  70. package/lib/owner-task-gaps.js.map +1 -0
  71. package/lib/post-activation.d.ts +33 -0
  72. package/lib/post-activation.d.ts.map +1 -0
  73. package/lib/post-activation.js +219 -0
  74. package/lib/post-activation.js.map +1 -0
  75. package/lib/registry-fetch.d.ts +50 -0
  76. package/lib/registry-fetch.d.ts.map +1 -0
  77. package/lib/registry-fetch.js +267 -0
  78. package/lib/registry-fetch.js.map +1 -0
  79. package/lib/release.d.ts +4 -4
  80. package/lib/release.d.ts.map +1 -1
  81. package/lib/release.js +142 -56
  82. package/lib/release.js.map +1 -1
  83. package/lib/replay-endpoint-protocol.d.ts +65 -0
  84. package/lib/replay-endpoint-protocol.d.ts.map +1 -0
  85. package/lib/replay-endpoint-protocol.js +183 -0
  86. package/lib/replay-endpoint-protocol.js.map +1 -0
  87. package/lib/replay-endpoint.d.ts +7 -0
  88. package/lib/replay-endpoint.d.ts.map +1 -0
  89. package/lib/replay-endpoint.js +193 -0
  90. package/lib/replay-endpoint.js.map +1 -0
  91. package/lib/replay-grant.d.ts +62 -0
  92. package/lib/replay-grant.d.ts.map +1 -0
  93. package/lib/replay-grant.js +208 -0
  94. package/lib/replay-grant.js.map +1 -0
  95. package/lib/replay-journal.d.ts +25 -0
  96. package/lib/replay-journal.d.ts.map +1 -0
  97. package/lib/replay-journal.js +275 -0
  98. package/lib/replay-journal.js.map +1 -0
  99. package/lib/runtime-observer-protocol.d.ts +63 -0
  100. package/lib/runtime-observer-protocol.d.ts.map +1 -0
  101. package/lib/runtime-observer-protocol.js +280 -0
  102. package/lib/runtime-observer-protocol.js.map +1 -0
  103. package/lib/runtime-observer.d.ts +8 -0
  104. package/lib/runtime-observer.d.ts.map +1 -0
  105. package/lib/runtime-observer.js +182 -0
  106. package/lib/runtime-observer.js.map +1 -0
  107. package/lib/service.d.ts +145 -1
  108. package/lib/service.d.ts.map +1 -1
  109. package/lib/service.js +734 -6
  110. package/lib/service.js.map +1 -1
  111. package/lib/source-adoption-authority.d.ts +65 -0
  112. package/lib/source-adoption-authority.d.ts.map +1 -0
  113. package/lib/source-adoption-authority.js +271 -0
  114. package/lib/source-adoption-authority.js.map +1 -0
  115. package/lib/source-adoption-runner.d.ts +28 -0
  116. package/lib/source-adoption-runner.d.ts.map +1 -0
  117. package/lib/source-adoption-runner.js +197 -0
  118. package/lib/source-adoption-runner.js.map +1 -0
  119. package/lib/source-approval-authority.d.ts +50 -0
  120. package/lib/source-approval-authority.d.ts.map +1 -0
  121. package/lib/source-approval-authority.js +412 -0
  122. package/lib/source-approval-authority.js.map +1 -0
  123. package/lib/source-approval-client.d.ts +32 -0
  124. package/lib/source-approval-client.d.ts.map +1 -0
  125. package/lib/source-approval-client.js +175 -0
  126. package/lib/source-approval-client.js.map +1 -0
  127. package/lib/source-build.d.ts +60 -0
  128. package/lib/source-build.d.ts.map +1 -0
  129. package/lib/source-build.js +311 -0
  130. package/lib/source-build.js.map +1 -0
  131. package/lib/source-context.d.ts +24 -0
  132. package/lib/source-context.d.ts.map +1 -0
  133. package/lib/source-context.js +99 -0
  134. package/lib/source-context.js.map +1 -0
  135. package/lib/source-job-types.d.ts +87 -0
  136. package/lib/source-job-types.d.ts.map +1 -0
  137. package/lib/source-job-types.js +2 -0
  138. package/lib/source-job-types.js.map +1 -0
  139. package/lib/source-jobs.d.ts +113 -0
  140. package/lib/source-jobs.d.ts.map +1 -0
  141. package/lib/source-jobs.js +535 -0
  142. package/lib/source-jobs.js.map +1 -0
  143. package/lib/source-release-authority.d.ts +35 -0
  144. package/lib/source-release-authority.d.ts.map +1 -0
  145. package/lib/source-release-authority.js +271 -0
  146. package/lib/source-release-authority.js.map +1 -0
  147. package/lib/source-release-client.d.ts +10 -0
  148. package/lib/source-release-client.d.ts.map +1 -0
  149. package/lib/source-release-client.js +24 -0
  150. package/lib/source-release-client.js.map +1 -0
  151. package/lib/source-release-runner.d.ts +31 -0
  152. package/lib/source-release-runner.d.ts.map +1 -0
  153. package/lib/source-release-runner.js +125 -0
  154. package/lib/source-release-runner.js.map +1 -0
  155. package/lib/source-versioning.d.ts +24 -0
  156. package/lib/source-versioning.d.ts.map +1 -0
  157. package/lib/source-versioning.js +213 -0
  158. package/lib/source-versioning.js.map +1 -0
  159. package/lib/source-workspace.d.ts +117 -0
  160. package/lib/source-workspace.d.ts.map +1 -0
  161. package/lib/source-workspace.js +458 -0
  162. package/lib/source-workspace.js.map +1 -0
  163. package/lib/sqlite.d.ts +1 -1
  164. package/lib/sqlite.d.ts.map +1 -1
  165. package/lib/sqlite.js +509 -7
  166. package/lib/sqlite.js.map +1 -1
  167. package/lib/store.d.ts +229 -6
  168. package/lib/store.d.ts.map +1 -1
  169. package/lib/store.js +2431 -383
  170. package/lib/store.js.map +1 -1
  171. package/lib/task-observation-authority.d.ts +29 -0
  172. package/lib/task-observation-authority.d.ts.map +1 -0
  173. package/lib/task-observation-authority.js +370 -0
  174. package/lib/task-observation-authority.js.map +1 -0
  175. package/lib/task-observation-client.d.ts +4 -0
  176. package/lib/task-observation-client.d.ts.map +1 -0
  177. package/lib/task-observation-client.js +20 -0
  178. package/lib/task-observation-client.js.map +1 -0
  179. package/lib/task-observation-runtime.d.ts +59 -0
  180. package/lib/task-observation-runtime.d.ts.map +1 -0
  181. package/lib/task-observation-runtime.js +331 -0
  182. package/lib/task-observation-runtime.js.map +1 -0
  183. package/lib/task-observation-store.d.ts +17 -0
  184. package/lib/task-observation-store.d.ts.map +1 -0
  185. package/lib/task-observation-store.js +115 -0
  186. package/lib/task-observation-store.js.map +1 -0
  187. package/lib/task-observation-types.d.ts +67 -0
  188. package/lib/task-observation-types.d.ts.map +1 -0
  189. package/lib/task-observation-types.js +2 -0
  190. package/lib/task-observation-types.js.map +1 -0
  191. package/lib/trust.d.ts +3 -0
  192. package/lib/trust.d.ts.map +1 -1
  193. package/lib/trust.js +0 -0
  194. package/lib/trust.js.map +1 -1
  195. package/lib/types.d.ts +275 -8
  196. package/lib/types.d.ts.map +1 -1
  197. package/lib/version.d.ts +1 -1
  198. package/lib/version.js +1 -1
  199. package/package.json +40 -5
package/lib/release.js CHANGED
@@ -1,9 +1,9 @@
1
- import { spawn } from 'node:child_process';
2
1
  import { createHash, createPublicKey, verify } from 'node:crypto';
3
2
  import { constants as fsConstants } from 'node:fs';
4
3
  import { lstat, open, realpath } from 'node:fs/promises';
5
4
  import { dirname, isAbsolute, posix, resolve } from 'node:path';
6
5
  import { catalogAdmissionId, parseCatalog } from './catalog.js';
6
+ import { ControlledProcessError, executeControlledProcess } from './adapter-process.js';
7
7
  import { ControlPlaneStoreError } from './store.js';
8
8
  import { inheritedReleaseAdapterEnvironment, openTrustedExecutable, verifyOpenTrustedExecutable } from './trust.js';
9
9
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u;
@@ -273,7 +273,7 @@ export function parseSourcePublishReconciliationRequest(value) {
273
273
  }
274
274
  return request;
275
275
  }
276
- function parseSourcePublishReconciliationEvidence(value) {
276
+ function parseSourcePublishReconciliationEvidenceV1(value) {
277
277
  const item = record(value, 'source publish reconciliation evidence');
278
278
  exact(item, ['kind', 'outcome', 'registryId', 'registryReference', 'packageName', 'packageVersion', 'expectedTarballSha256',
279
279
  'expectedTarballIntegrity', 'expectedArtifactStatementDigest', 'expectedArtifactSignatureDigest',
@@ -326,17 +326,90 @@ function parseSourcePublishReconciliationEvidence(value) {
326
326
  }
327
327
  return Object.freeze(evidence);
328
328
  }
329
+ function bareHttpsUrl(value, label, allowEncodedSlash = false) {
330
+ const raw = opaqueLine(value, label);
331
+ let parsed;
332
+ try {
333
+ parsed = new URL(raw);
334
+ }
335
+ catch {
336
+ throw new ControlPlaneStoreError('invalid-input', `${label} is invalid`);
337
+ }
338
+ const canonical = raw === parsed.href || (parsed.pathname === '/' && raw === parsed.href.slice(0, -1));
339
+ if (parsed.protocol !== 'https:' || parsed.username !== '' || parsed.password !== '' || parsed.search !== '' || parsed.hash !== ''
340
+ || !canonical || /%(?:25|5c|2e)|%(?![a-f0-9]{2})/iu.test(parsed.pathname) || (!allowEncodedSlash && /%2f/iu.test(parsed.pathname))
341
+ || parsed.pathname.includes('//')) {
342
+ throw new ControlPlaneStoreError('invalid-input', `${label} is not a bare canonical HTTPS URL`);
343
+ }
344
+ return raw;
345
+ }
346
+ function parseSourceNpmPublishReconciliationEvidence(value) {
347
+ const item = record(value, 'npm publish reconciliation evidence');
348
+ exact(item, ['kind', 'outcome', 'registryId', 'registryReference', 'packageName', 'packageVersion', 'expectedTarballSha256',
349
+ 'expectedTarballIntegrity', 'expectedArtifactStatementDigest', 'expectedArtifactSignatureDigest', 'observedTarballSha256',
350
+ 'observedTarballIntegrity', 'ambiguousPublishOperationId', 'ambiguousPublishReceiptDigest', 'detailDigest',
351
+ 'metadataReference', 'metadataIntegrity', 'downloadedBytes'], 'npm publish reconciliation evidence');
352
+ if (item.kind !== 'npm-publish-reconciliation' || !['exists-match', 'unknown', 'digest-conflict'].includes(String(item.outcome))) {
353
+ throw new ControlPlaneStoreError('invalid-input', 'npm publish reconciliation evidence outcome is invalid');
354
+ }
355
+ const nullableDigest = (raw, label) => raw === null ? null : digestText(raw, label);
356
+ const nullableIntegrity = (raw, label) => raw === null ? null : integrity(raw, label);
357
+ const nullableUrl = (raw, label, allowEncodedSlash = false) => raw === null ? null
358
+ : bareHttpsUrl(raw, label, allowEncodedSlash);
359
+ const evidence = {
360
+ kind: 'npm-publish-reconciliation',
361
+ outcome: item.outcome,
362
+ registryId: text(item.registryId, 'registryId'), registryReference: nullableUrl(item.registryReference, 'registryReference'),
363
+ packageName: text(item.packageName, 'packageName', PACKAGE), packageVersion: text(item.packageVersion, 'packageVersion', VERSION),
364
+ expectedTarballSha256: digestText(item.expectedTarballSha256, 'expectedTarballSha256'),
365
+ expectedTarballIntegrity: integrity(item.expectedTarballIntegrity, 'expectedTarballIntegrity'),
366
+ expectedArtifactStatementDigest: digestText(item.expectedArtifactStatementDigest, 'expectedArtifactStatementDigest'),
367
+ expectedArtifactSignatureDigest: digestText(item.expectedArtifactSignatureDigest, 'expectedArtifactSignatureDigest'),
368
+ observedTarballSha256: nullableDigest(item.observedTarballSha256, 'observedTarballSha256'),
369
+ observedTarballIntegrity: nullableIntegrity(item.observedTarballIntegrity, 'observedTarballIntegrity'),
370
+ ambiguousPublishOperationId: text(item.ambiguousPublishOperationId, 'ambiguousPublishOperationId'),
371
+ ambiguousPublishReceiptDigest: digestText(item.ambiguousPublishReceiptDigest, 'ambiguousPublishReceiptDigest'),
372
+ detailDigest: digestText(item.detailDigest, 'detailDigest'), metadataReference: nullableUrl(item.metadataReference, 'metadataReference', true),
373
+ metadataIntegrity: nullableIntegrity(item.metadataIntegrity, 'metadataIntegrity'),
374
+ downloadedBytes: item.downloadedBytes === null ? null : integer(item.downloadedBytes, 'downloadedBytes', 1),
375
+ };
376
+ if (evidence.downloadedBytes !== null && evidence.downloadedBytes > 268_435_456) {
377
+ throw new ControlPlaneStoreError('invalid-input', 'downloadedBytes must be a bounded integer');
378
+ }
379
+ const fullyObserved = evidence.registryReference !== null && evidence.metadataReference !== null && evidence.metadataIntegrity !== null
380
+ && evidence.downloadedBytes !== null && evidence.observedTarballSha256 !== null && evidence.observedTarballIntegrity !== null
381
+ && evidence.metadataIntegrity === evidence.observedTarballIntegrity;
382
+ if (evidence.outcome === 'exists-match' && (!fullyObserved || evidence.observedTarballSha256 !== evidence.expectedTarballSha256
383
+ || evidence.observedTarballIntegrity !== evidence.expectedTarballIntegrity)) {
384
+ throw new ControlPlaneStoreError('invalid-input', 'npm exists-match reconciliation does not prove the exact artifact');
385
+ }
386
+ if (evidence.outcome === 'digest-conflict' && (!fullyObserved || (evidence.observedTarballSha256 === evidence.expectedTarballSha256
387
+ && evidence.observedTarballIntegrity === evidence.expectedTarballIntegrity))) {
388
+ throw new ControlPlaneStoreError('invalid-input', 'npm digest-conflict reconciliation does not prove a conflicting artifact');
389
+ }
390
+ if (evidence.outcome === 'unknown' && (evidence.registryReference !== null || evidence.metadataReference !== null
391
+ || evidence.metadataIntegrity !== null || evidence.downloadedBytes !== null || evidence.observedTarballSha256 !== null
392
+ || evidence.observedTarballIntegrity !== null)) {
393
+ throw new ControlPlaneStoreError('invalid-input', 'npm unknown reconciliation contains observed registry state');
394
+ }
395
+ return Object.freeze(evidence);
396
+ }
329
397
  export function sourcePublishReconciliationRequestDigest(value) { return digest(value); }
330
398
  export function sourcePublishReconciliationEvidenceDigest(value) { return digest(value); }
331
399
  export function parseSourcePublishReconciliationReceipt(value) {
332
400
  const item = record(value, 'source publish reconciliation receipt');
333
401
  exact(item, ['schemaVersion', 'kind', 'receiptId', 'authority', 'keyId', 'installationId', 'planId', 'planDigest', 'releaseId',
334
402
  'fence', 'operationId', 'requestDigest', 'evidence', 'evidenceDigest', 'observedAt', 'expiresAt', 'signature'], 'source publish reconciliation receipt');
335
- if (item.schemaVersion !== 1 || item.kind !== 'dsh-source-publish-reconciliation-receipt') {
403
+ if ((item.schemaVersion !== 1 && item.schemaVersion !== 2) || item.kind !== 'dsh-source-publish-reconciliation-receipt') {
336
404
  throw new ControlPlaneStoreError('invalid-input', 'unsupported source publish reconciliation receipt schema');
337
405
  }
338
- const evidence = parseSourcePublishReconciliationEvidence(item.evidence);
339
- const receipt = { schemaVersion: 1, kind: 'dsh-source-publish-reconciliation-receipt',
406
+ const evidence = item.schemaVersion === 1 ? parseSourcePublishReconciliationEvidenceV1(item.evidence)
407
+ : parseSourceNpmPublishReconciliationEvidence(item.evidence);
408
+ if ((item.schemaVersion === 1 && evidence.kind !== 'publish-reconciliation')
409
+ || (item.schemaVersion === 2 && evidence.kind !== 'npm-publish-reconciliation')) {
410
+ throw new ControlPlaneStoreError('invalid-input', 'publish reconciliation receipt schema and evidence kind disagree');
411
+ }
412
+ const receipt = { schemaVersion: item.schemaVersion, kind: 'dsh-source-publish-reconciliation-receipt',
340
413
  receiptId: text(item.receiptId, 'receiptId'), authority: text(item.authority, 'authority'), keyId: text(item.keyId, 'keyId'),
341
414
  installationId: text(item.installationId, 'installationId', /^[a-f0-9-]{36}$/u), planId: text(item.planId, 'planId'),
342
415
  planDigest: digestText(item.planDigest, 'planDigest'), releaseId: text(item.releaseId, 'releaseId'), fence: integer(item.fence, 'fence', 1),
@@ -355,6 +428,19 @@ function canonicalReconciliationReceipt(receipt) {
355
428
  export function sourcePublishReconciliationSigningPayload(receipt) {
356
429
  return canonicalReconciliationReceipt({ ...receipt, signature: '' });
357
430
  }
431
+ function npmRegistryBase(locator) {
432
+ const raw = bareHttpsUrl(locator, 'registry.locator');
433
+ const parsed = new URL(raw);
434
+ if (!parsed.pathname.endsWith('/'))
435
+ parsed.pathname = `${parsed.pathname}/`;
436
+ return parsed;
437
+ }
438
+ function npmUrlUnderBase(reference, base, label) {
439
+ const parsed = new URL(reference);
440
+ if (parsed.origin !== base.origin || !parsed.pathname.startsWith(base.pathname)) {
441
+ throw new ControlPlaneStoreError('conflict', `${label} is not under the configured npm registry`);
442
+ }
443
+ }
358
444
  export class Ed25519SourcePublishReconciliationAuthority {
359
445
  publicKey;
360
446
  expectedAuthority;
@@ -384,8 +470,9 @@ export class Ed25519SourcePublishReconciliationAuthority {
384
470
  || evidence.expectedTarballIntegrity !== request.artifact.tarballIntegrity
385
471
  || evidence.expectedArtifactStatementDigest !== request.expectedArtifactStatementDigest
386
472
  || evidence.expectedArtifactSignatureDigest !== request.expectedArtifactSignatureDigest
387
- || (evidence.outcome === 'exists-match' && (evidence.observedArtifactStatementDigest !== request.expectedArtifactStatementDigest
388
- || evidence.observedArtifactSignatureDigest !== request.expectedArtifactSignatureDigest))
473
+ || (evidence.kind === 'publish-reconciliation' && evidence.outcome === 'exists-match'
474
+ && (evidence.observedArtifactStatementDigest !== request.expectedArtifactStatementDigest
475
+ || evidence.observedArtifactSignatureDigest !== request.expectedArtifactSignatureDigest))
389
476
  || evidence.ambiguousPublishOperationId !== request.ambiguousPublish.operationId
390
477
  || evidence.ambiguousPublishReceiptDigest !== request.ambiguousPublish.receiptDigest
391
478
  || (evidence.registryReference !== null && evidence.registryReference !== request.expectedRegistryReference)) {
@@ -396,6 +483,18 @@ export class Ed25519SourcePublishReconciliationAuthority {
396
483
  || receipt.expiresAt - receipt.observedAt > request.receiptTtlMs) {
397
484
  throw new ControlPlaneStoreError('expired', 'publish reconciliation receipt is outside its request-bound validity interval');
398
485
  }
486
+ if (evidence.kind === 'npm-publish-reconciliation') {
487
+ const base = npmRegistryBase(request.registry.locator);
488
+ const metadataReference = `${base.href}${encodeURIComponent(request.artifact.packageName)}/${request.artifact.packageVersion}`;
489
+ if (evidence.registryReference !== null)
490
+ npmUrlUnderBase(evidence.registryReference, base, 'registryReference');
491
+ if (evidence.metadataReference !== null)
492
+ npmUrlUnderBase(evidence.metadataReference, base, 'metadataReference');
493
+ if ((evidence.registryReference !== null && evidence.registryReference !== request.expectedRegistryReference)
494
+ || (evidence.metadataReference !== null && evidence.metadataReference !== metadataReference)) {
495
+ throw new ControlPlaneStoreError('conflict', 'npm reconciliation receipt is not bound to the exact npm registry URLs');
496
+ }
497
+ }
399
498
  const signatureBytes = Buffer.from(receipt.signature, 'base64');
400
499
  if (!verify(null, Buffer.from(canonicalReconciliationReceipt(receipt)), createPublicKey(this.publicKey), signatureBytes)) {
401
500
  throw new ControlPlaneStoreError('invalid-input', 'publish reconciliation receipt signature is invalid');
@@ -1035,7 +1134,8 @@ export class ReleaseAdapterError extends Error {
1035
1134
  this.name = 'ReleaseAdapterError';
1036
1135
  }
1037
1136
  }
1038
- async function executePinned(executable, interpreter, args, environment, timeoutMs, stdin, maximumOutput, phase, inherited = []) {
1137
+ async function executePinned(executable, interpreter, args, environment, timeoutMs, stdin, maximumOutput, phase, inherited = [], signal) {
1138
+ signal?.throwIfAborted();
1039
1139
  if (process.platform !== 'linux')
1040
1140
  throw new ReleaseAdapterError('FAILED', phase, 'descriptor-pinned adapters require Linux');
1041
1141
  try {
@@ -1044,50 +1144,32 @@ async function executePinned(executable, interpreter, args, environment, timeout
1044
1144
  catch {
1045
1145
  throw new ReleaseAdapterError('FAILED', phase, 'descriptor-pinned adapters require /proc/self/fd');
1046
1146
  }
1047
- return new Promise((resolvePromise, reject) => {
1048
- const executableFd = 3 + inherited.length;
1049
- const interpreterFd = interpreter === undefined ? undefined : executableFd + 1;
1050
- const command = `/proc/self/fd/${interpreterFd ?? executableFd}`;
1051
- const commandArguments = interpreter === undefined ? [...args] : [`/proc/self/fd/${executableFd}`, ...args];
1052
- const stdio = ['pipe', 'pipe', 'ignore', ...inherited.map(handle => handle.fd), executable.handle.fd];
1053
- if (interpreter !== undefined)
1054
- stdio.push(interpreter.handle.fd);
1055
- const child = spawn(command, commandArguments, { env: environment, shell: false, stdio });
1056
- const chunks = [];
1057
- let bytes = 0;
1058
- let timedOut = false;
1059
- let outputLimit = false;
1060
- let settled = false;
1061
- child.stdout.on('data', (chunk) => { bytes += chunk.length; if (bytes > maximumOutput) {
1062
- outputLimit = true;
1063
- child.kill('SIGKILL');
1064
- }
1065
- else
1066
- chunks.push(chunk); });
1067
- child.once('error', () => { if (!settled) {
1068
- settled = true;
1069
- reject(new ReleaseAdapterError('FAILED', phase, 'adapter could not start'));
1070
- } });
1071
- child.once('close', code => {
1072
- if (settled)
1073
- return;
1074
- settled = true;
1075
- if (timedOut)
1076
- reject(new ReleaseAdapterError('TIMEOUT', phase, 'adapter exceeded its deadline'));
1077
- else if (outputLimit)
1078
- reject(new ReleaseAdapterError('OUTPUT_LIMIT', phase, 'adapter exceeded its output bound'));
1079
- else if (code !== 0)
1080
- reject(new ReleaseAdapterError('FAILED', phase, 'adapter returned a non-zero status'));
1081
- else
1082
- resolvePromise(Buffer.concat(chunks).toString('utf8'));
1083
- });
1084
- const timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, timeoutMs);
1085
- child.once('close', () => clearTimeout(timer));
1086
- child.stdin.end(stdin, 'utf8');
1087
- });
1147
+ const executableFd = 3 + inherited.length;
1148
+ const interpreterFd = interpreter === undefined ? undefined : executableFd + 1;
1149
+ const command = `/proc/self/fd/${interpreterFd ?? executableFd}`;
1150
+ const commandArguments = interpreter === undefined ? [...args] : [`/proc/self/fd/${executableFd}`, ...args];
1151
+ const stdio = ['pipe', 'pipe', 'ignore', ...inherited.map(handle => handle.fd), executable.handle.fd];
1152
+ if (interpreter !== undefined)
1153
+ stdio.push(interpreter.handle.fd);
1154
+ try {
1155
+ return await executeControlledProcess({ command, args: commandArguments, env: environment, stdio,
1156
+ stdin, timeoutMs, maximumOutput, ...(signal ? { signal } : {}) });
1157
+ }
1158
+ catch (error) {
1159
+ if (!(error instanceof ControlledProcessError))
1160
+ throw error;
1161
+ if (error.code === 'ABORTED')
1162
+ throw new ReleaseAdapterError('ABORTED', phase, 'adapter cancelled');
1163
+ if (error.code === 'TIMEOUT')
1164
+ throw new ReleaseAdapterError('TIMEOUT', phase, 'adapter exceeded its deadline');
1165
+ if (error.code === 'OUTPUT_LIMIT')
1166
+ throw new ReleaseAdapterError('OUTPUT_LIMIT', phase, 'adapter exceeded its output bound');
1167
+ throw new ReleaseAdapterError('FAILED', phase, error.code === 'NON_ZERO'
1168
+ ? 'adapter returned a non-zero status' : 'adapter could not be safely reclaimed');
1169
+ }
1088
1170
  }
1089
- async function assertPinnedAdapterCapabilities(executable, interpreter, adapter, phase, environment) {
1090
- const source = await executePinned(executable, interpreter, ['--capabilities'], environment, adapter.timeoutMs, undefined, 1_024, phase);
1171
+ async function assertPinnedAdapterCapabilities(executable, interpreter, adapter, phase, environment, signal) {
1172
+ const source = await executePinned(executable, interpreter, ['--capabilities'], environment, adapter.timeoutMs, undefined, 1_024, phase, [], signal);
1091
1173
  let value;
1092
1174
  try {
1093
1175
  value = JSON.parse(source);
@@ -1122,7 +1204,8 @@ async function withPinnedAdapter(adapter, phase, action) {
1122
1204
  await executable.handle.close();
1123
1205
  }
1124
1206
  }
1125
- export async function invokeSourceReleaseAdapter(trust, request) {
1207
+ export async function invokeSourceReleaseAdapter(trust, request, signal) {
1208
+ signal?.throwIfAborted();
1126
1209
  const parsedRequest = parseSourceReleaseRequest(request);
1127
1210
  const adapter = trust.releaseAdapters?.[parsedRequest.phase];
1128
1211
  if (adapter === undefined)
@@ -1131,17 +1214,17 @@ export async function invokeSourceReleaseAdapter(trust, request) {
1131
1214
  interpreter: adapter.interpreter, authority: adapter.authority, keyId: adapter.keyId };
1132
1215
  if (digest(identity) !== digest(parsedRequest.adapter))
1133
1216
  throw new ReleaseAdapterError('FAILED', parsedRequest.phase, 'durable request is not bound to the configured adapter');
1134
- return withPinnedAdapter(adapter, parsedRequest.phase, async (executable, interpreter) => {
1217
+ const receipt = await withPinnedAdapter(adapter, parsedRequest.phase, async (executable, interpreter) => {
1135
1218
  const environment = inheritedReleaseAdapterEnvironment(trust, parsedRequest.phase);
1136
- const version = (await executePinned(executable, interpreter, ['--version'], environment, adapter.timeoutMs, undefined, 1_024, parsedRequest.phase)).trim();
1219
+ const version = (await executePinned(executable, interpreter, ['--version'], environment, adapter.timeoutMs, undefined, 1_024, parsedRequest.phase, [], signal)).trim();
1137
1220
  if (version !== adapter.version)
1138
1221
  throw new ReleaseAdapterError('VERSION_MISMATCH', parsedRequest.phase, 'adapter reported a different version');
1139
- await assertPinnedAdapterCapabilities(executable, interpreter, adapter, parsedRequest.phase, environment);
1222
+ await assertPinnedAdapterCapabilities(executable, interpreter, adapter, parsedRequest.phase, environment, signal);
1140
1223
  const artifact = 'artifact' in parsedRequest.input ? await openArtifactSnapshot(parsedRequest.input.artifact) : undefined;
1141
1224
  try {
1142
1225
  const artifactEnvironment = artifact === undefined ? environment : { ...environment, DSH_RELEASE_TARBALL_FD: '3',
1143
1226
  DSH_RELEASE_SBOM_FD: '4', DSH_RELEASE_PROVENANCE_FD: '5' };
1144
- const source = await executePinned(executable, interpreter, ['release'], artifactEnvironment, adapter.timeoutMs, `${JSON.stringify(parsedRequest)}\n`, 262_144, parsedRequest.phase, artifact?.handles ?? []);
1227
+ const source = await executePinned(executable, interpreter, ['release'], artifactEnvironment, adapter.timeoutMs, `${JSON.stringify(parsedRequest)}\n`, 262_144, parsedRequest.phase, artifact?.handles ?? [], signal);
1145
1228
  if (artifact !== undefined)
1146
1229
  await verifyOpenArtifactSnapshot(artifact);
1147
1230
  let value;
@@ -1151,6 +1234,7 @@ export async function invokeSourceReleaseAdapter(trust, request) {
1151
1234
  catch {
1152
1235
  throw new ReleaseAdapterError('FAILED', parsedRequest.phase, 'adapter did not return one JSON receipt');
1153
1236
  }
1237
+ signal?.throwIfAborted();
1154
1238
  return parseSourceReleaseReceipt(value);
1155
1239
  }
1156
1240
  finally {
@@ -1158,6 +1242,8 @@ export async function invokeSourceReleaseAdapter(trust, request) {
1158
1242
  await Promise.all(artifact.handles.map(async (handle) => handle.close()));
1159
1243
  }
1160
1244
  });
1245
+ signal?.throwIfAborted();
1246
+ return receipt;
1161
1247
  }
1162
1248
  export async function invokeSourcePublishReconciliationAdapter(trust, request) {
1163
1249
  const parsedRequest = parseSourcePublishReconciliationRequest(request);