gdc-common-utils-ts 1.0.1

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 (100) hide show
  1. package/PUBLISHING.md +33 -0
  2. package/__tests__/AesManager.test.ts +53 -0
  3. package/__tests__/CryptographyService.test.ts +194 -0
  4. package/__tests__/bundle.test.ts +29 -0
  5. package/__tests__/content.test.ts +72 -0
  6. package/__tests__/crypto-encode-decode.test.ts +52 -0
  7. package/__tests__/crypto-hmac.test.ts +21 -0
  8. package/__tests__/did-generateServiceId.errors.test.ts +8 -0
  9. package/__tests__/did-generateServiceId.test.ts +18 -0
  10. package/__tests__/models-clinical-sections.test.ts +32 -0
  11. package/__tests__/models-multibase58.test.ts +33 -0
  12. package/__tests__/multibase58.errors.test.ts +7 -0
  13. package/__tests__/multibase58.test.ts +28 -0
  14. package/__tests__/multibasehash.test.ts +25 -0
  15. package/__tests__/utils-actor.test.ts +22 -0
  16. package/__tests__/utils-base-convert.test.ts +57 -0
  17. package/__tests__/utils-baseN.test.ts +40 -0
  18. package/__tests__/utils-did-extra.test.ts +33 -0
  19. package/__tests__/utils-format-converter.test.ts +87 -0
  20. package/__tests__/utils-jwt.test.ts +57 -0
  21. package/__tests__/utils-manager-error.test.ts +11 -0
  22. package/__tests__/utils-normalize.test.ts +15 -0
  23. package/__tests__/utils-object-convert.test.ts +38 -0
  24. package/__tests__/utils-string-convert.test.ts +20 -0
  25. package/__tests__/utils-string-utils.test.ts +25 -0
  26. package/__tests__/utils-url.test.ts +21 -0
  27. package/babel.config.cjs +5 -0
  28. package/jest.config.ts +46 -0
  29. package/package.json +36 -0
  30. package/src/AesManager.ts +82 -0
  31. package/src/CryptographyService.ts +461 -0
  32. package/src/JweManager.ts.txt +365 -0
  33. package/src/KmsService.txt +493 -0
  34. package/src/constants/Schemas.ts +61 -0
  35. package/src/constants/index.ts +1 -0
  36. package/src/constants/schemaorg.ts +193 -0
  37. package/src/cryptoDecode.ts +104 -0
  38. package/src/cryptoEncode.ts +36 -0
  39. package/src/cryptography.abstract.ts +29 -0
  40. package/src/hmac.ts +15 -0
  41. package/src/index.ts +3 -0
  42. package/src/interfaces/Cryptography.types.ts +131 -0
  43. package/src/interfaces/ICryptoHelper.ts +33 -0
  44. package/src/interfaces/ICryptography.ts +177 -0
  45. package/src/interfaces/IWallet.ts +62 -0
  46. package/src/interfaces/MlDsa.ts +25 -0
  47. package/src/interfaces/MlKem.ts +18 -0
  48. package/src/models/aes.ts +93 -0
  49. package/src/models/auth.ts +38 -0
  50. package/src/models/bundle.ts +152 -0
  51. package/src/models/bundle.txt +93 -0
  52. package/src/models/clinical-sections.en.ts +82 -0
  53. package/src/models/clinical-sections.ts +64 -0
  54. package/src/models/comm.ts +63 -0
  55. package/src/models/confidential-job.ts +100 -0
  56. package/src/models/confidential-message.ts +137 -0
  57. package/src/models/confidential-storage.ts +170 -0
  58. package/src/models/consent-rule.ts +141 -0
  59. package/src/models/crypto.ts +43 -0
  60. package/src/models/device-license.ts +161 -0
  61. package/src/models/did.ts +81 -0
  62. package/src/models/index.ts +31 -0
  63. package/src/models/indexing.ts +20 -0
  64. package/src/models/issue.ts +85 -0
  65. package/src/models/jsonapi.ts +19 -0
  66. package/src/models/jwe.ts +132 -0
  67. package/src/models/jwk.ts +50 -0
  68. package/src/models/jws.ts +42 -0
  69. package/src/models/jwt.ts +15 -0
  70. package/src/models/multibase58.ts +46 -0
  71. package/src/models/oidc4ida.common.model.ts +39 -0
  72. package/src/models/oidc4ida.document.model.ts +61 -0
  73. package/src/models/oidc4ida.electronicRecord.model.ts +86 -0
  74. package/src/models/oidc4ida.evidence.model.ts +69 -0
  75. package/src/models/openid-device.ts +146 -0
  76. package/src/models/operation-outcome.ts +34 -0
  77. package/src/models/params.ts +142 -0
  78. package/src/models/resource-document.ts +21 -0
  79. package/src/models/response.ts +5 -0
  80. package/src/models/urlPath.ts +76 -0
  81. package/src/models/verifiable-credential.ts +52 -0
  82. package/src/types/noble-hashes.d.ts +4 -0
  83. package/src/utils/actor.ts +52 -0
  84. package/src/utils/base-convert.ts +77 -0
  85. package/src/utils/baseN.ts +203 -0
  86. package/src/utils/bundle.ts +30 -0
  87. package/src/utils/content.ts +66 -0
  88. package/src/utils/did.ts +155 -0
  89. package/src/utils/format-converter.ts +119 -0
  90. package/src/utils/index.ts +13 -0
  91. package/src/utils/jwt.ts +165 -0
  92. package/src/utils/manager-error.ts +27 -0
  93. package/src/utils/multibase58.ts +46 -0
  94. package/src/utils/multibasehash.ts +28 -0
  95. package/src/utils/normalize.ts +43 -0
  96. package/src/utils/object-convert.ts +57 -0
  97. package/src/utils/string-convert.ts +71 -0
  98. package/src/utils/string-utils.ts +70 -0
  99. package/src/utils/url.ts +46 -0
  100. package/tsconfig.json +13 -0
package/PUBLISHING.md ADDED
@@ -0,0 +1,33 @@
1
+ # Publishing
2
+
3
+ This package is published as a public, unscoped package (`gdc-common-utils-ts`).
4
+
5
+ ## NPM token
6
+
7
+ When creating a token in npmjs.com:
8
+ - `Packages and scopes` must be **Read and write**.
9
+ - `Organizations` permissions are **not required** for unscoped packages. Enable only if publishing under an org scope (`@org/`).
10
+
11
+ ## Configure the token
12
+
13
+ ```bash
14
+ npm config set //registry.npmjs.org/:_authToken=YOUR_TOKEN
15
+ ```
16
+
17
+ Verify:
18
+
19
+ ```bash
20
+ npm whoami
21
+ ```
22
+
23
+ ## Publish
24
+
25
+ ```bash
26
+ npm publish --access public
27
+ ```
28
+
29
+ If 2FA for publish is enabled, add an OTP:
30
+
31
+ ```bash
32
+ NPM_CONFIG_OTP=123456 npm publish --access public
33
+ ```
@@ -0,0 +1,53 @@
1
+ // src/__tests__/unit/security/AesManager.test.ts
2
+ // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
3
+
4
+ import { jest } from '@jest/globals';
5
+ import { randomBytes } from 'crypto'; // Use Node.js native crypto module
6
+ import { ProtectedDataAES } from '../src/models/aes.js';
7
+ import { AesManager } from '../src/AesManager.js';
8
+
9
+ describe('AesManager', () => {
10
+ let aesManager: AesManager;
11
+
12
+ beforeEach(() => {
13
+ aesManager = new AesManager();
14
+ });
15
+
16
+ it('should perform a round-trip (encrypt/decrypt) using base64url strings', async () => {
17
+ // --- 1. Arrange ---
18
+ const payload = { message: 'This is a secret message.', timestamp: Date.now() };
19
+ const plaintext = JSON.stringify(payload);
20
+ const cekBytes = randomBytes(32); // 256-bit Content Encryption Key
21
+ const aad = 'integrity-protected-data';
22
+ // --- 2. Act ---
23
+ // The public interface works with base64url strings, as required by JWE.
24
+ const encryptedData: ProtectedDataAES = await aesManager.encrypt(plaintext, cekBytes, aad);
25
+
26
+ // The result is then passed back to the decryption method.
27
+ const decryptedText = await aesManager.decrypt(encryptedData, cekBytes, aad);
28
+ const decryptedPayload = JSON.parse(decryptedText);
29
+ // --- 3. Assert ---
30
+ // The output of encrypt should be correctly formatted strings.
31
+ expect(typeof encryptedData.ciphertext).toBe('string');
32
+ expect(typeof encryptedData.iv).toBe('string');
33
+ expect(typeof encryptedData.tag).toBe('string');
34
+
35
+ // The final decrypted object must match the original payload.
36
+ expect(decryptedPayload).toEqual(payload);
37
+ });
38
+
39
+ it('should fail decryption if the AAD is tampered with', async () => {
40
+ // --- 1. Arrange ---
41
+ const plaintext = JSON.stringify({ data: 'secret' });
42
+ const cekBytes = randomBytes(32);
43
+ const originalAad = 'original_aad';
44
+ const tamperedAad = 'tampered_aad';
45
+
46
+ const encryptedData = await aesManager.encrypt(plaintext, cekBytes, originalAad);
47
+ // --- 2. Assert ---
48
+ // Expect decryption to fail when using the wrong AAD.
49
+ await expect(
50
+ aesManager.decrypt(encryptedData, cekBytes, tamperedAad)
51
+ ).rejects.toThrow();
52
+ });
53
+ });
@@ -0,0 +1,194 @@
1
+ // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+
3
+ import { randomBytes, createHash, randomUUID } from 'crypto';
4
+ import { CryptographyService } from '../src/CryptographyService';
5
+ import type { ICryptoHelper } from '../src/interfaces/ICryptoHelper';
6
+ import { Content } from '../src/utils/content';
7
+ import { MlDsaPrivKeySizeLevel2, MlDsaPubKeySizeLevel2 } from '../src/interfaces/MlDsa';
8
+ import { Kyber512PKBytes } from '../src/interfaces/MlKem';
9
+
10
+ const cryptoHelper: ICryptoHelper = {
11
+ async getRandomBytes(byteCount: number): Promise<Uint8Array> {
12
+ return randomBytes(byteCount);
13
+ },
14
+ async digestString(data: string, algorithm: string): Promise<string> {
15
+ const normalized = String(algorithm).replace('-', '').toLowerCase();
16
+ return createHash(normalized).update(data).digest('hex');
17
+ },
18
+ randomUUID(): string {
19
+ return randomUUID();
20
+ },
21
+ };
22
+
23
+ describe('CryptographyService (PQC)', () => {
24
+ const service = new CryptographyService(cryptoHelper);
25
+
26
+ it('generates ML-KEM key pairs and encapsulates/decapsulates', async () => {
27
+ const { publicJWKey, secretKeyBytes } = await service.generateKeyPairMlKem();
28
+ expect(publicJWKey?.x).toBeDefined();
29
+ expect(publicJWKey?.kid).toBeDefined();
30
+ expect(secretKeyBytes.length).toBeGreaterThan(0);
31
+
32
+ const recipientPublicKeyBytes = Content.base64ToBytes(publicJWKey.x);
33
+ const cekSeedBytes = await cryptoHelper.getRandomBytes(32);
34
+ const { derivedCekBytes, encapsulatedCekBytes } = await service.encapsulate(
35
+ cekSeedBytes,
36
+ secretKeyBytes,
37
+ recipientPublicKeyBytes
38
+ );
39
+ const derivedFromDecap = await service.decapsulate(encapsulatedCekBytes, secretKeyBytes);
40
+
41
+ expect(derivedCekBytes.length).toBeGreaterThan(0);
42
+ expect(derivedFromDecap.length).toBe(derivedCekBytes.length);
43
+ expect(Buffer.from(derivedFromDecap).equals(Buffer.from(derivedCekBytes))).toBe(true);
44
+ });
45
+
46
+ it('generates deterministic ML-KEM key pairs with a seed', async () => {
47
+ const seed = new Uint8Array(64).fill(7);
48
+ const first = await service.generateKeyPairMlKem(seed, 'ML-KEM-512');
49
+ const second = await service.generateKeyPairMlKem(seed, 'ML-KEM-512');
50
+ expect(first.publicJWKey.x).toBe(second.publicJWKey.x);
51
+ expect(first.publicJWKey.kid).toBe(second.publicJWKey.kid);
52
+ expect(first.secretKeyBytes.length).toBeGreaterThan(0);
53
+ expect(Content.base64ToBytes(first.publicJWKey.x).length).toBe(Kyber512PKBytes);
54
+ });
55
+
56
+ it('generates ML-DSA key pairs with expected sizes', async () => {
57
+ const seed = new Uint8Array(32).fill(3);
58
+ const { publicJWKey, secretKeyBytes } = await service.generateKeyPairMlDsa(seed, 'ML-DSA-44');
59
+ expect(publicJWKey.alg).toBe('ML-DSA-44');
60
+ expect(secretKeyBytes.length).toBeGreaterThanOrEqual(MlDsaPrivKeySizeLevel2);
61
+ expect(Content.base64ToBytes(publicJWKey.pub).length).toBe(MlDsaPubKeySizeLevel2);
62
+ });
63
+
64
+ it('signs and verifies with ML-DSA', async () => {
65
+ const { publicJWKey, secretKeyBytes } = await service.generateKeyPairMlDsa(undefined, 'ML-DSA-44');
66
+ const payload = Content.stringToBytesUTF8('test-payload');
67
+
68
+ const signature = await service.signBytes(payload, secretKeyBytes, 'ML-DSA-44');
69
+ const verified = await service.verifyBytes(signature, payload, publicJWKey);
70
+ expect(verified).toBe(true);
71
+
72
+ const tampered = Content.stringToBytesUTF8('test-payload-tampered');
73
+ const verifiedTampered = await service.verifyBytes(signature, tampered, publicJWKey);
74
+ expect(verifiedTampered).toBe(false);
75
+ });
76
+
77
+ it('signs JWS objects and verifies compact + detached forms', async () => {
78
+ const { publicJWKey, secretKeyBytes } = await service.generateKeyPairMlDsa(undefined, 'ML-DSA-44');
79
+ const protectedHeader = { alg: 'ML-DSA-44', typ: 'JWT' };
80
+ const payload = { sub: '123', aud: 'test' };
81
+
82
+ const jws = await service.signDataJws(payload, protectedHeader, secretKeyBytes);
83
+ const compact = `${jws.protected}.${jws.payload}.${jws.signature}`;
84
+ const verified = await service.verifyJws(compact, publicJWKey);
85
+ expect(verified).toBe(true);
86
+
87
+ const detached = `${jws.protected}..${jws.signature}`;
88
+ const payloadBytes = Content.stringToBytesUTF8(JSON.stringify(payload));
89
+ expect(await service.verifyDetachedJws(payloadBytes, detached, publicJWKey)).toBe(true);
90
+ });
91
+
92
+ it('throws on invalid detached JWS format', async () => {
93
+ await expect(service.verifyDetachedJws(new Uint8Array([1]), 'not-detached', { kty: 'AKP', alg: 'ML-DSA-44', pub: 'AA' }))
94
+ .rejects.toThrow(/Detached JWS format/);
95
+ });
96
+
97
+ it('throws on missing alg in headers for signing and verification', async () => {
98
+ const { publicJWKey, secretKeyBytes } = await service.generateKeyPairMlDsa(undefined, 'ML-DSA-44');
99
+ await expect(service.signDataJws({ a: 1 }, {}, secretKeyBytes)).rejects.toThrow(/alg/);
100
+
101
+ const missingAlg = { ...publicJWKey };
102
+ delete (missingAlg as any).alg;
103
+ await expect(service.verifyBytes(new Uint8Array([1]), new Uint8Array([2]), missingAlg as any)).rejects.toThrow(/alg/);
104
+ });
105
+
106
+ it('encrypts and decrypts JWE objects (zip + no zip)', async () => {
107
+ const recipient = await service.generateKeyPairMlKem(undefined, 'ML-KEM-768');
108
+ const recipientPrivate = { ...recipient.publicJWKey, dBytes: recipient.secretKeyBytes };
109
+
110
+ const payload = { msg: 'hello' };
111
+ const headerNoZip = { typ: 'JWE', kid: recipient.publicJWKey.kid };
112
+ const jwe = await service.encryptJwe(payload, headerNoZip, recipientPrivate, [recipient.publicJWKey]);
113
+ const decrypted = await service.decryptJwe(jwe, recipientPrivate);
114
+ expect(Content.bytesToStringUTF8(decrypted.decryptedBytes)).toBe(JSON.stringify(payload));
115
+
116
+ const headerZip = { typ: 'JWE', zip: 'DEF', kid: recipient.publicJWKey.kid };
117
+ const jweZip = await service.encryptJwe(payload, headerZip, recipientPrivate, [recipient.publicJWKey]);
118
+ const decryptedZip = await service.decryptJwe(jweZip, recipientPrivate);
119
+ expect(Content.bytesToStringUTF8(decryptedZip.decryptedBytes)).toBe(JSON.stringify(payload));
120
+ });
121
+
122
+ it('encrypts and decrypts compact JWE', async () => {
123
+ const recipient = await service.generateKeyPairMlKem(undefined, 'ML-KEM-768');
124
+ const recipientPrivate = { ...recipient.publicJWKey, dBytes: recipient.secretKeyBytes };
125
+
126
+ const payload = 'hello-compact';
127
+ const protectedHeader = { typ: 'JWE', zip: 'DEF' };
128
+ const compact = await service.encryptJweToCompact(payload, protectedHeader, recipientPrivate, recipient.publicJWKey);
129
+ const parsed = service.parseCompactJwe(compact);
130
+ const decrypted = await service.decryptJwe(parsed, recipientPrivate);
131
+ expect(Content.bytesToStringUTF8(decrypted.decryptedBytes)).toBe(payload);
132
+ });
133
+
134
+ it('rejects multiple recipients in encryptJwe', async () => {
135
+ const recipient = await service.generateKeyPairMlKem(undefined, 'ML-KEM-768');
136
+ const recipientPrivate = { ...recipient.publicJWKey, dBytes: recipient.secretKeyBytes };
137
+ await expect(
138
+ service.encryptJwe({ a: 1 }, { typ: 'JWE' }, recipientPrivate, [recipient.publicJWKey, recipient.publicJWKey])
139
+ ).rejects.toThrow(/single recipient/);
140
+ });
141
+
142
+ it('parses and rejects malformed compact structures', async () => {
143
+ expect(() => service.parseCompactJws('a.b')).toThrow(/Compact JWS/);
144
+ expect(() => service.parseCompactJwe('a.b.c')).toThrow(/Compact JWE/);
145
+ });
146
+
147
+ it('parses compact JWS into structured data', async () => {
148
+ const { publicJWKey, secretKeyBytes } = await service.generateKeyPairMlDsa(undefined, 'ML-DSA-44');
149
+ const jws = await service.signDataJws({ sub: 'abc' }, { alg: 'ML-DSA-44' }, secretKeyBytes);
150
+ const compact = `${jws.protected}.${jws.payload}.${jws.signature}`;
151
+ const parsed = service.parseCompactJws(compact);
152
+ expect(parsed.payload).toEqual({ sub: 'abc' });
153
+ expect(parsed.protected).toEqual({ alg: 'ML-DSA-44' });
154
+ expect(await service.verifyJws(compact, publicJWKey)).toBe(true);
155
+ });
156
+
157
+ it('parses JWS JSON serialization', async () => {
158
+ const { secretKeyBytes } = await service.generateKeyPairMlDsa(undefined, 'ML-DSA-44');
159
+ const jws = await service.signDataJws({ sub: 'json' }, { alg: 'ML-DSA-44' }, secretKeyBytes);
160
+ const jwsJson = JSON.stringify({
161
+ payload: jws.payload,
162
+ signatures: [{ protected: jws.protected, signature: jws.signature }],
163
+ });
164
+ const parsed = service.parseCompactJws(jwsJson);
165
+ expect(parsed.payload).toBe(jws.payload);
166
+ expect(parsed.protected).toBe(jws.protected);
167
+ expect(parsed.signature).toBe(jws.signature);
168
+ });
169
+
170
+ it('extracts recipient kids from JWE objects', async () => {
171
+ const recipient = await service.generateKeyPairMlKem(undefined, 'ML-KEM-768');
172
+ const recipientPrivate = { ...recipient.publicJWKey, dBytes: recipient.secretKeyBytes };
173
+ const jwe = await service.encryptJwe({ a: 1 }, { typ: 'JWE', kid: recipient.publicJWKey.kid }, recipientPrivate, [recipient.publicJWKey]);
174
+ expect(service.getRecipientKidsFromJwe(jwe)).toEqual([recipient.publicJWKey.kid]);
175
+ expect(service.getRecipientKidsFromJwe({} as any)).toEqual([]);
176
+ });
177
+
178
+ it('parses JWE JSON serialization', async () => {
179
+ const recipient = await service.generateKeyPairMlKem(undefined, 'ML-KEM-768');
180
+ const recipientPrivate = { ...recipient.publicJWKey, dBytes: recipient.secretKeyBytes };
181
+ const jwe = await service.encryptJwe({ msg: 'json' }, { typ: 'JWE', kid: recipient.publicJWKey.kid }, recipientPrivate, [recipient.publicJWKey]);
182
+ const parsed = service.parseCompactJwe(JSON.stringify(jwe));
183
+ const decrypted = await service.decryptJwe(parsed, recipientPrivate);
184
+ expect(Content.bytesToStringUTF8(decrypted.decryptedBytes)).toBe(JSON.stringify({ msg: 'json' }));
185
+ });
186
+
187
+ it('computes JWK thumbprints for EC keys', async () => {
188
+ const ecJwk = { kty: 'EC', crv: 'P-256', x: 'AA', y: 'BB' };
189
+ const thumbprint = await (service as any)._computeJwkThumbprint(ecJwk, 'SHA-384');
190
+ const thumbprint2 = await (service as any)._computeJwkThumbprint(ecJwk, 'SHA-384');
191
+ expect(thumbprint).toBe(thumbprint2);
192
+ expect(thumbprint.length).toBeGreaterThan(0);
193
+ });
194
+ });
@@ -0,0 +1,29 @@
1
+ // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+
3
+ import { extractResources, getNextLink } from '../src/utils/bundle';
4
+
5
+ describe('bundle utils', () => {
6
+ it('extracts resources from FHIR bundle entry', () => {
7
+ const bundle = {
8
+ entry: [{ resource: { id: 'r1' } }, { resource: { id: 'r2' } }],
9
+ };
10
+ expect(extractResources(bundle)).toEqual([{ id: 'r1' }, { id: 'r2' }]);
11
+ });
12
+
13
+ it('extracts resources from JSON:API data', () => {
14
+ const bundle = {
15
+ data: [{ resource: { id: 'r1' } }, { id: 'r2' }],
16
+ };
17
+ expect(extractResources(bundle)).toEqual([{ id: 'r1' }, { id: 'r2' }]);
18
+ });
19
+
20
+ it('returns resource fallback when bundle is a resource', () => {
21
+ const bundle = { resourceType: 'Patient', id: 'p1' };
22
+ expect(extractResources(bundle)).toEqual([bundle]);
23
+ });
24
+
25
+ it('reads next link', () => {
26
+ const bundle = { link: [{ relation: 'next', url: 'https://next' }] };
27
+ expect(getNextLink(bundle)).toBe('https://next');
28
+ });
29
+ });
@@ -0,0 +1,72 @@
1
+ // src/__tests__/unit/utils/convert.test.ts
2
+
3
+ import { Content } from '../src/utils/content.js';
4
+
5
+ describe("Content Class - Conversion Utilities", () => {
6
+
7
+ describe("Base58 Conversions", () => {
8
+ it("should encode and decode a string to/from Base58", () => {
9
+ const testString = "Hello world!";
10
+ const testBytes = Content.stringToBytesUTF8(testString);
11
+
12
+ // Encode
13
+ const encoded = Content.bytesToBase58(testBytes);
14
+ expect(encoded).toBe("2NEpo7TZRhna7vSvL");
15
+
16
+ // Decode
17
+ const decodedBytes = Content.base58ToBytes(encoded);
18
+ const decodedString = Content.bytesToStringUTF8(decodedBytes);
19
+
20
+ expect(decodedString).toBe(testString);
21
+ });
22
+ });
23
+
24
+ describe("Base64URL Conversions", () => {
25
+ it("should encode and decode bytes to/from raw Base64URL", () => {
26
+ const testString = "some important data!?";
27
+ const testBytes = Content.stringToBytesUTF8(testString);
28
+
29
+ // Encode
30
+ const encoded = Content.bytesToRawBase64UrlSafe(testBytes);
31
+ expect(encoded).toBe("c29tZSBpbXBvcnRhbnQgZGF0YSE_");
32
+
33
+ // Decode
34
+ const decodedBytes = Content.base64ToBytes(encoded);
35
+ expect(decodedBytes).toEqual(testBytes);
36
+ });
37
+ });
38
+
39
+ describe("Object Serialization", () => {
40
+ it("should serialize and deserialize an object to/from raw Base64URL", () => {
41
+ const testObject = {
42
+ id: "12345",
43
+ aud: ["did:web:example.com"],
44
+ exp: 1678886400,
45
+ verified: true,
46
+ };
47
+
48
+ // Serialize
49
+ const encoded = Content.objectToRawBase64UrlSafe(testObject);
50
+
51
+ // Deserialize
52
+ const decodedObject = Content.base64UrlSafeToJSON(encoded);
53
+
54
+ expect(decodedObject).toEqual(testObject);
55
+ });
56
+ });
57
+
58
+ describe("Array Utilities", () => {
59
+ it("should correctly compare two identical arrays of primitives", () => {
60
+ const arr1 = [1, 2, "hello", true, null];
61
+ const arr2 = [1, 2, "hello", true, null];
62
+ expect(Content.arrayCompare(arr1, arr2)).toBe(true);
63
+ });
64
+
65
+ it("should correctly identify two different arrays", () => {
66
+ const arr1 = [1, 2, 5];
67
+ const arr2 = [1, 2, 9];
68
+ expect(Content.arrayCompare(arr1, arr2)).toBe(false);
69
+ });
70
+ });
71
+ });
72
+
@@ -0,0 +1,52 @@
1
+ import { encodeJWT as encodeJWTEncode } from '../src/cryptoEncode.js';
2
+ import { encodeJWT as encodeJWTDecode, decodeBase64Url, decodePayloadRequest } from '../src/cryptoDecode.js';
3
+
4
+ function base64UrlEncode(str: string): string {
5
+ return Buffer.from(str)
6
+ .toString('base64')
7
+ .replace(/\+/g, '-')
8
+ .replace(/\//g, '_')
9
+ .replace(/=+$/, '');
10
+ }
11
+
12
+ describe('crypto encode/decode', () => {
13
+ it('encodes JWTs with alg none (cryptoEncode)', () => {
14
+ const header = { alg: 'none' };
15
+ const payload = { sub: '123' };
16
+ const token = encodeJWTEncode([], payload, [], header);
17
+ const parts = token.split('.');
18
+ expect(parts.length).toBe(3);
19
+ expect(decodeBase64Url(parts[0])).toBe(JSON.stringify(header));
20
+ expect(decodeBase64Url(parts[1])).toBe(JSON.stringify(payload));
21
+ });
22
+
23
+ it('encodes JWTs with alg none (cryptoDecode)', () => {
24
+ const header = { alg: 'none' };
25
+ const payload = { sub: '456' };
26
+ const token = encodeJWTDecode([], payload, [], header);
27
+ const parts = token.split('.');
28
+ expect(decodeBase64Url(parts[0])).toBe(JSON.stringify(header));
29
+ expect(decodeBase64Url(parts[1])).toBe(JSON.stringify(payload));
30
+ });
31
+
32
+ it('decodes payloads from authorization headers', () => {
33
+ const header = { alg: 'none' };
34
+ const payload = { a: 1 };
35
+ const token = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(payload))}.`;
36
+ const decoded = decodePayloadRequest('tenant', undefined, `Bearer ${token}`);
37
+ expect(decoded).toBe(JSON.stringify(payload));
38
+ });
39
+
40
+ it('returns null when token is missing', () => {
41
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
42
+ expect(decodePayloadRequest('tenant', undefined, undefined)).toBeNull();
43
+ spy.mockRestore();
44
+ });
45
+
46
+ it('returns empty string for invalid payload JSON', () => {
47
+ const invalid = base64UrlEncode('not-json');
48
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
49
+ expect(decodeBase64Url(invalid)).toBe('');
50
+ spy.mockRestore();
51
+ });
52
+ });
@@ -0,0 +1,21 @@
1
+ import { computeHmacSha256, computeHmacSha256Base64Url } from '../src/hmac.js';
2
+
3
+ describe('hmac utilities', () => {
4
+ it('computes HMAC-SHA3-256 bytes deterministically', async () => {
5
+ const key = new Uint8Array([1, 2, 3, 4]);
6
+ const message = 'hello';
7
+ const first = await computeHmacSha256(message, key);
8
+ const second = await computeHmacSha256(message, key);
9
+ expect(Array.from(first)).toEqual(Array.from(second));
10
+ expect(first.length).toBe(32);
11
+ });
12
+
13
+ it('computes base64url HMAC output consistently', async () => {
14
+ const key = new Uint8Array([9, 8, 7, 6]);
15
+ const message = 'hello';
16
+ const first = await computeHmacSha256Base64Url(message, key);
17
+ const second = await computeHmacSha256Base64Url(message, key);
18
+ expect(first).toBe(second);
19
+ expect(first.length).toBe(43);
20
+ });
21
+ });
@@ -0,0 +1,8 @@
1
+ import { generateServiceId } from '../src/utils/did.js';
2
+
3
+ describe('generateServiceId (edge cases)', () => {
4
+ it('keeps action casing', () => {
5
+ const id = generateServiceId({ section: 'identity', format: 'openid', resourceType: 'device', action: '_DCR' });
6
+ expect(id).toBe('#identity:openid:device:_DCR');
7
+ });
8
+ });
@@ -0,0 +1,18 @@
1
+ import { generateServiceId } from '../src/utils/did.js';
2
+
3
+ describe('generateServiceId', () => {
4
+ it('builds #section:format:resourceType:action', () => {
5
+ const id = generateServiceId({
6
+ section: 'identity',
7
+ format: 'openid',
8
+ resourceType: 'Device',
9
+ action: '_dcr',
10
+ });
11
+ expect(id).toBe('#identity:openid:device:_dcr');
12
+ });
13
+
14
+ it('filters empty parts', () => {
15
+ const id = generateServiceId({ section: 'x', format: 'y', resourceType: '', action: '_batch' });
16
+ expect(id).toBe('#x:y:_batch');
17
+ });
18
+ });
@@ -0,0 +1,32 @@
1
+ import {
2
+ clinicalSectionRegistry,
3
+ getClinicalSectionByCode,
4
+ loincI18nKey,
5
+ supportedClinicalSectionCodes,
6
+ clinicalSectionTitleEn,
7
+ } from '../src/models/clinical-sections.js';
8
+
9
+ describe('clinical sections', () => {
10
+ it('builds i18n keys from LOINC codes', () => {
11
+ expect(loincI18nKey('1234-5')).toBe('org.loinc.1234-5');
12
+ });
13
+
14
+ it('exposes supported section codes', () => {
15
+ const codes = Object.keys(clinicalSectionTitleEn);
16
+ expect(supportedClinicalSectionCodes.length).toBe(codes.length);
17
+ expect(supportedClinicalSectionCodes).toContain(codes[0]);
18
+ });
19
+
20
+ it('provides registry entries', () => {
21
+ const code = supportedClinicalSectionCodes[0];
22
+ const entry = clinicalSectionRegistry[code];
23
+ expect(entry.code).toBe(code);
24
+ expect(entry.i18nKey).toBe(loincI18nKey(code));
25
+ });
26
+
27
+ it('gets clinical section by code', () => {
28
+ const code = supportedClinicalSectionCodes[0];
29
+ expect(getClinicalSectionByCode(code)?.code).toBe(code);
30
+ expect(getClinicalSectionByCode('missing')).toBeUndefined();
31
+ });
32
+ });
@@ -0,0 +1,33 @@
1
+ import {
2
+ decodeMultibase58btc,
3
+ decodeMultibase58btcToHex,
4
+ decodeMultibase58btcToUUID,
5
+ encodeHexToMultibase58btc,
6
+ encodeMultibase58btc,
7
+ } from '../src/models/multibase58.js';
8
+
9
+ describe('models multibase58', () => {
10
+ it('round-trips hex to multibase and back', () => {
11
+ const hex = '00112233445566778899aabbccddeeff';
12
+ const encoded = encodeHexToMultibase58btc(hex);
13
+ const decodedHex = decodeMultibase58btcToHex(encoded);
14
+ expect(decodedHex).toBe(hex);
15
+ });
16
+
17
+ it('decodes to UUID with hyphens', () => {
18
+ const hex = '00112233445566778899aabbccddeeff';
19
+ const encoded = encodeHexToMultibase58btc(hex);
20
+ expect(decodeMultibase58btcToUUID(encoded)).toBe('00112233-4455-6677-8899-aabbccddeeff');
21
+ });
22
+
23
+ it('encodes and decodes raw bytes', () => {
24
+ const bytes = new Uint8Array([9, 9, 9]);
25
+ const encoded = encodeMultibase58btc(bytes);
26
+ const decoded = decodeMultibase58btc(encoded);
27
+ expect(Array.from(decoded)).toEqual([9, 9, 9]);
28
+ });
29
+
30
+ it('throws for invalid hex input', () => {
31
+ expect(() => encodeHexToMultibase58btc('not-hex')).toThrow(/Invalid 16-byte hex string/);
32
+ });
33
+ });
@@ -0,0 +1,7 @@
1
+ import { decodeMultibase58btc } from '../src/utils/multibase58.js';
2
+
3
+ describe('multibase58 (errors)', () => {
4
+ it('throws on empty string', () => {
5
+ expect(() => decodeMultibase58btc('')).toThrow();
6
+ });
7
+ });
@@ -0,0 +1,28 @@
1
+ import {
2
+ decodeMultibase58btc,
3
+ decodeMultibase58btcToHex,
4
+ decodeMultibase58btcToUUID,
5
+ encodeHexToMultibase58btc,
6
+ encodeMultibase58btc,
7
+ } from '../src/utils/multibase58.js';
8
+
9
+ describe('multibase58', () => {
10
+ it('round-trips bytes', () => {
11
+ const bytes = new Uint8Array([1, 2, 3, 250, 251, 252]);
12
+ const encoded = encodeMultibase58btc(bytes);
13
+ expect(encoded.startsWith('z')).toBe(true);
14
+ const decoded = decodeMultibase58btc(encoded);
15
+ expect(Array.from(decoded)).toEqual(Array.from(bytes));
16
+ });
17
+
18
+ it('throws if prefix is missing', () => {
19
+ expect(() => decodeMultibase58btc('not-prefixed')).toThrow(/missing 'z' prefix/i);
20
+ });
21
+
22
+ it('round-trips hex to multibase and back', () => {
23
+ const hex = '00112233445566778899aabbccddeeff';
24
+ const encoded = encodeHexToMultibase58btc(hex);
25
+ expect(decodeMultibase58btcToHex(encoded)).toBe(hex);
26
+ expect(decodeMultibase58btcToUUID(encoded)).toBe('00112233-4455-6677-8899-aabbccddeeff');
27
+ });
28
+ });
@@ -0,0 +1,25 @@
1
+ // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+
3
+ import baseX from 'base-x';
4
+ import { encodeMultibaseSha384 } from '../src/utils/multibasehash';
5
+
6
+ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
7
+ const base58btc = baseX(BASE58_ALPHABET);
8
+
9
+ describe('encodeMultibaseSha384', () => {
10
+ it('returns a multibase base58btc string with SHA-384 prefix', () => {
11
+ const output = encodeMultibaseSha384('hello');
12
+ expect(output.startsWith('z')).toBe(true);
13
+
14
+ const decoded = base58btc.decode(output.slice(1));
15
+ expect(decoded[0]).toBe(0x15);
16
+ expect(decoded[1]).toBe(0x30);
17
+ expect(decoded.length).toBe(2 + 48);
18
+ });
19
+
20
+ it('is deterministic for same input', () => {
21
+ const a = encodeMultibaseSha384('same');
22
+ const b = encodeMultibaseSha384('same');
23
+ expect(a).toBe(b);
24
+ });
25
+ });
@@ -0,0 +1,22 @@
1
+ import { parseActorFromSub } from '../src/utils/actor.js';
2
+
3
+ describe('parseActorFromSub', () => {
4
+ it('returns trimmed subject and minimal info for empty input', () => {
5
+ expect(parseActorFromSub(' ')).toEqual({ sub: '' });
6
+ });
7
+
8
+ it('parses did:web subject with organization, email, and role', () => {
9
+ const sub = 'did:web:api.acme.org:employee:Doctor1@ACME.org:role:ISCO-08|2211';
10
+ const parsed = parseActorFromSub(sub);
11
+ expect(parsed.sub).toBe(sub);
12
+ expect(parsed.organization).toBe('did:web:api.acme.org');
13
+ expect(parsed.email).toBe('doctor1@acme.org');
14
+ expect(parsed.role).toBe('ISCO-08|2211');
15
+ });
16
+
17
+ it('parses raw email subject', () => {
18
+ const parsed = parseActorFromSub(' User@Example.com ');
19
+ expect(parsed.sub).toBe('User@Example.com');
20
+ expect(parsed.email).toBe('user@example.com');
21
+ });
22
+ });