@casecore/release-agent 0.1.3 → 0.1.6

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.
@@ -3,16 +3,18 @@ import { blocker, smokeScenarios, safeStatus } from './producer-support.mjs';
3
3
  import path from 'node:path';
4
4
  import { createProtectedJsonStore } from './journal.mjs';
5
5
  import { validProducerState } from './producer-journal.mjs';
6
- import { installOperationBarrier, markOperationStateLoaded, reserveExclusiveOperation } from './exclusive-operation.mjs';
6
+ import {
7
+ installOperationBarrier,
8
+ markOperationStateLoaded,
9
+ reserveExclusiveOperation,
10
+ } from './exclusive-operation.mjs';
7
11
 
8
12
  export class ProducerState {
9
- constructor({
10
- installationKey,
11
- journalFile,
12
- provider,
13
- now = () => new Date(),
14
- sourcePolicy,
15
- }) {
13
+ #engineInstance;
14
+ static isEngineInstance(value) {
15
+ return value !== null && typeof value === 'object' && #engineInstance in value;
16
+ }
17
+ constructor({ installationKey, journalFile, provider, now = () => new Date(), sourcePolicy }) {
16
18
  assert.match(installationKey, /^[\w.-]{1,200}$/);
17
19
  assert.ok(path.isAbsolute(journalFile));
18
20
  Object.assign(this, { installationKey, journalFile, provider, now, sourcePolicy });
@@ -26,7 +28,7 @@ export class ProducerState {
26
28
  (Number.isSafeInteger(provider.snapshotTimeoutMs) &&
27
29
  provider.snapshotTimeoutMs > 0 &&
28
30
  provider.snapshotTimeoutMs <= 30000),
29
- 'Invalid fixed provider snapshot budget',
31
+ 'Invalid fixed provider snapshot budget'
30
32
  );
31
33
  this.state = {
32
34
  schema: 1,
@@ -47,9 +49,7 @@ export class ProducerState {
47
49
  async initialize() {
48
50
  this.state = this.journal.read();
49
51
  // A restarted process must reconcile a previously admitted mutation first.
50
- for (const run of this.state.runs.filter((r) =>
51
- ['running', 'queued'].includes(r.status),
52
- )) {
52
+ for (const run of this.state.runs.filter((r) => ['running', 'queued'].includes(r.status))) {
53
53
  if (
54
54
  run.status === 'queued' &&
55
55
  run.lane === 'scheduled' &&
@@ -62,7 +62,7 @@ export class ProducerState {
62
62
  blocker(
63
63
  'RECONCILIATION_REQUIRED',
64
64
  'Предыдущее выполнение требует проверки фактического состояния',
65
- true,
65
+ true
66
66
  ),
67
67
  ];
68
68
  }
@@ -108,15 +108,9 @@ export class ProducerState {
108
108
  occurredAt: this.now().toISOString(),
109
109
  code,
110
110
  frames,
111
- ...(smokeScenarios.has(error?.scenario)
112
- ? { scenario: error.scenario }
113
- : {}),
114
- ...(safeStatus(error?.expectedStatus)
115
- ? { expectedStatus: error.expectedStatus }
116
- : {}),
117
- ...(safeStatus(error?.actualStatus)
118
- ? { actualStatus: error.actualStatus }
119
- : {}),
111
+ ...(smokeScenarios.has(error?.scenario) ? { scenario: error.scenario } : {}),
112
+ ...(safeStatus(error?.expectedStatus) ? { expectedStatus: error.expectedStatus } : {}),
113
+ ...(safeStatus(error?.actualStatus) ? { actualStatus: error.actualStatus } : {}),
120
114
  ...([
121
115
  'ENOTFOUND',
122
116
  'EAI_AGAIN',
@@ -131,9 +125,7 @@ export class ProducerState {
131
125
  ? { transportCode: error.transportCode }
132
126
  : {}),
133
127
  };
134
- this.state.diagnostics = [...(this.state.diagnostics ?? []), item].slice(
135
- -200,
136
- );
128
+ this.state.diagnostics = [...(this.state.diagnostics ?? []), item].slice(-200);
137
129
  return code;
138
130
  }
139
131
  async snapshot(after = '0', { deadlineAt } = {}) {
@@ -144,8 +136,8 @@ export class ProducerState {
144
136
  this.provider.snapshotTimeoutMs ?? 10000,
145
137
  deadlineAt
146
138
  ? Date.parse(deadlineAt) - this.now().getTime()
147
- : (this.provider.snapshotTimeoutMs ?? 10000),
148
- ),
139
+ : (this.provider.snapshotTimeoutMs ?? 10000)
140
+ )
149
141
  );
150
142
  const controller = new AbortController();
151
143
  let timer;
@@ -155,22 +147,17 @@ export class ProducerState {
155
147
  reject(
156
148
  Object.assign(Error('Producer status read timed out'), {
157
149
  code: 'PRODUCER_STATUS_TIMEOUT',
158
- }),
150
+ })
159
151
  );
160
152
  }, limit);
161
153
  });
162
154
  let data;
163
155
  try {
164
- data = await Promise.race([
165
- this.provider.snapshot({ signal: controller.signal }),
166
- timeout,
167
- ]);
156
+ data = await Promise.race([this.provider.snapshot({ signal: controller.signal }), timeout]);
168
157
  } finally {
169
158
  clearTimeout(timer);
170
159
  }
171
- const candidates = new Map(
172
- data.candidates.map((candidate) => [candidate.id, candidate]),
173
- );
160
+ const candidates = new Map(data.candidates.map((candidate) => [candidate.id, candidate]));
174
161
  for (const item of Object.values(this.state.commands)) {
175
162
  if (
176
163
  item.command.action !== 'publish' ||
@@ -13,6 +13,56 @@ export * from './protocol.js';
13
13
  export interface ProviderContext {
14
14
  signal: AbortSignal;
15
15
  }
16
+ declare const publicationTerminalBrand: unique symbol;
17
+ export interface PublicationTerminalAuthority {
18
+ readonly [publicationTerminalBrand]: true;
19
+ }
20
+ export interface PublicationTerminalBindings {
21
+ action: 'publish' | 'deploy';
22
+ commandId: string;
23
+ runId: string;
24
+ installationKey: string;
25
+ repository: string;
26
+ ref: string;
27
+ provider: RunnerCommand['executionProvider'];
28
+ candidateId: string;
29
+ delivery: OriginalDelivery;
30
+ }
31
+ export interface PublicationTerminalProof {
32
+ schema: 1;
33
+ bindings: PublicationTerminalBindings;
34
+ status: 'deployed' | 'failed';
35
+ completedAt: string;
36
+ artifactIdentity: ArtifactIdentity | null;
37
+ readback: ProductionReadback;
38
+ failure: PublicationFailureFact | null;
39
+ }
40
+ export function readPublicationTerminalAuthority(
41
+ authority: PublicationTerminalAuthority,
42
+ bindings: PublicationTerminalBindings
43
+ ): PublicationTerminalProof;
44
+ export interface PublicationTerminalReceipt {
45
+ schema: 1;
46
+ kind: 'publication_terminal';
47
+ issuedAt: string;
48
+ proof: PublicationTerminalProof;
49
+ mac: string;
50
+ }
51
+ export function sealPublicationTerminalReceipt(
52
+ authority: PublicationTerminalAuthority,
53
+ bindings: PublicationTerminalBindings,
54
+ token: string
55
+ ): Promise<PublicationTerminalReceipt>;
56
+ /** Authenticated data, never an execution capability. */
57
+ export function verifyPublicationTerminalReceipt(
58
+ receipt: PublicationTerminalReceipt,
59
+ token: string
60
+ ): PublicationTerminalReceipt;
61
+ export interface TerminalVerificationContext extends ProviderContext {
62
+ completedAt: string;
63
+ status: 'deployed' | 'failed';
64
+ artifactIdentity?: ArtifactIdentity;
65
+ }
16
66
  export interface ExecutionContext {
17
67
  stage<T>(
18
68
  id: StageId,
@@ -39,6 +89,13 @@ export interface ReleaseProvider {
39
89
  actions: RunnerCommand['action'][];
40
90
  snapshotTimeoutMs?: number;
41
91
  remotePublication?: boolean;
92
+ publicationTerminal?(
93
+ command: RunnerCommand,
94
+ context: ProviderContext & {
95
+ authority: PublicationTerminalAuthority;
96
+ bindings: PublicationTerminalBindings;
97
+ }
98
+ ): Promise<void>;
42
99
  snapshot(context: ProviderContext): Promise<{
43
100
  connection: ProducerSnapshot['connection'];
44
101
  installed?: ProductionReadback | null;
@@ -56,9 +113,21 @@ export interface ReleaseProvider {
56
113
  verifyPublicationArtifact?(
57
114
  command: RunnerCommand,
58
115
  identity: ArtifactIdentity,
59
- delivery: OriginalDelivery
116
+ delivery: OriginalDelivery,
117
+ context?: TerminalVerificationContext
118
+ ): Promise<boolean>;
119
+ verifyPublicationDelivery?(
120
+ command: RunnerCommand,
121
+ delivery: OriginalDelivery,
122
+ context?: TerminalVerificationContext
123
+ ): Promise<boolean>;
124
+ verifyArtifactDelivery?(
125
+ command: RunnerCommand,
126
+ identity: ArtifactIdentity,
127
+ delivery: OriginalDelivery,
128
+ source: { repository: string; ref: string; sourceCommit: string },
129
+ context: TerminalVerificationContext
60
130
  ): Promise<boolean>;
61
- verifyPublicationDelivery?(command: RunnerCommand, delivery: OriginalDelivery): Promise<boolean>;
62
131
  reconcilePublicationFailure?(
63
132
  command: RunnerCommand,
64
133
  context: ProviderContext & { delivery: OriginalDelivery | null; recoveryDeadlineAt: number }
@@ -152,6 +221,7 @@ export function producerServer(
152
221
  workflowRunHeader?: string;
153
222
  claimPublication?(commandId: string, workflowRunId: number): Promise<unknown>;
154
223
  reportPublication?(commandId: string, report: unknown): Promise<unknown>;
224
+ readTerminalReceipt?(commandId: string): Promise<PublicationTerminalReceipt | null | undefined>;
155
225
  configure?(request: ConfigurationRequest): Promise<ConfigurationAcknowledgement | undefined>;
156
226
  reconcileConfiguration?(
157
227
  configurationId: string
package/dist/producer.mjs CHANGED
@@ -3,6 +3,12 @@ import { sameIdentity, acceptedProduction } from './protocol.mjs';
3
3
  import { blocker, failureMessage, failedProduction } from './producer-support.mjs';
4
4
  import { ProducerExecution } from './producer-execution.mjs';
5
5
  import { reconcilePublicationFailure } from './producer-publication-failure.mjs';
6
+ import { notifyPublicationTerminal } from './publication-terminal.mjs';
7
+ export {
8
+ readPublicationTerminalAuthority,
9
+ sealPublicationTerminalReceipt,
10
+ verifyPublicationTerminalReceipt,
11
+ } from './publication-terminal.mjs';
6
12
  export { publicationFailureProofDigest } from './producer-publication-failure.mjs';
7
13
  export {
8
14
  sameIdentity,
@@ -18,6 +24,43 @@ export class ReleaseControlProducer extends ProducerExecution {
18
24
  const item = this.state.commands[commandId];
19
25
  assert.ok(item, 'Unknown command');
20
26
  const run = this.state.runs.find((r) => r.commandId === commandId);
27
+ if (
28
+ ['publish', 'deploy'].includes(item.command.action) &&
29
+ typeof this.provider.publicationTerminal === 'function'
30
+ ) {
31
+ if (run?.status === 'failed' && item.publicationFailureReconciliation?.state === 'settled') {
32
+ await reconcilePublicationFailure(this, item, run, { terminalRetry: true });
33
+ return this.acknowledgement(commandId);
34
+ }
35
+ if (run?.status === 'deployed') {
36
+ const controller = new AbortController();
37
+ let timer;
38
+ try {
39
+ const fact = await Promise.race([
40
+ this.provider.readProduction(this.resolvedCommand(item.command), {
41
+ signal: controller.signal,
42
+ }),
43
+ new Promise((_, reject) => {
44
+ timer = setTimeout(() => {
45
+ controller.abort();
46
+ reject(Error('PUBLICATION_TERMINAL_READBACK_TIMEOUT'));
47
+ }, 30000);
48
+ }),
49
+ ]);
50
+ assert.ok(
51
+ acceptedProduction(fact, this.resolvedCommand(item.command), this.now()),
52
+ 'PUBLICATION_TERMINAL_READBACK_REQUIRED'
53
+ );
54
+ run.readback = structuredClone(fact);
55
+ await this.save();
56
+ await notifyPublicationTerminal(this, item, run);
57
+ } finally {
58
+ controller.abort();
59
+ clearTimeout(timer);
60
+ }
61
+ return this.acknowledgement(commandId);
62
+ }
63
+ }
21
64
  if (
22
65
  item.command.action === 'publish' &&
23
66
  run?.status === 'blocked' &&
@@ -140,6 +183,7 @@ export class ReleaseControlProducer extends ProducerExecution {
140
183
  await this.save();
141
184
  }
142
185
  }
186
+ await notifyPublicationTerminal(this, item, run);
143
187
  return this.acknowledgement(commandId);
144
188
  }
145
189
  }
@@ -1,40 +1,42 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "package": "@casecore/release-agent",
4
- "version": "0.1.3",
5
- "sourceCommit": "a9736762537078698065c74bbb0febb675b980c7",
4
+ "version": "0.1.6",
5
+ "sourceCommit": "70442860e0f89db88c78142c6f6a7e8073414af5",
6
6
  "sourceVerified": true,
7
7
  "sourceDirty": false,
8
8
  "buildKind": "release",
9
- "contentDigest": "sha256:21d9013bdd65fefe213eee463eaa9dc13bb2ee1647f392e2c6c90ca5953d2886",
9
+ "contentDigest": "sha256:a41148b8adf594a5da507b1f8495b5b8c63e32b514d815c7bbce2634b0e6fb89",
10
10
  "files": {
11
11
  "src/configuration-hook.mjs": "sha256:1171bf982cbf7cb0092f63b633ef0b0a0947d8ed1106f88c9175c8add33d81b1",
12
- "src/exclusive-operation.mjs": "sha256:5a1d0b7d3aa41b901132e239586d66d6c7aa2c16682a7db033a7aab91b1d781a",
12
+ "src/exclusive-operation.mjs": "sha256:d5c2718a2a4228d09559e914360329383ae276f034bb468c52642a4bd6aa0d99",
13
13
  "src/identity.mjs": "sha256:253021058fc79232d83f3ecba6f1023fd046f27aabdf9c129a103d3c9ccb07f5",
14
14
  "src/index.mjs": "sha256:76bda5e1dc376bb7033d8fdbddb0afa85c7c68bbe028aea078ffceaaa35ec75e",
15
15
  "src/journal.mjs": "sha256:35a2620d56084ec1aa164e09d77b8e77096413d9572f7b601da8ef7149b996bd",
16
16
  "src/pipeline.mjs": "sha256:7a2d39d235ce7160441d177200389c5f3eae1cc494ec36082a23e8d64a0abe3f",
17
- "src/producer-admission.mjs": "sha256:975c7dac25d4c8f94423e5a4db2bd99c556afd6492b26fbda58e841f1df8fc3c",
18
- "src/producer-delivery.mjs": "sha256:b20d9ae20dee90bbbe739245a84c9f4b91b1ab958b5e2995ff8705da24ba3988",
19
- "src/producer-execution.mjs": "sha256:6d9fbca493e4386c92c4d853a66ece3677ec7ab644d6da15b68dd744f30186ef",
20
- "src/producer-journal.mjs": "sha256:0934057702fb611b9a8d9dde58cb2c2024f436a49a4e5f6ab02f8884a320cea7",
21
- "src/producer-publication-failure.mjs": "sha256:726b2b85880143b70219473896b894f64b25b1c495f77488f1e73ce950b839f6",
17
+ "src/producer-admission.mjs": "sha256:03d868141daf92351b3e1a4e70997c87fc39044624644536a374aa86d108d12d",
18
+ "src/producer-delivery.mjs": "sha256:04439a95afcb52d879cc83b99fa1d7716f4dff84bb7c07f88d31f534799e76a8",
19
+ "src/producer-execution.mjs": "sha256:e78724fa2f2fd4946ae8103bb5165b363eb968fab42add4fed572155f51871d9",
20
+ "src/producer-journal.mjs": "sha256:9ec7c5e4ae6add8c0b64644673c3243946175fb5cc2dafcd5f1fdee2c6485ffd",
21
+ "src/producer-publication-failure.mjs": "sha256:9db4effbeae8287070e3c27133abecf2bf435a8c6917c3162f0efd3189ea9445",
22
22
  "src/producer-publication.mjs": "sha256:c09fa07b37507093b71bbaff3af9df8ec2331c0cba24a4b936d247e9b35257f7",
23
- "src/producer-server.mjs": "sha256:def28e10a17eb80fb04ba69b09050731bb005949f949a7f1fb46dae267323434",
24
- "src/producer-state.mjs": "sha256:c3fcdace4c6579be092d39803cfb308e4bf88060b3f75bb7fc8b91284609b4ce",
23
+ "src/producer-server.mjs": "sha256:fc67ceced78239a1d159ff1af82b0061a407891523ad2a1a9139a3d4d158619c",
24
+ "src/producer-state.mjs": "sha256:61d6a914c268f94790c459562436bfe95efd07865ca1052b0b58760d9087d535",
25
25
  "src/producer-support.mjs": "sha256:0a10f6fa50cd8e729ba432640d0845004488ead2bcc1c9383f02548037f83d66",
26
- "src/producer.mjs": "sha256:6d0b875a9901a14edf5b93c771356f5352e924ff7977c13b027343c9bdca42f4",
26
+ "src/producer.mjs": "sha256:d5a9fca0bcb14f8c757d7219fcb8f2d39a2bf7af05c9cce3b8f654fa3060552c",
27
27
  "src/protocol-internals.mjs": "sha256:d08a0c72079d091792c5327adafac5ffc38ea20138a6c44b88879e7157a890b3",
28
28
  "src/protocol.mjs": "sha256:1fe135a2e60922b517886a392c2b0c2250b773abda8b1a1fcd9af86df2be6dae",
29
+ "src/publication-terminal-receipt.mjs": "sha256:b4ecc620f7e4325ec9d42652b8f1620f609fad65b1400cd2810a0a2f7e7b6794",
30
+ "src/publication-terminal.mjs": "sha256:643663218b31526a47c88f6ea2475428d94e2450b49225c253f39d54de30fed2",
29
31
  "src/snapshot-validator.mjs": "sha256:1279aa19e0932776bc1998935f70ca6fae65391f96b60e2d65bcab3b2cb46036",
30
32
  "src/source-admission.mjs": "sha256:25e0e3cc163895c3f6b5057162538c4c313bd37e67956c63b2624df52517b437",
31
- "src/source-non-admission.mjs": "sha256:a2308518c8337b662946c5778fe30a5a35a6e7f0b8ed6130eeca93d8ea959ded",
33
+ "src/source-non-admission.mjs": "sha256:fd513148f9cd8beb1faddf9ad30189913d387a3cd6bc67f3308e7b0cbee46396",
32
34
  "src/source-store.mjs": "sha256:b58a833518086a3d64fe163c323c90b6d6f4cd0793982891146c0a5f9d7241b9",
33
35
  "src/stage-journal.mjs": "sha256:96321418e4f8b48aba3dd0a4269a8f345d6763acb81f3b1e330ba3baf06a213e",
34
36
  "types/index.d.ts": "sha256:92ef7b5600f05d52dcb646fc3e4d2b4944ead52ff1c9ee23bb9f4659bebf05a6",
35
37
  "types/journal.d.ts": "sha256:79beb61af50b00400588a87426a7f5b38bf38d4b8224284dc6e0631378aa244e",
36
38
  "types/pipeline.d.ts": "sha256:c9135e25fb852377c32a16ce781405393c5857886e3f77754b715747202c70a9",
37
- "types/producer.d.ts": "sha256:980aad137365f6c131b6bfabfab411a73c4a2b2fb35a167152ef8ed446ec1ade",
39
+ "types/producer.d.ts": "sha256:f64ae58d8e5984caea148a97e2cdf7d52a77bc472fb17ec75b5af1350f763fcc",
38
40
  "types/protocol.d.ts": "sha256:02c06986c81bfc4214169f62da8e0566704b71a5add58434dfca92c673fad4ce",
39
41
  "types/source-store.d.ts": "sha256:ce2086dd682e8f57bc5e1581b411ec9fe1cf1cfba8dccafe38c22d102c2b9904",
40
42
  "types/types.d.ts": "sha256:cdc6664854bffc371a3da45f29bf1b31f14fd61b66e8f8bae8309dd836f6de6e"
@@ -0,0 +1,119 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createHmac, timingSafeEqual } from 'node:crypto';
3
+ import { completeIdentity, keys, timestamp, uuid } from './protocol-internals.mjs';
4
+ import { validateOriginalDelivery } from './producer-support.mjs';
5
+
6
+ const domain = 'casecore.release-agent/publication-terminal/v1\0';
7
+ const sorted = (value) => {
8
+ if (Array.isArray(value)) return value.map(sorted);
9
+ if (value && typeof value === 'object')
10
+ return Object.fromEntries(
11
+ Object.keys(value)
12
+ .sort()
13
+ .map((key) => [key, sorted(value[key])])
14
+ );
15
+ assert.ok(value === null || ['string', 'boolean', 'number'].includes(typeof value));
16
+ assert.ok(typeof value !== 'number' || Number.isFinite(value));
17
+ return value;
18
+ };
19
+ const body = (receipt) =>
20
+ JSON.stringify(
21
+ sorted({
22
+ schema: receipt.schema,
23
+ kind: receipt.kind,
24
+ issuedAt: receipt.issuedAt,
25
+ proof: receipt.proof,
26
+ })
27
+ );
28
+ function secret(token) {
29
+ assert.ok(
30
+ typeof token === 'string' && token.length >= 43 && token.length <= 4096 && !/\s/.test(token),
31
+ 'PUBLICATION_TERMINAL_TOKEN_REQUIRED'
32
+ );
33
+ return token;
34
+ }
35
+ export function validatePublicationTerminalReceipt(receipt) {
36
+ keys(receipt, ['schema', 'kind', 'issuedAt', 'proof', 'mac']);
37
+ assert.equal(receipt.schema, 1);
38
+ assert.equal(receipt.kind, 'publication_terminal');
39
+ timestamp(receipt.issuedAt);
40
+ assert.match(receipt.mac, /^[a-f0-9]{64}$/);
41
+ const proof = receipt.proof;
42
+ keys(proof, [
43
+ 'schema',
44
+ 'bindings',
45
+ 'status',
46
+ 'completedAt',
47
+ 'artifactIdentity',
48
+ 'readback',
49
+ 'failure',
50
+ ]);
51
+ assert.equal(proof.schema, 1);
52
+ assert.ok(['deployed', 'failed'].includes(proof.status));
53
+ timestamp(proof.completedAt);
54
+ const bindings = proof.bindings;
55
+ keys(bindings, [
56
+ 'action',
57
+ 'commandId',
58
+ 'runId',
59
+ 'installationKey',
60
+ 'repository',
61
+ 'ref',
62
+ 'provider',
63
+ 'candidateId',
64
+ 'delivery',
65
+ ]);
66
+ assert.ok(['publish', 'deploy'].includes(bindings.action));
67
+ assert.match(bindings.commandId, uuid);
68
+ assert.match(bindings.runId, uuid);
69
+ assert.match(bindings.installationKey, /^[\w.-]{1,200}$/);
70
+ assert.match(bindings.repository, /^[\w.-]+\/[\w.-]+$/);
71
+ assert.ok(
72
+ typeof bindings.ref === 'string' && bindings.ref.length > 0 && bindings.ref.length <= 200
73
+ );
74
+ assert.ok(['github', 'local'].includes(bindings.provider));
75
+ assert.ok(
76
+ typeof bindings.candidateId === 'string' &&
77
+ bindings.candidateId.length > 0 &&
78
+ bindings.candidateId.length <= 200
79
+ );
80
+ validateOriginalDelivery(bindings.delivery);
81
+ assert.ok(Date.parse(proof.completedAt) >= Date.parse(bindings.delivery.requestedAt));
82
+ if (proof.status === 'deployed') {
83
+ assert.ok(Date.parse(proof.completedAt) <= Date.parse(bindings.delivery.deadlineAt));
84
+ assert.ok(completeIdentity(proof.artifactIdentity));
85
+ } else assert.ok(proof.artifactIdentity === null || completeIdentity(proof.artifactIdentity));
86
+ assert.ok(proof.readback && typeof proof.readback === 'object');
87
+ assert.ok(proof.failure === null || (proof.failure && typeof proof.failure === 'object'));
88
+ assert.ok(Buffer.byteLength(body(receipt)) <= 262144, 'PUBLICATION_TERMINAL_RECEIPT_TOO_LARGE');
89
+ return receipt;
90
+ }
91
+ // Internal data constructor. Public sealing additionally requires the genuine
92
+ // active engine capability and durably writes the existing command journal.
93
+ export function createPublicationTerminalReceipt(proof, token) {
94
+ const receipt = {
95
+ schema: 1,
96
+ kind: 'publication_terminal',
97
+ issuedAt: new Date().toISOString(),
98
+ proof: structuredClone(proof),
99
+ mac: '',
100
+ };
101
+ receipt.mac = createHmac('sha256', secret(token))
102
+ .update(domain)
103
+ .update(body(receipt))
104
+ .digest('hex');
105
+ return validatePublicationTerminalReceipt(receipt);
106
+ }
107
+ /** Authenticated data only: this never creates or revives a WeakMap capability. */
108
+ export function verifyPublicationTerminalReceipt(receipt, token) {
109
+ validatePublicationTerminalReceipt(receipt);
110
+ const expected = createHmac('sha256', secret(token))
111
+ .update(domain)
112
+ .update(body(receipt))
113
+ .digest();
114
+ assert.ok(
115
+ timingSafeEqual(expected, Buffer.from(receipt.mac, 'hex')),
116
+ 'PUBLICATION_TERMINAL_RECEIPT_AUTHENTICATION_REQUIRED'
117
+ );
118
+ return structuredClone(receipt);
119
+ }