@futdevpro/fsm-dynamo 1.16.40 → 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.
Files changed (23) hide show
  1. package/README.md +28 -8
  2. package/__documentations/2026-08-22-angular-20-phase1-release.md +39 -0
  3. package/__documentations/2026-08-23-atomic-rollback-manifest-contract.md +52 -0
  4. package/build/_modules/crypto/_services/artifact-signature.control-service.js.map +1 -1
  5. package/build/_modules/deployment/_collections/atomic-rollback-manifest.util.d.ts +9 -0
  6. package/build/_modules/deployment/_collections/atomic-rollback-manifest.util.d.ts.map +1 -0
  7. package/build/_modules/deployment/_collections/atomic-rollback-manifest.util.js +354 -0
  8. package/build/_modules/deployment/_collections/atomic-rollback-manifest.util.js.map +1 -0
  9. package/build/_modules/deployment/_models/atomic-rollback-manifest.interface.d.ts +54 -0
  10. package/build/_modules/deployment/_models/atomic-rollback-manifest.interface.d.ts.map +1 -0
  11. package/build/_modules/deployment/_models/atomic-rollback-manifest.interface.js +3 -0
  12. package/build/_modules/deployment/_models/atomic-rollback-manifest.interface.js.map +1 -0
  13. package/build/_modules/deployment/index.d.ts +3 -0
  14. package/build/_modules/deployment/index.d.ts.map +1 -0
  15. package/build/_modules/deployment/index.js +6 -0
  16. package/build/_modules/deployment/index.js.map +1 -0
  17. package/package.json +315 -306
  18. package/src/_modules/crypto/_services/artifact-signature.control-service.spec.ts +2 -2
  19. package/src/_modules/crypto/_services/artifact-signature.control-service.ts +10 -10
  20. package/src/_modules/deployment/_collections/atomic-rollback-manifest.util.spec.ts +221 -0
  21. package/src/_modules/deployment/_collections/atomic-rollback-manifest.util.ts +455 -0
  22. package/src/_modules/deployment/_models/atomic-rollback-manifest.interface.ts +59 -0
  23. package/src/_modules/deployment/index.ts +2 -0
@@ -13,7 +13,7 @@ interface DyFM_ArtifactSignatureSpecKeyMaterial {
13
13
  describe('| DyFM artifact signature', (): void => {
14
14
  const signature_CS: DyFM_ArtifactSignature_ControlService =
15
15
  new DyFM_ArtifactSignature_ControlService();
16
- const subjectBytes: Uint8Array = new TextEncoder().encode(
16
+ const subjectBytes: Uint8Array<ArrayBuffer> = new TextEncoder().encode(
17
17
  '{"contractVersion":"ctm-artifact-canonical/1","bundleSource":"exact"}'
18
18
  );
19
19
  let keyMaterial: DyFM_ArtifactSignatureSpecKeyMaterial;
@@ -182,7 +182,7 @@ describe('| DyFM artifact signature', (): void => {
182
182
  }
183
183
 
184
184
  /** Calculates lowercase SHA-256 through the same platform boundary as production. */
185
- async function checksum(bytes: Uint8Array): Promise<string> {
185
+ async function checksum(bytes: Uint8Array<ArrayBuffer>): Promise<string> {
186
186
  const digest: ArrayBuffer = await globalThis.crypto.subtle.digest('SHA-256', bytes);
187
187
 
188
188
  return [ ...new Uint8Array(digest) ]
@@ -34,14 +34,14 @@ export class DyFM_ArtifactSignature_ControlService {
34
34
  this.validateTimestamp(set.signedAt);
35
35
 
36
36
  const subtle: SubtleCrypto = this.getSubtleCrypto();
37
- const subjectBytes: Uint8Array = new Uint8Array(set.subjectBytes);
37
+ const subjectBytes: Uint8Array<ArrayBuffer> = new Uint8Array(set.subjectBytes);
38
38
  const actualChecksum: string = await this.checksum(subjectBytes);
39
39
 
40
40
  if (actualChecksum !== set.subjectChecksum) {
41
41
  throw this.buildError('SUBJECT_CHECKSUM_MISMATCH');
42
42
  }
43
43
 
44
- const privateKeyBytes: Uint8Array = this.decodeBase64UrlStrict(set.privateKeyPkcs8Base64Url);
44
+ const privateKeyBytes: Uint8Array<ArrayBuffer> = this.decodeBase64UrlStrict(set.privateKeyPkcs8Base64Url);
45
45
 
46
46
  if (privateKeyBytes.byteLength < 64 || privateKeyBytes.byteLength > 512) {
47
47
  throw this.buildError('PRIVATE_KEY_LENGTH_INVALID');
@@ -73,7 +73,7 @@ export class DyFM_ArtifactSignature_ControlService {
73
73
  privateKeyBytes.fill(0);
74
74
  }
75
75
 
76
- let signatureBytes: Uint8Array;
76
+ let signatureBytes: Uint8Array<ArrayBuffer>;
77
77
 
78
78
  try {
79
79
  signatureBytes = new Uint8Array(await subtle.sign(
@@ -128,7 +128,7 @@ export class DyFM_ArtifactSignature_ControlService {
128
128
  return this.result(set.evidence, false, 'DYFM|ARTIFACT_SIGNATURE|SUBJECT_INVALID');
129
129
  }
130
130
 
131
- const subjectBytes: Uint8Array = new Uint8Array(set.subjectBytes);
131
+ const subjectBytes: Uint8Array<ArrayBuffer> = new Uint8Array(set.subjectBytes);
132
132
  const actualChecksum: string = await this.checksum(subjectBytes);
133
133
 
134
134
  if (actualChecksum !== set.evidence.subjectChecksum) {
@@ -136,7 +136,7 @@ export class DyFM_ArtifactSignature_ControlService {
136
136
  }
137
137
 
138
138
  let publicKeyJwk: JsonWebKey;
139
- let signatureBytes: Uint8Array;
139
+ let signatureBytes: Uint8Array<ArrayBuffer>;
140
140
 
141
141
  try {
142
142
  publicKeyJwk = this.validatePublicJwk(set.publicKeyJwk);
@@ -193,7 +193,7 @@ export class DyFM_ArtifactSignature_ControlService {
193
193
  return this.checksum(new TextEncoder().encode(canonicalJwk));
194
194
  }
195
195
 
196
- private async checksum(bytes: Uint8Array): Promise<string> {
196
+ private async checksum(bytes: Uint8Array<ArrayBuffer>): Promise<string> {
197
197
  const digest: ArrayBuffer = await this.getSubtleCrypto().digest('SHA-256', bytes);
198
198
 
199
199
  return [ ...new Uint8Array(digest) ]
@@ -201,7 +201,7 @@ export class DyFM_ArtifactSignature_ControlService {
201
201
  .join('');
202
202
  }
203
203
 
204
- private decodeBase64UrlStrict(value: string): Uint8Array {
204
+ private decodeBase64UrlStrict(value: string): Uint8Array<ArrayBuffer> {
205
205
  if (!value || !/^[A-Za-z0-9_-]+$/u.test(value) || value.length % 4 === 1) {
206
206
  throw this.buildError('BASE64URL_INVALID');
207
207
  }
@@ -230,7 +230,7 @@ export class DyFM_ArtifactSignature_ControlService {
230
230
  }
231
231
  }
232
232
 
233
- const decoded: Uint8Array = new Uint8Array(bytes);
233
+ const decoded: Uint8Array<ArrayBuffer> = new Uint8Array(bytes);
234
234
 
235
235
  if (this.encodeBase64Url(decoded) !== value) {
236
236
  throw this.buildError('BASE64URL_NON_CANONICAL');
@@ -360,8 +360,8 @@ export class DyFM_ArtifactSignature_ControlService {
360
360
  throw this.buildError('PUBLIC_KEY_INVALID');
361
361
  }
362
362
 
363
- const xBytes: Uint8Array = this.decodeBase64UrlStrict(value.x);
364
- const yBytes: Uint8Array = this.decodeBase64UrlStrict(value.y);
363
+ const xBytes: Uint8Array<ArrayBuffer> = this.decodeBase64UrlStrict(value.x);
364
+ const yBytes: Uint8Array<ArrayBuffer> = this.decodeBase64UrlStrict(value.y);
365
365
 
366
366
  if (xBytes.byteLength !== 32 || yBytes.byteLength !== 32) {
367
367
  throw this.buildError('PUBLIC_KEY_COORDINATE_INVALID');
@@ -0,0 +1,221 @@
1
+ import { DyFM_AtomicRollbackManifest_Util } from './atomic-rollback-manifest.util';
2
+ import {
3
+ DyFM_AtomicRollbackImage_Interface,
4
+ DyFM_AtomicRollbackLocalValidation_Interface,
5
+ DyFM_AtomicRollbackManifest_Interface,
6
+ DyFM_AtomicRollbackPostCheck_Interface
7
+ } from '../_models/atomic-rollback-manifest.interface';
8
+
9
+ /** Builds one fully valid manifest for contract mutation tests. */
10
+ const dyfm_createManifest: () => DyFM_AtomicRollbackManifest_Interface =
11
+ (): DyFM_AtomicRollbackManifest_Interface => {
12
+ const manifest: DyFM_AtomicRollbackManifest_Interface = {
13
+ contractVersion: 'fdp-atomic-rollback-manifest/1',
14
+ operationId: `rollback_${'1'.repeat(32)}`,
15
+ environment: 'test',
16
+ authority: {
17
+ principalId: 'fdp-cdp',
18
+ evidenceId: '2'.repeat(64),
19
+ },
20
+ sourceRevision: '3'.repeat(40),
21
+ configuration: {
22
+ revision: '4'.repeat(40),
23
+ sha256: '5'.repeat(64),
24
+ compatibilityVersion: 'ctm-db/1',
25
+ policyChecksum: '6'.repeat(64),
26
+ },
27
+ images: [
28
+ {
29
+ service: 'company-toolmaker-server',
30
+ version: '0.0.1',
31
+ immutableReference: 'registry.example/company-toolmaker-server:release-0.0.1',
32
+ mutableReference: 'registry.example/company-toolmaker-server:release',
33
+ digest: `sha256:${'7'.repeat(64)}`,
34
+ },
35
+ {
36
+ service: 'company-toolmaker-client',
37
+ version: '0.0.1',
38
+ immutableReference: 'registry.example/company-toolmaker-client:release-0.0.1',
39
+ mutableReference: 'registry.example/company-toolmaker-client:release',
40
+ digest: `sha256:${'8'.repeat(64)}`,
41
+ },
42
+ ],
43
+ postChecks: [
44
+ {
45
+ checkId: 'server-readiness',
46
+ service: 'company-toolmaker-server',
47
+ endpoint: 'https://test-company-toolmaker.futdevpro.hu/readiness',
48
+ expectedProbeContract: 'ctm-service-probe/4',
49
+ },
50
+ {
51
+ checkId: 'client-smoke',
52
+ service: 'company-toolmaker-client',
53
+ endpoint: 'https://test-company-toolmaker.futdevpro.hu/',
54
+ expectedProbeContract: 'ctm-client-smoke/1',
55
+ },
56
+ ],
57
+ targetChecksum: '',
58
+ };
59
+
60
+ manifest.targetChecksum = DyFM_AtomicRollbackManifest_Util.computeTargetChecksum(manifest);
61
+
62
+ return manifest;
63
+ };
64
+
65
+ describe('| DyFM atomic rollback manifest', (): void => {
66
+ it('| validates an exact manifest with order-independent service and post-check sets', (): void => {
67
+ const manifest: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
68
+ const checksum: string = manifest.targetChecksum;
69
+
70
+ manifest.images.reverse();
71
+ manifest.postChecks.reverse();
72
+
73
+ expect(DyFM_AtomicRollbackManifest_Util.computeTargetChecksum(manifest)).toBe(checksum);
74
+ expect(checksum).toBe('2b416434a8d36c5b3305383eb1f2ecebae4057d30d2f46d986d04728225b58df');
75
+ expect(DyFM_AtomicRollbackManifest_Util.validate(manifest)).toEqual(jasmine.objectContaining({
76
+ valid: true,
77
+ environment: 'test',
78
+ operationId: manifest.operationId,
79
+ targetChecksum: checksum,
80
+ serviceCount: 2,
81
+ postCheckCount: 2,
82
+ gapCodes: [],
83
+ }));
84
+ });
85
+
86
+ it('| ignores nested object insertion order when calculating the canonical checksum', (): void => {
87
+ const manifest: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
88
+ const reordered: DyFM_AtomicRollbackManifest_Interface = {
89
+ ...manifest,
90
+ authority: {
91
+ evidenceId: manifest.authority.evidenceId,
92
+ principalId: manifest.authority.principalId,
93
+ },
94
+ configuration: {
95
+ policyChecksum: manifest.configuration.policyChecksum,
96
+ compatibilityVersion: manifest.configuration.compatibilityVersion,
97
+ sha256: manifest.configuration.sha256,
98
+ revision: manifest.configuration.revision,
99
+ },
100
+ images: manifest.images.map((
101
+ image: DyFM_AtomicRollbackImage_Interface,
102
+ ): DyFM_AtomicRollbackImage_Interface => ({
103
+ digest: image.digest,
104
+ mutableReference: image.mutableReference,
105
+ immutableReference: image.immutableReference,
106
+ version: image.version,
107
+ service: image.service,
108
+ })),
109
+ postChecks: manifest.postChecks.map((
110
+ check: DyFM_AtomicRollbackPostCheck_Interface,
111
+ ): DyFM_AtomicRollbackPostCheck_Interface => ({
112
+ expectedProbeContract: check.expectedProbeContract,
113
+ endpoint: check.endpoint,
114
+ service: check.service,
115
+ checkId: check.checkId,
116
+ })),
117
+ };
118
+
119
+ expect(DyFM_AtomicRollbackManifest_Util.computeTargetChecksum(reordered)).toBe(manifest.targetChecksum);
120
+ expect(DyFM_AtomicRollbackManifest_Util.validate(reordered).valid).toBeTrue();
121
+ });
122
+
123
+ it('| rejects missing, duplicated and non-targeted service members', (): void => {
124
+ const missing: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
125
+
126
+ missing.images.pop();
127
+ const duplicated: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
128
+
129
+ duplicated.images[1] = { ...duplicated.images[0] };
130
+
131
+ expect(DyFM_AtomicRollbackManifest_Util.validate(missing).gapCodes).toContain(
132
+ 'FDP|ATOMIC_ROLLBACK|POST_CHECK_SERVICE_NOT_TARGETED',
133
+ );
134
+ expect(DyFM_AtomicRollbackManifest_Util.validate(duplicated).gapCodes).toContain(
135
+ 'FDP|ATOMIC_ROLLBACK|IMAGE_SERVICE_DUPLICATED',
136
+ );
137
+ });
138
+
139
+ it('| rejects tag, version, digest and repository substitution', (): void => {
140
+ const mutable: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
141
+
142
+ mutable.images[0].immutableReference = 'registry.example/company-toolmaker-server:release';
143
+ const versionMismatch: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
144
+
145
+ versionMismatch.images[0].version = '0.0.2';
146
+ const digestInvalid: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
147
+
148
+ digestInvalid.images[0].digest = 'sha256:not-a-digest';
149
+ const repositoryMismatch: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
150
+
151
+ repositoryMismatch.images[0].mutableReference = 'other.example/company-toolmaker-server:release';
152
+
153
+ expect(DyFM_AtomicRollbackManifest_Util.validate(mutable).gapCodes).toContain(
154
+ 'FDP|ATOMIC_ROLLBACK|IMAGE_IMMUTABLE_REFERENCE_INVALID|company-toolmaker-server',
155
+ );
156
+ expect(DyFM_AtomicRollbackManifest_Util.validate(versionMismatch).gapCodes).toContain(
157
+ 'FDP|ATOMIC_ROLLBACK|IMAGE_IMMUTABLE_REFERENCE_INVALID|company-toolmaker-server',
158
+ );
159
+ expect(DyFM_AtomicRollbackManifest_Util.validate(digestInvalid).gapCodes).toContain(
160
+ 'FDP|ATOMIC_ROLLBACK|IMAGE_DIGEST_INVALID|company-toolmaker-server',
161
+ );
162
+ expect(DyFM_AtomicRollbackManifest_Util.validate(repositoryMismatch).gapCodes).toContain(
163
+ 'FDP|ATOMIC_ROLLBACK|IMAGE_REPOSITORY_MISMATCH|company-toolmaker-server',
164
+ );
165
+ });
166
+
167
+ it('| rejects configuration, authority and target-checksum substitution', (): void => {
168
+ const configuration: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
169
+
170
+ configuration.configuration.sha256 = '9'.repeat(64);
171
+ const authority: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
172
+
173
+ authority.authority.evidenceId = 'not-evidence';
174
+ const checksum: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
175
+
176
+ checksum.targetChecksum = 'a'.repeat(64);
177
+
178
+ expect(DyFM_AtomicRollbackManifest_Util.validate(configuration).gapCodes).toEqual([
179
+ 'FDP|ATOMIC_ROLLBACK|TARGET_CHECKSUM_MISMATCH',
180
+ ]);
181
+ expect(DyFM_AtomicRollbackManifest_Util.validate(authority).gapCodes).toContain(
182
+ 'FDP|ATOMIC_ROLLBACK|AUTHORITY_EVIDENCE_INVALID',
183
+ );
184
+ expect(DyFM_AtomicRollbackManifest_Util.validate(checksum).gapCodes).toEqual([
185
+ 'FDP|ATOMIC_ROLLBACK|TARGET_CHECKSUM_MISMATCH',
186
+ ]);
187
+ });
188
+
189
+ it('| rejects oversized operational references before they can expand evidence or work', (): void => {
190
+ const imageReference: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
191
+
192
+ imageReference.images[0].immutableReference =
193
+ `registry.example/${'segment/'.repeat(70)}company-toolmaker-server:release-0.0.1`;
194
+ imageReference.targetChecksum = DyFM_AtomicRollbackManifest_Util.computeTargetChecksum(imageReference);
195
+ const endpoint: DyFM_AtomicRollbackManifest_Interface = dyfm_createManifest();
196
+
197
+ endpoint.postChecks[0].endpoint = `https://${'a'.repeat(2040)}.example/readiness`;
198
+ endpoint.targetChecksum = DyFM_AtomicRollbackManifest_Util.computeTargetChecksum(endpoint);
199
+
200
+ expect(DyFM_AtomicRollbackManifest_Util.validate(imageReference).gapCodes).toContain(
201
+ 'FDP|ATOMIC_ROLLBACK|IMAGE_IMMUTABLE_REFERENCE_INVALID|company-toolmaker-server',
202
+ );
203
+ expect(DyFM_AtomicRollbackManifest_Util.validate(endpoint).gapCodes).toContain(
204
+ 'FDP|ATOMIC_ROLLBACK|POST_CHECK_ENDPOINT_INVALID|server-readiness',
205
+ );
206
+ });
207
+
208
+ it('| never reflects unknown content in validation evidence', (): void => {
209
+ const candidate: Record<string, unknown> = {
210
+ ...dyfm_createManifest(),
211
+ workbookName: 'sensitive-customer.xlsx',
212
+ };
213
+ const report: DyFM_AtomicRollbackLocalValidation_Interface = DyFM_AtomicRollbackManifest_Util.validate(candidate);
214
+ const serialized: string = JSON.stringify(report);
215
+
216
+ expect(report.gapCodes).toEqual([ 'FDP|ATOMIC_ROLLBACK|MANIFEST_SHAPE_INVALID' ]);
217
+ expect(serialized).not.toContain('sensitive-customer');
218
+ expect(serialized).not.toContain('registry.example');
219
+ expect(serialized).not.toContain('workbookName');
220
+ });
221
+ });