@futdevpro/fsm-dynamo 1.20.6 → 1.20.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +28 -28
  2. package/__documentations/2026-08-23-atomic-rollback-manifest-contract.md +53 -53
  3. package/__documentations/2026-08-23-forms-v2-color-picker-settings.md +20 -0
  4. package/build/_modules/ngx-models/_enums/field-type.enum.d.ts +1 -0
  5. package/build/_modules/ngx-models/_enums/field-type.enum.d.ts.map +1 -1
  6. package/build/_modules/ngx-models/_enums/field-type.enum.js +1 -0
  7. package/build/_modules/ngx-models/_enums/field-type.enum.js.map +1 -1
  8. package/build/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.d.ts +14 -0
  9. package/build/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.d.ts.map +1 -0
  10. package/build/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.js +23 -0
  11. package/build/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.js.map +1 -0
  12. package/build/_modules/ngx-models/_models/control-models/forms-new/field-new-types.d.ts +2 -1
  13. package/build/_modules/ngx-models/_models/control-models/forms-new/field-new-types.d.ts.map +1 -1
  14. package/build/_modules/ngx-models/_models/control-models/forms-new/index.d.ts +1 -0
  15. package/build/_modules/ngx-models/_models/control-models/forms-new/index.d.ts.map +1 -1
  16. package/build/_modules/ngx-models/_models/control-models/forms-new/index.js +1 -0
  17. package/build/_modules/ngx-models/_models/control-models/forms-new/index.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/_modules/deployment/_collections/atomic-rollback-manifest.util.spec.ts +221 -221
  20. package/src/_modules/deployment/_collections/atomic-rollback-manifest.util.ts +455 -455
  21. package/src/_modules/deployment/_models/atomic-rollback-manifest.interface.ts +59 -59
  22. package/src/_modules/deployment/index.ts +2 -2
  23. package/src/_modules/ngx-models/_enums/field-type.enum.ts +5 -4
  24. package/src/_modules/ngx-models/_models/control-models/forms-new/color-picker-field-new-settings.control-model.ts +21 -0
  25. package/src/_modules/ngx-models/_models/control-models/forms-new/field-new-types.ts +5 -3
  26. package/src/_modules/ngx-models/_models/control-models/forms-new/forms-new.spec.ts +16 -3
  27. 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
+ }