@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.
- package/README.md +49 -1
- package/dist/exclusive-operation.mjs +3 -0
- package/dist/producer-admission.mjs +3 -0
- package/dist/producer-delivery.mjs +2 -2
- package/dist/producer-execution.mjs +33 -67
- package/dist/producer-journal.mjs +41 -0
- package/dist/producer-publication-failure.mjs +16 -2
- package/dist/producer-server.mjs +29 -51
- package/dist/producer-state.mjs +22 -35
- package/dist/producer.d.ts +72 -2
- package/dist/producer.mjs +44 -0
- package/dist/provenance.json +16 -14
- package/dist/publication-terminal-receipt.mjs +119 -0
- package/dist/publication-terminal.mjs +251 -0
- package/dist/source-non-admission.mjs +4 -2
- package/package.json +1 -1
package/dist/producer-state.mjs
CHANGED
|
@@ -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 {
|
|
6
|
+
import {
|
|
7
|
+
installOperationBarrier,
|
|
8
|
+
markOperationStateLoaded,
|
|
9
|
+
reserveExclusiveOperation,
|
|
10
|
+
} from './exclusive-operation.mjs';
|
|
7
11
|
|
|
8
12
|
export class ProducerState {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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' ||
|
package/dist/producer.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/provenance.json
CHANGED
|
@@ -1,40 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"package": "@casecore/release-agent",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"sourceCommit": "
|
|
4
|
+
"version": "0.1.6",
|
|
5
|
+
"sourceCommit": "70442860e0f89db88c78142c6f6a7e8073414af5",
|
|
6
6
|
"sourceVerified": true,
|
|
7
7
|
"sourceDirty": false,
|
|
8
8
|
"buildKind": "release",
|
|
9
|
-
"contentDigest": "sha256:
|
|
9
|
+
"contentDigest": "sha256:a41148b8adf594a5da507b1f8495b5b8c63e32b514d815c7bbce2634b0e6fb89",
|
|
10
10
|
"files": {
|
|
11
11
|
"src/configuration-hook.mjs": "sha256:1171bf982cbf7cb0092f63b633ef0b0a0947d8ed1106f88c9175c8add33d81b1",
|
|
12
|
-
"src/exclusive-operation.mjs": "sha256:
|
|
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:
|
|
18
|
-
"src/producer-delivery.mjs": "sha256:
|
|
19
|
-
"src/producer-execution.mjs": "sha256:
|
|
20
|
-
"src/producer-journal.mjs": "sha256:
|
|
21
|
-
"src/producer-publication-failure.mjs": "sha256:
|
|
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:
|
|
24
|
-
"src/producer-state.mjs": "sha256:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
}
|