@futdevpro/fsm-dynamo 1.20.2 → 1.20.5
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 +28 -8
- package/__documentations/2026-08-22-angular-20-phase1-release.md +39 -39
- package/__documentations/2026-08-23-atomic-rollback-manifest-contract.md +52 -0
- package/build/_modules/deployment/_collections/atomic-rollback-manifest.util.d.ts +9 -0
- package/build/_modules/deployment/_collections/atomic-rollback-manifest.util.d.ts.map +1 -0
- package/build/_modules/deployment/_collections/atomic-rollback-manifest.util.js +354 -0
- package/build/_modules/deployment/_collections/atomic-rollback-manifest.util.js.map +1 -0
- package/build/_modules/deployment/_models/atomic-rollback-manifest.interface.d.ts +54 -0
- package/build/_modules/deployment/_models/atomic-rollback-manifest.interface.d.ts.map +1 -0
- package/build/_modules/deployment/_models/atomic-rollback-manifest.interface.js +3 -0
- package/build/_modules/deployment/_models/atomic-rollback-manifest.interface.js.map +1 -0
- package/build/_modules/deployment/index.d.ts +3 -0
- package/build/_modules/deployment/index.d.ts.map +1 -0
- package/build/_modules/deployment/index.js +6 -0
- package/build/_modules/deployment/index.js.map +1 -0
- package/package.json +10 -1
- package/src/_modules/crypto/_services/artifact-signature.control-service.spec.ts +2 -2
- package/src/_modules/crypto/_services/artifact-signature.control-service.ts +10 -10
- package/src/_modules/deployment/_collections/atomic-rollback-manifest.util.spec.ts +221 -0
- package/src/_modules/deployment/_collections/atomic-rollback-manifest.util.ts +455 -0
- package/src/_modules/deployment/_models/atomic-rollback-manifest.interface.ts +59 -0
- package/src/_modules/deployment/index.ts +2 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import * as CryptoJS from 'crypto-js';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DyFM_AtomicRollbackAuthority_Interface,
|
|
5
|
+
DyFM_AtomicRollbackConfiguration_Interface,
|
|
6
|
+
DyFM_AtomicRollbackEnvironment_Type,
|
|
7
|
+
DyFM_AtomicRollbackImage_Interface,
|
|
8
|
+
DyFM_AtomicRollbackLocalValidation_Interface,
|
|
9
|
+
DyFM_AtomicRollbackManifest_Interface,
|
|
10
|
+
DyFM_AtomicRollbackPostCheck_Interface
|
|
11
|
+
} from '../_models/atomic-rollback-manifest.interface';
|
|
12
|
+
|
|
13
|
+
const dyfm_exactManifestKeys: string = [
|
|
14
|
+
'authority',
|
|
15
|
+
'configuration',
|
|
16
|
+
'contractVersion',
|
|
17
|
+
'environment',
|
|
18
|
+
'images',
|
|
19
|
+
'operationId',
|
|
20
|
+
'postChecks',
|
|
21
|
+
'sourceRevision',
|
|
22
|
+
'targetChecksum',
|
|
23
|
+
].join(',');
|
|
24
|
+
const dyfm_exactAuthorityKeys: string = 'evidenceId,principalId';
|
|
25
|
+
const dyfm_exactConfigurationKeys: string = 'compatibilityVersion,policyChecksum,revision,sha256';
|
|
26
|
+
const dyfm_exactImageKeys: string = 'digest,immutableReference,mutableReference,service,version';
|
|
27
|
+
const dyfm_exactPostCheckKeys: string = 'checkId,endpoint,expectedProbeContract,service';
|
|
28
|
+
const dyfm_revisionPattern: RegExp = /^[0-9a-f]{40}$/u;
|
|
29
|
+
const dyfm_sha256Pattern: RegExp = /^[0-9a-f]{64}$/u;
|
|
30
|
+
const dyfm_digestPattern: RegExp = /^sha256:[0-9a-f]{64}$/u;
|
|
31
|
+
const dyfm_operationPattern: RegExp = /^rollback_[0-9a-f]{32}$/u;
|
|
32
|
+
const dyfm_safeIdPattern: RegExp = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
|
|
33
|
+
const dyfm_versionPattern: RegExp = /^[0-9A-Za-z][0-9A-Za-z._-]{0,63}$/u;
|
|
34
|
+
const dyfm_compatibilityPattern: RegExp = /^[a-z0-9][a-z0-9-]{1,63}\/[1-9][0-9]{0,8}$/u;
|
|
35
|
+
const dyfm_contractPattern: RegExp = /^[a-z0-9][a-z0-9-]{1,63}\/[1-9][0-9]{0,8}$/u;
|
|
36
|
+
const dyfm_imageReferencePattern: RegExp =
|
|
37
|
+
/^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[0-9]+)?\/)+[a-z0-9]+(?:[._-][a-z0-9]+)*:[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
38
|
+
|
|
39
|
+
/** Determines whether an unknown value is a non-array object. */
|
|
40
|
+
function dyfm_isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Compares an object's sorted keys with one exact key signature. */
|
|
45
|
+
function dyfm_hasExactKeys(value: Record<string, unknown>, expected: string): boolean {
|
|
46
|
+
return Object.keys(value).sort().join(',') === expected;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Narrows a value to a deployment environment supported by the contract. */
|
|
50
|
+
function dyfm_isEnvironment(value: unknown): value is DyFM_AtomicRollbackEnvironment_Type {
|
|
51
|
+
return value === 'test' || value === 'production';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Allows bounded HTTPS probes and service-local HTTP probes without credentials or query data. */
|
|
55
|
+
function dyfm_isSafePostCheckEndpoint(value: string, service: string): boolean {
|
|
56
|
+
if (value.length > 2048) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const endpoint: URL = new URL(value);
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
endpoint.username
|
|
64
|
+
|| endpoint.password
|
|
65
|
+
|| endpoint.search
|
|
66
|
+
|| endpoint.hash
|
|
67
|
+
|| !/^\/[A-Za-z0-9/_-]{0,127}$/u.test(endpoint.pathname)
|
|
68
|
+
) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (endpoint.protocol === 'https:') {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return endpoint.protocol === 'http:'
|
|
77
|
+
&& [ service, '127.0.0.1', 'localhost' ].includes(endpoint.hostname);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Parses bounded deployment authority evidence without returning rejected content. */
|
|
84
|
+
function dyfm_parseAuthority(
|
|
85
|
+
value: unknown,
|
|
86
|
+
gapCodes: string[],
|
|
87
|
+
): DyFM_AtomicRollbackAuthority_Interface | undefined {
|
|
88
|
+
if (!dyfm_isRecord(value) || !dyfm_hasExactKeys(value, dyfm_exactAuthorityKeys)) {
|
|
89
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|AUTHORITY_SHAPE_INVALID');
|
|
90
|
+
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof value.principalId !== 'string' || !dyfm_safeIdPattern.test(value.principalId)) {
|
|
95
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|AUTHORITY_PRINCIPAL_INVALID');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof value.evidenceId !== 'string' || !dyfm_sha256Pattern.test(value.evidenceId)) {
|
|
99
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|AUTHORITY_EVIDENCE_INVALID');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
typeof value.principalId !== 'string'
|
|
104
|
+
|| typeof value.evidenceId !== 'string'
|
|
105
|
+
|| gapCodes.some((code: string): boolean => code.startsWith('FDP|ATOMIC_ROLLBACK|AUTHORITY_'))
|
|
106
|
+
) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { principalId: value.principalId, evidenceId: value.evidenceId };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Parses an exact configuration and compatibility target. */
|
|
114
|
+
function dyfm_parseConfiguration(
|
|
115
|
+
value: unknown,
|
|
116
|
+
gapCodes: string[],
|
|
117
|
+
): DyFM_AtomicRollbackConfiguration_Interface | undefined {
|
|
118
|
+
if (!dyfm_isRecord(value) || !dyfm_hasExactKeys(value, dyfm_exactConfigurationKeys)) {
|
|
119
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|CONFIGURATION_SHAPE_INVALID');
|
|
120
|
+
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof value.revision !== 'string' || !dyfm_revisionPattern.test(value.revision)) {
|
|
125
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|CONFIGURATION_REVISION_INVALID');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof value.sha256 !== 'string' || !dyfm_sha256Pattern.test(value.sha256)) {
|
|
129
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|CONFIGURATION_CHECKSUM_INVALID');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
typeof value.compatibilityVersion !== 'string'
|
|
134
|
+
|| !dyfm_compatibilityPattern.test(value.compatibilityVersion)
|
|
135
|
+
) {
|
|
136
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|CONFIGURATION_COMPATIBILITY_INVALID');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (typeof value.policyChecksum !== 'string' || !dyfm_sha256Pattern.test(value.policyChecksum)) {
|
|
140
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|CONFIGURATION_POLICY_CHECKSUM_INVALID');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
typeof value.revision !== 'string'
|
|
145
|
+
|| typeof value.sha256 !== 'string'
|
|
146
|
+
|| typeof value.compatibilityVersion !== 'string'
|
|
147
|
+
|| typeof value.policyChecksum !== 'string'
|
|
148
|
+
|| gapCodes.some((code: string): boolean => code.startsWith('FDP|ATOMIC_ROLLBACK|CONFIGURATION_'))
|
|
149
|
+
) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
revision: value.revision,
|
|
155
|
+
sha256: value.sha256,
|
|
156
|
+
compatibilityVersion: value.compatibilityVersion,
|
|
157
|
+
policyChecksum: value.policyChecksum,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Parses the complete immutable image promotion set. */
|
|
162
|
+
function dyfm_parseImages(value: unknown, gapCodes: string[]): DyFM_AtomicRollbackImage_Interface[] {
|
|
163
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 16) {
|
|
164
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|IMAGE_SET_CARDINALITY_INVALID');
|
|
165
|
+
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const images: DyFM_AtomicRollbackImage_Interface[] = [];
|
|
169
|
+
|
|
170
|
+
for (const candidate of value) {
|
|
171
|
+
if (!dyfm_isRecord(candidate) || !dyfm_hasExactKeys(candidate, dyfm_exactImageKeys)) {
|
|
172
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|IMAGE_SHAPE_INVALID');
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const service: string | undefined = typeof candidate.service === 'string' ? candidate.service : undefined;
|
|
176
|
+
const version: string | undefined = typeof candidate.version === 'string' ? candidate.version : undefined;
|
|
177
|
+
const immutableReference: string | undefined = typeof candidate.immutableReference === 'string'
|
|
178
|
+
? candidate.immutableReference
|
|
179
|
+
: undefined;
|
|
180
|
+
const mutableReference: string | undefined = typeof candidate.mutableReference === 'string'
|
|
181
|
+
? candidate.mutableReference
|
|
182
|
+
: undefined;
|
|
183
|
+
const digest: string | undefined = typeof candidate.digest === 'string' ? candidate.digest : undefined;
|
|
184
|
+
|
|
185
|
+
if (!service || !dyfm_safeIdPattern.test(service)) {
|
|
186
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|IMAGE_SERVICE_INVALID');
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!version || !dyfm_versionPattern.test(version)) {
|
|
191
|
+
gapCodes.push(`FDP|ATOMIC_ROLLBACK|IMAGE_VERSION_INVALID|${service}`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (
|
|
196
|
+
!immutableReference
|
|
197
|
+
|| immutableReference.length > 512
|
|
198
|
+
|| !dyfm_imageReferencePattern.test(immutableReference)
|
|
199
|
+
|| !immutableReference.endsWith(`/${service}:release-${version}`)
|
|
200
|
+
) {
|
|
201
|
+
gapCodes.push(`FDP|ATOMIC_ROLLBACK|IMAGE_IMMUTABLE_REFERENCE_INVALID|${service}`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
!mutableReference
|
|
207
|
+
|| mutableReference.length > 512
|
|
208
|
+
|| !dyfm_imageReferencePattern.test(mutableReference)
|
|
209
|
+
|| !mutableReference.endsWith(`/${service}:release`)
|
|
210
|
+
) {
|
|
211
|
+
gapCodes.push(`FDP|ATOMIC_ROLLBACK|IMAGE_MUTABLE_REFERENCE_INVALID|${service}`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
immutableReference.slice(0, immutableReference.lastIndexOf(':'))
|
|
217
|
+
!== mutableReference.slice(0, mutableReference.lastIndexOf(':'))
|
|
218
|
+
) {
|
|
219
|
+
gapCodes.push(`FDP|ATOMIC_ROLLBACK|IMAGE_REPOSITORY_MISMATCH|${service}`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!digest || !dyfm_digestPattern.test(digest)) {
|
|
224
|
+
gapCodes.push(`FDP|ATOMIC_ROLLBACK|IMAGE_DIGEST_INVALID|${service}`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
images.push({
|
|
228
|
+
service: service,
|
|
229
|
+
version: version,
|
|
230
|
+
immutableReference: immutableReference,
|
|
231
|
+
mutableReference: mutableReference,
|
|
232
|
+
digest: digest,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const services: string[] = images.map((image: DyFM_AtomicRollbackImage_Interface): string => image.service);
|
|
236
|
+
|
|
237
|
+
if (new Set(services).size !== services.length) {
|
|
238
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|IMAGE_SERVICE_DUPLICATED');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return images;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Parses bounded post-deployment checks targeting manifest services only. */
|
|
245
|
+
function dyfm_parsePostChecks(value: unknown, gapCodes: string[]): DyFM_AtomicRollbackPostCheck_Interface[] {
|
|
246
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 32) {
|
|
247
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|POST_CHECK_SET_CARDINALITY_INVALID');
|
|
248
|
+
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
const postChecks: DyFM_AtomicRollbackPostCheck_Interface[] = [];
|
|
252
|
+
|
|
253
|
+
for (const candidate of value) {
|
|
254
|
+
if (!dyfm_isRecord(candidate) || !dyfm_hasExactKeys(candidate, dyfm_exactPostCheckKeys)) {
|
|
255
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|POST_CHECK_SHAPE_INVALID');
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const checkId: string | undefined = typeof candidate.checkId === 'string' ? candidate.checkId : undefined;
|
|
259
|
+
const service: string | undefined = typeof candidate.service === 'string' ? candidate.service : undefined;
|
|
260
|
+
const endpoint: string | undefined = typeof candidate.endpoint === 'string' ? candidate.endpoint : undefined;
|
|
261
|
+
const expectedProbeContract: string | undefined = typeof candidate.expectedProbeContract === 'string'
|
|
262
|
+
? candidate.expectedProbeContract
|
|
263
|
+
: undefined;
|
|
264
|
+
|
|
265
|
+
if (!checkId || !dyfm_safeIdPattern.test(checkId)) {
|
|
266
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|POST_CHECK_ID_INVALID');
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!service || !dyfm_safeIdPattern.test(service)) {
|
|
271
|
+
gapCodes.push(`FDP|ATOMIC_ROLLBACK|POST_CHECK_SERVICE_INVALID|${checkId}`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!endpoint || !dyfm_isSafePostCheckEndpoint(endpoint, service)) {
|
|
276
|
+
gapCodes.push(`FDP|ATOMIC_ROLLBACK|POST_CHECK_ENDPOINT_INVALID|${checkId}`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!expectedProbeContract || !dyfm_contractPattern.test(expectedProbeContract)) {
|
|
281
|
+
gapCodes.push(`FDP|ATOMIC_ROLLBACK|POST_CHECK_CONTRACT_INVALID|${checkId}`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
postChecks.push({
|
|
285
|
+
checkId: checkId,
|
|
286
|
+
service: service,
|
|
287
|
+
endpoint: endpoint,
|
|
288
|
+
expectedProbeContract: expectedProbeContract,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (
|
|
293
|
+
new Set(postChecks.map((check: DyFM_AtomicRollbackPostCheck_Interface): string => check.checkId)).size
|
|
294
|
+
!== postChecks.length
|
|
295
|
+
) {
|
|
296
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|POST_CHECK_ID_DUPLICATED');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return postChecks;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Creates the exact, insertion-order-independent JSON value hashed by every producer and consumer. */
|
|
303
|
+
function dyfm_canonicalTarget(manifest: DyFM_AtomicRollbackManifest_Interface): string {
|
|
304
|
+
return JSON.stringify({
|
|
305
|
+
contractVersion: manifest.contractVersion,
|
|
306
|
+
operationId: manifest.operationId,
|
|
307
|
+
environment: manifest.environment,
|
|
308
|
+
authority: {
|
|
309
|
+
principalId: manifest.authority.principalId,
|
|
310
|
+
evidenceId: manifest.authority.evidenceId,
|
|
311
|
+
},
|
|
312
|
+
sourceRevision: manifest.sourceRevision,
|
|
313
|
+
configuration: {
|
|
314
|
+
revision: manifest.configuration.revision,
|
|
315
|
+
sha256: manifest.configuration.sha256,
|
|
316
|
+
compatibilityVersion: manifest.configuration.compatibilityVersion,
|
|
317
|
+
policyChecksum: manifest.configuration.policyChecksum,
|
|
318
|
+
},
|
|
319
|
+
images: [ ...manifest.images ].sort(
|
|
320
|
+
(left: DyFM_AtomicRollbackImage_Interface, right: DyFM_AtomicRollbackImage_Interface): number => left.service
|
|
321
|
+
.localeCompare(right.service),
|
|
322
|
+
).map((image: DyFM_AtomicRollbackImage_Interface): DyFM_AtomicRollbackImage_Interface => ({
|
|
323
|
+
service: image.service,
|
|
324
|
+
version: image.version,
|
|
325
|
+
immutableReference: image.immutableReference,
|
|
326
|
+
mutableReference: image.mutableReference,
|
|
327
|
+
digest: image.digest,
|
|
328
|
+
})),
|
|
329
|
+
postChecks: [ ...manifest.postChecks ].sort(
|
|
330
|
+
(left: DyFM_AtomicRollbackPostCheck_Interface, right: DyFM_AtomicRollbackPostCheck_Interface): number => left
|
|
331
|
+
.checkId.localeCompare(right.checkId),
|
|
332
|
+
).map((check: DyFM_AtomicRollbackPostCheck_Interface): DyFM_AtomicRollbackPostCheck_Interface => ({
|
|
333
|
+
checkId: check.checkId,
|
|
334
|
+
service: check.service,
|
|
335
|
+
endpoint: check.endpoint,
|
|
336
|
+
expectedProbeContract: check.expectedProbeContract,
|
|
337
|
+
})),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Canonical manifest checksum and strict content-free local validation. */
|
|
342
|
+
export class DyFM_AtomicRollbackManifest_Util {
|
|
343
|
+
/** Calculates the canonical SHA-256 target checksum for a typed manifest. */
|
|
344
|
+
public static computeTargetChecksum(manifest: DyFM_AtomicRollbackManifest_Interface): string {
|
|
345
|
+
return CryptoJS.SHA256(dyfm_canonicalTarget(manifest)).toString();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Validates an unknown manifest without reflecting rejected content. */
|
|
349
|
+
public static validate(value: unknown): DyFM_AtomicRollbackLocalValidation_Interface {
|
|
350
|
+
if (!dyfm_isRecord(value) || !dyfm_hasExactKeys(value, dyfm_exactManifestKeys)) {
|
|
351
|
+
return {
|
|
352
|
+
contractVersion: 'fdp-atomic-rollback-local-validation/1',
|
|
353
|
+
valid: false,
|
|
354
|
+
environment: 'invalid',
|
|
355
|
+
operationId: 'invalid',
|
|
356
|
+
targetChecksum: 'invalid',
|
|
357
|
+
serviceCount: 0,
|
|
358
|
+
postCheckCount: 0,
|
|
359
|
+
gapCodes: [ 'FDP|ATOMIC_ROLLBACK|MANIFEST_SHAPE_INVALID' ],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const gapCodes: string[] = [];
|
|
364
|
+
|
|
365
|
+
if (value.contractVersion !== 'fdp-atomic-rollback-manifest/1') {
|
|
366
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|CONTRACT_VERSION_INVALID');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (typeof value.operationId !== 'string' || !dyfm_operationPattern.test(value.operationId)) {
|
|
370
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|OPERATION_ID_INVALID');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!dyfm_isEnvironment(value.environment)) {
|
|
374
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|ENVIRONMENT_INVALID');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (typeof value.sourceRevision !== 'string' || !dyfm_revisionPattern.test(value.sourceRevision)) {
|
|
378
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|SOURCE_REVISION_INVALID');
|
|
379
|
+
}
|
|
380
|
+
const authority: DyFM_AtomicRollbackAuthority_Interface | undefined = dyfm_parseAuthority(
|
|
381
|
+
value.authority,
|
|
382
|
+
gapCodes,
|
|
383
|
+
);
|
|
384
|
+
const configuration: DyFM_AtomicRollbackConfiguration_Interface | undefined = dyfm_parseConfiguration(
|
|
385
|
+
value.configuration,
|
|
386
|
+
gapCodes,
|
|
387
|
+
);
|
|
388
|
+
const images: DyFM_AtomicRollbackImage_Interface[] = dyfm_parseImages(value.images, gapCodes);
|
|
389
|
+
const postChecks: DyFM_AtomicRollbackPostCheck_Interface[] = dyfm_parsePostChecks(value.postChecks, gapCodes);
|
|
390
|
+
const imageServices: Set<string> = new Set(images.map(
|
|
391
|
+
(image: DyFM_AtomicRollbackImage_Interface): string => image.service,
|
|
392
|
+
));
|
|
393
|
+
|
|
394
|
+
if (
|
|
395
|
+
postChecks.some(
|
|
396
|
+
(check: DyFM_AtomicRollbackPostCheck_Interface): boolean => !imageServices.has(check.service),
|
|
397
|
+
)
|
|
398
|
+
) {
|
|
399
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|POST_CHECK_SERVICE_NOT_TARGETED');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (typeof value.targetChecksum !== 'string' || !dyfm_sha256Pattern.test(value.targetChecksum)) {
|
|
403
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|TARGET_CHECKSUM_INVALID');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let manifest: DyFM_AtomicRollbackManifest_Interface | undefined;
|
|
407
|
+
|
|
408
|
+
if (
|
|
409
|
+
!gapCodes.length
|
|
410
|
+
&& typeof value.operationId === 'string'
|
|
411
|
+
&& dyfm_isEnvironment(value.environment)
|
|
412
|
+
&& authority
|
|
413
|
+
&& typeof value.sourceRevision === 'string'
|
|
414
|
+
&& configuration
|
|
415
|
+
&& images.length > 0
|
|
416
|
+
&& postChecks.length > 0
|
|
417
|
+
&& typeof value.targetChecksum === 'string'
|
|
418
|
+
) {
|
|
419
|
+
manifest = {
|
|
420
|
+
contractVersion: 'fdp-atomic-rollback-manifest/1',
|
|
421
|
+
operationId: value.operationId,
|
|
422
|
+
environment: value.environment,
|
|
423
|
+
authority: authority,
|
|
424
|
+
sourceRevision: value.sourceRevision,
|
|
425
|
+
configuration: configuration,
|
|
426
|
+
images: images,
|
|
427
|
+
postChecks: postChecks,
|
|
428
|
+
targetChecksum: value.targetChecksum,
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
if (this.computeTargetChecksum(manifest) !== value.targetChecksum) {
|
|
432
|
+
gapCodes.push('FDP|ATOMIC_ROLLBACK|TARGET_CHECKSUM_MISMATCH');
|
|
433
|
+
manifest = undefined;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const normalizedGapCodes: string[] = [ ...new Set(gapCodes) ].sort();
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
contractVersion: 'fdp-atomic-rollback-local-validation/1',
|
|
441
|
+
valid: !normalizedGapCodes.length,
|
|
442
|
+
environment: dyfm_isEnvironment(value.environment) ? value.environment : 'invalid',
|
|
443
|
+
operationId: !normalizedGapCodes.length && typeof value.operationId === 'string'
|
|
444
|
+
? value.operationId
|
|
445
|
+
: 'invalid',
|
|
446
|
+
targetChecksum: !normalizedGapCodes.length && typeof value.targetChecksum === 'string'
|
|
447
|
+
? value.targetChecksum
|
|
448
|
+
: 'invalid',
|
|
449
|
+
serviceCount: images.length,
|
|
450
|
+
postCheckCount: postChecks.length,
|
|
451
|
+
gapCodes: normalizedGapCodes,
|
|
452
|
+
...(manifest ? { manifest: manifest } : {}),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Deployment environments accepted by the shared atomic rollback contract. */
|
|
2
|
+
export type DyFM_AtomicRollbackEnvironment_Type = 'test' | 'production';
|
|
3
|
+
|
|
4
|
+
/** Content-free authority reference supplied by the deployment control plane. */
|
|
5
|
+
export interface DyFM_AtomicRollbackAuthority_Interface {
|
|
6
|
+
principalId: string;
|
|
7
|
+
evidenceId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Exact version-controlled configuration and database compatibility target. */
|
|
11
|
+
export interface DyFM_AtomicRollbackConfiguration_Interface {
|
|
12
|
+
revision: string;
|
|
13
|
+
sha256: string;
|
|
14
|
+
compatibilityVersion: string;
|
|
15
|
+
policyChecksum: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One immutable image target and its mutable release promotion destination. */
|
|
19
|
+
export interface DyFM_AtomicRollbackImage_Interface {
|
|
20
|
+
service: string;
|
|
21
|
+
version: string;
|
|
22
|
+
immutableReference: string;
|
|
23
|
+
mutableReference: string;
|
|
24
|
+
digest: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One bounded post-deployment probe tied to a service in the image set. */
|
|
28
|
+
export interface DyFM_AtomicRollbackPostCheck_Interface {
|
|
29
|
+
checkId: string;
|
|
30
|
+
service: string;
|
|
31
|
+
endpoint: string;
|
|
32
|
+
expectedProbeContract: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Complete versioned target for an atomic multi-service rollout or rollback. */
|
|
36
|
+
export interface DyFM_AtomicRollbackManifest_Interface {
|
|
37
|
+
contractVersion: 'fdp-atomic-rollback-manifest/1';
|
|
38
|
+
operationId: string;
|
|
39
|
+
environment: DyFM_AtomicRollbackEnvironment_Type;
|
|
40
|
+
authority: DyFM_AtomicRollbackAuthority_Interface;
|
|
41
|
+
sourceRevision: string;
|
|
42
|
+
configuration: DyFM_AtomicRollbackConfiguration_Interface;
|
|
43
|
+
images: DyFM_AtomicRollbackImage_Interface[];
|
|
44
|
+
postChecks: DyFM_AtomicRollbackPostCheck_Interface[];
|
|
45
|
+
targetChecksum: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Bounded content-free validation evidence for an unknown manifest value. */
|
|
49
|
+
export interface DyFM_AtomicRollbackLocalValidation_Interface {
|
|
50
|
+
contractVersion: 'fdp-atomic-rollback-local-validation/1';
|
|
51
|
+
valid: boolean;
|
|
52
|
+
environment: DyFM_AtomicRollbackEnvironment_Type | 'invalid';
|
|
53
|
+
operationId: string | 'invalid';
|
|
54
|
+
targetChecksum: string | 'invalid';
|
|
55
|
+
serviceCount: number;
|
|
56
|
+
postCheckCount: number;
|
|
57
|
+
gapCodes: string[];
|
|
58
|
+
manifest?: DyFM_AtomicRollbackManifest_Interface;
|
|
59
|
+
}
|