@futdevpro/fsm-dynamo 1.20.6 → 1.20.8
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 -28
- package/__documentations/2026-08-23-atomic-rollback-manifest-contract.md +53 -53
- package/__documentations/2026-08-23-forms-v2-color-picker-settings.md +23 -0
- package/build/_modules/ngx-models/_enums/field-type.enum.d.ts +1 -0
- package/build/_modules/ngx-models/_enums/field-type.enum.d.ts.map +1 -1
- package/build/_modules/ngx-models/_enums/field-type.enum.js +1 -0
- package/build/_modules/ngx-models/_enums/field-type.enum.js.map +1 -1
- package/build/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.d.ts +14 -0
- package/build/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.d.ts.map +1 -0
- package/build/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.js +38 -0
- package/build/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.js.map +1 -0
- package/build/_modules/ngx-models/_models/control-models/forms-new/field-new-types.d.ts +2 -1
- package/build/_modules/ngx-models/_models/control-models/forms-new/field-new-types.d.ts.map +1 -1
- package/build/_modules/ngx-models/_models/control-models/forms-new/index.d.ts +1 -0
- package/build/_modules/ngx-models/_models/control-models/forms-new/index.d.ts.map +1 -1
- package/build/_modules/ngx-models/_models/control-models/forms-new/index.js +1 -0
- package/build/_modules/ngx-models/_models/control-models/forms-new/index.js.map +1 -1
- package/package.json +1 -1
- package/src/_modules/deployment/_collections/atomic-rollback-manifest.util.spec.ts +221 -221
- package/src/_modules/deployment/_collections/atomic-rollback-manifest.util.ts +455 -455
- package/src/_modules/deployment/_models/atomic-rollback-manifest.interface.ts +59 -59
- package/src/_modules/deployment/index.ts +2 -2
- package/src/_modules/ngx-models/_enums/field-type.enum.ts +5 -4
- package/src/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.ts +44 -0
- package/src/_modules/ngx-models/_models/control-models/forms-new/field-new-types.ts +5 -3
- package/src/_modules/ngx-models/_models/control-models/forms-new/forms-new.spec.ts +36 -5
- package/src/_modules/ngx-models/_models/control-models/forms-new/index.ts +3 -2
|
@@ -1,455 +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
|
-
}
|
|
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
|
+
}
|