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
@@ -0,0 +1,57 @@
1
+ import {
2
+ base58ToBytes,
3
+ base64OrUrlSafeToBytes,
4
+ base64ToBase64Url,
5
+ base64UrlToBase64,
6
+ bytesToBase58,
7
+ bytesToBase64,
8
+ bytesToHexString,
9
+ bytesToRawBase64UrlSafe,
10
+ stringToBase64Url,
11
+ stringToStdBase64,
12
+ } from '../src/utils/base-convert.js';
13
+ import { stringToBytesUTF8, bytesToStringUTF8 } from '../src/utils/string-convert.js';
14
+
15
+ function toArray(bytes: Uint8Array): number[] {
16
+ return Array.from(bytes);
17
+ }
18
+
19
+ describe('base-convert utilities', () => {
20
+ it('converts bytes to hex string', () => {
21
+ const hex = bytesToHexString(new Uint8Array([0, 15, 255]));
22
+ expect(hex).toBe('000fff');
23
+ });
24
+
25
+ it('round-trips base58 encoding', () => {
26
+ const bytes = stringToBytesUTF8('hello');
27
+ const encoded = bytesToBase58(bytes);
28
+ const decoded = base58ToBytes(encoded);
29
+ expect(toArray(decoded)).toEqual(toArray(bytes));
30
+ });
31
+
32
+ it('handles base64 and base64url conversions', () => {
33
+ const std = stringToStdBase64('hi');
34
+ expect(std).toBe('aGk=');
35
+ const url = stringToBase64Url('hi');
36
+ expect(url).toBe('aGk=');
37
+ expect(base64ToBase64Url('ab+/')).toBe('ab-_');
38
+ expect(base64UrlToBase64('ab-_')).toBe('ab+/');
39
+ });
40
+
41
+ it('decodes base64 or base64url into bytes', () => {
42
+ const withSlash = '//8='; // bytes [255, 255]
43
+ const fromBase64 = base64OrUrlSafeToBytes(withSlash);
44
+ expect(toArray(fromBase64)).toEqual([255, 255]);
45
+
46
+ const urlSafe = 'AQID'; // bytes [1,2,3]
47
+ const fromUrlSafe = base64OrUrlSafeToBytes(urlSafe);
48
+ expect(toArray(fromUrlSafe)).toEqual([1, 2, 3]);
49
+ });
50
+
51
+ it('encodes bytes to base64 and raw base64url', () => {
52
+ const bytes = stringToBytesUTF8('test');
53
+ expect(bytesToBase64(bytes)).toBe('dGVzdA==');
54
+ expect(bytesToRawBase64UrlSafe(bytes)).toBe('dGVzdA');
55
+ expect(bytesToStringUTF8(base64OrUrlSafeToBytes(bytesToRawBase64UrlSafe(bytes)))).toBe('test');
56
+ });
57
+ });
@@ -0,0 +1,40 @@
1
+ import { alphabetBase58, BaseN, decodeN, encodeN } from '../src/utils/baseN.js';
2
+ import { stringToBytesUTF8 } from '../src/utils/string-convert.js';
3
+
4
+ describe('baseN', () => {
5
+ it('round-trips base58 encoding/decoding', () => {
6
+ const bytes = stringToBytesUTF8('hello world');
7
+ const encoded = encodeN(bytes, alphabetBase58, undefined);
8
+ const decoded = decodeN(encoded, alphabetBase58);
9
+ expect(Array.from(decoded)).toEqual(Array.from(bytes));
10
+ });
11
+
12
+ it('supports maxline output wrapping', () => {
13
+ const bytes = stringToBytesUTF8('hello world');
14
+ const encoded = encodeN(bytes, alphabetBase58, 4 as unknown as any);
15
+ expect(encoded.includes('\r\n')).toBe(true);
16
+ });
17
+
18
+ it('supports BaseN class helpers', () => {
19
+ const bytes = new Uint8Array([0, 1, 2, 3]);
20
+ const encodedBytes = BaseN.encodeBytesToBase58(bytes);
21
+ expect(Array.from(BaseN.decodeBytesFromBase58(encodedBytes))).toEqual(Array.from(bytes));
22
+
23
+ const encodedStr = BaseN.encodeStrToBase58('hello');
24
+ expect(BaseN.decodeStrFromBase58(encodedStr)).toBe('hello');
25
+ });
26
+
27
+ it('throws on invalid encode inputs', () => {
28
+ expect(() => encodeN('nope' as unknown as Uint8Array, alphabetBase58, undefined)).toThrow(/Uint8Array/);
29
+ expect(() => encodeN(new Uint8Array([1, 2, 3]), 123 as unknown as string, undefined)).toThrow(/alphabet/);
30
+ expect(() => encodeN(new Uint8Array([1, 2, 3]), alphabetBase58, 'x' as unknown as any)).toThrow(/maxline/);
31
+ });
32
+
33
+ it('handles decode edge cases', () => {
34
+ expect(() => decodeN(123 as unknown as string, alphabetBase58)).toThrow(/input/);
35
+ expect(() => decodeN('abc', 123 as unknown as string)).toThrow(/alphabet/);
36
+ expect(decodeN('', alphabetBase58)).toEqual(new Uint8Array(0));
37
+ expect(decodeN('0', alphabetBase58)).toEqual(new Uint8Array(0));
38
+ expect(decodeN(' 11', alphabetBase58).length).toBeGreaterThan(0);
39
+ });
40
+ });
@@ -0,0 +1,33 @@
1
+ import {
2
+ buildHostedDidDetails,
3
+ createHostedDidWeb,
4
+ getBaseUrlFromDidWeb,
5
+ normalizeDidWeb,
6
+ } from '../src/utils/did.js';
7
+
8
+ describe('did utilities', () => {
9
+ it('normalizes did:web values and role codes', () => {
10
+ const input = 'did:web:Api.Acme.Org:employee:doctor1@acme.org:role:ISCO-08|2211';
11
+ expect(normalizeDidWeb(input)).toBe('did:web:api.acme.org:employee:doctor1@acme.org:role:isco-08|2211');
12
+ });
13
+
14
+ it('creates hosted did:web strings', () => {
15
+ const did = createHostedDidWeb('did:web:host.example.com', 'acme', {
16
+ jurisdiction: 'es',
17
+ version: 'v1',
18
+ sector: 'health-care',
19
+ });
20
+ expect(did).toBe('did:web:host.example.com:acme:cds-es:v1:health-care');
21
+ });
22
+
23
+ it('builds hosted DID details with defaults', () => {
24
+ const details = buildHostedDidDetails({ host: 'example.com', alternateName: 'acme' });
25
+ expect(details.did).toBe('did:web:example.com:acme:cds-ES:v1:health-care');
26
+ expect(details.url).toBe('https://example.com/acme/cds-ES/v1/health-care/');
27
+ });
28
+
29
+ it('derives base URL from did:web', () => {
30
+ const url = getBaseUrlFromDidWeb('did:web:localhost%3A3000:acme:cds-es:v1:health-care');
31
+ expect(url).toBe('http://localhost:3000/acme/cds-ES/v1/health-care/');
32
+ });
33
+ });
@@ -0,0 +1,87 @@
1
+ import {
2
+ convertFhirErrorBundleToJsonApiError,
3
+ convertPrimaryDocToBundleFHIR,
4
+ convertResourceDataToArrayOfDataEntries,
5
+ convertResourceOrBundleToPrimaryDoc,
6
+ } from '../src/utils/format-converter.js';
7
+
8
+ describe('format-converter', () => {
9
+ it('normalizes a FHIR resource into entries', () => {
10
+ const resource = { resourceType: 'Patient', id: '123', name: [{ given: ['A'] }] };
11
+ const entries = convertResourceDataToArrayOfDataEntries(resource, '/fhir', 'https://example.com');
12
+ expect(entries).toEqual([
13
+ {
14
+ fullUrl: 'https://example.com/fhir/123',
15
+ resource,
16
+ },
17
+ ]);
18
+ });
19
+
20
+ it('passes through bundle entries', () => {
21
+ const bundle = { resourceType: 'Bundle', entry: [{ resource: { id: '1' } }] };
22
+ const entries = convertResourceDataToArrayOfDataEntries(bundle, '/fhir', 'https://example.com');
23
+ expect(entries).toEqual(bundle.entry);
24
+ });
25
+
26
+ it('returns input when already JSON:API-like', () => {
27
+ const input = { id: 'abc', type: 'test', attributes: { a: 1 } };
28
+ const entries = convertResourceDataToArrayOfDataEntries(input, '/fhir', 'https://example.com');
29
+ expect(entries).toEqual([input]);
30
+ });
31
+
32
+ it('converts resource or bundle to primary document', () => {
33
+ const resource = { resourceType: 'Patient', id: '123', name: [{ given: ['A'] }] };
34
+ const doc = convertResourceOrBundleToPrimaryDoc(resource, 'spec', 'https://example.com', '/fhir');
35
+ expect(doc).toEqual({
36
+ data: [
37
+ {
38
+ type: 'spec.Patient',
39
+ id: '123',
40
+ attributes: resource,
41
+ },
42
+ ],
43
+ });
44
+ });
45
+
46
+ it('converts primary doc with data and errors to FHIR bundle', () => {
47
+ const primaryDoc = {
48
+ data: [{ id: '1', attributes: { resourceType: 'Patient', id: '1' } }],
49
+ errors: [{ id: 'e1', status: '400', detail: 'Bad' }],
50
+ };
51
+ const bundle = convertPrimaryDocToBundleFHIR(primaryDoc, 'transaction');
52
+ expect(bundle.resourceType).toBe('Bundle');
53
+ expect(bundle.total).toBe(1);
54
+ expect(bundle.type).toBe('transaction');
55
+ expect(bundle.entry.length).toBe(2);
56
+ expect(bundle.entry[0].resource.resourceType).toBe('Patient');
57
+ expect(bundle.entry[1].resource.resourceType).toBe('OperationOutcome');
58
+ });
59
+
60
+ it('converts error bundles to JSON:API errors', () => {
61
+ const errorBundle = {
62
+ entry: [
63
+ {
64
+ resource: {
65
+ id: 'err-1',
66
+ issue: [{ code: 'invalid', severity: 'error', details: { text: 'Bad' }, diagnostics: 'Oops' }],
67
+ },
68
+ response: { status: 400 },
69
+ },
70
+ ],
71
+ };
72
+ const jsonApiError = convertFhirErrorBundleToJsonApiError(errorBundle);
73
+ expect(jsonApiError.errors[0]).toEqual({
74
+ id: 'err-1',
75
+ status: '400',
76
+ code: 'invalid',
77
+ title: 'Bad',
78
+ detail: 'Oops',
79
+ meta: { severity: 'error' },
80
+ });
81
+ });
82
+
83
+ it('returns default error for empty bundles', () => {
84
+ const jsonApiError = convertFhirErrorBundleToJsonApiError({});
85
+ expect(jsonApiError.errors[0].status).toBe('500');
86
+ });
87
+ });
@@ -0,0 +1,57 @@
1
+ import {
2
+ compactJWT,
3
+ decodeHeader,
4
+ decodePayload,
5
+ encodeHeader,
6
+ encodePayload,
7
+ encodeSignature,
8
+ getDataJWT,
9
+ getPartsJWT,
10
+ } from '../src/utils/jwt.js';
11
+
12
+ const signatureBytes = new Uint8Array([1, 2, 3]);
13
+
14
+ describe('jwt utilities', () => {
15
+ it('splits compact JWTs into parts', () => {
16
+ expect(getPartsJWT(undefined)).toBeUndefined();
17
+ expect(getPartsJWT('a.b')).toBeUndefined();
18
+ expect(getPartsJWT('a.b.c')).toEqual({ protected: 'a', payload: 'b', signature: 'c' });
19
+ });
20
+
21
+ it('encodes and decodes headers', () => {
22
+ const header = { alg: 'none' };
23
+ const encoded = encodeHeader(header);
24
+ expect(decodeHeader(encoded)).toEqual(header);
25
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
26
+ expect(decodeHeader('not-base64')).toEqual({});
27
+ spy.mockRestore();
28
+ });
29
+
30
+ it('encodes and decodes payloads (raw and deflated)', async () => {
31
+ const payload = { sub: '123', aud: 'test' };
32
+ const encoded = await encodePayload(payload);
33
+ const decoded = await decodePayload(encoded);
34
+ expect(decoded).toEqual(payload);
35
+
36
+ const encodedDeflated = await encodePayload(payload, true);
37
+ const decodedDeflated = await decodePayload(encodedDeflated, true);
38
+ expect(decodedDeflated).toEqual(payload);
39
+ });
40
+
41
+ it('encodes signatures', () => {
42
+ expect(encodeSignature()).toBe('');
43
+ expect(encodeSignature(new Uint8Array(0))).toBe('');
44
+ expect(encodeSignature(signatureBytes)).not.toBe('');
45
+ });
46
+
47
+ it('round-trips compact JWTs', async () => {
48
+ const header = { alg: 'none' };
49
+ const payload = { sub: '123' };
50
+ const token = await compactJWT(header, payload, signatureBytes);
51
+
52
+ const data = await getDataJWT(token);
53
+ expect(data?.protected).toEqual(header);
54
+ expect(data?.payload).toEqual(payload);
55
+ expect(Array.from(data?.signature || [])).toEqual(Array.from(signatureBytes));
56
+ });
57
+ });
@@ -0,0 +1,11 @@
1
+ import { ManagerError } from '../src/utils/manager-error.js';
2
+ import { IssueType } from '../src/models/issue.js';
3
+
4
+ describe('ManagerError', () => {
5
+ it('sets name, code, and status from issue type', () => {
6
+ const err = new ManagerError('bad input', IssueType.Invalid);
7
+ expect(err.name).toBe('ManagerError');
8
+ expect(err.code).toBe(IssueType.Invalid);
9
+ expect(err.status).toBe('400');
10
+ });
11
+ });
@@ -0,0 +1,15 @@
1
+ import { normalizeDidcommPayloadForId, normalizeObject } from '../src/utils/normalize.js';
2
+
3
+ describe('normalize utilities', () => {
4
+ it('normalizes objects by removing volatile fields and sorting keys', () => {
5
+ const input = { b: 2, id: '123', a: 1, meta: { a: 1 }, contained: [], text: 'ignore' };
6
+ const normalized = normalizeObject(input);
7
+ expect(normalized).toBe('{"a":1,"b":2}');
8
+ });
9
+
10
+ it('normalizes DIDComm payloads by excluding id and meta', () => {
11
+ const payload = { z: 1, id: 'abc', meta: { i: 2 }, a: 2 };
12
+ const normalized = normalizeDidcommPayloadForId(payload);
13
+ expect(normalized).toBe('{"a":2,"z":1}');
14
+ });
15
+ });
@@ -0,0 +1,38 @@
1
+ import {
2
+ arrayCompare,
3
+ arrayMerge,
4
+ base64UrlSafeToJSON,
5
+ objectToBytes,
6
+ objectToRawBase64UrlSafe,
7
+ } from '../src/utils/object-convert.js';
8
+ import { bytesToStringUTF8 } from '../src/utils/string-convert.js';
9
+
10
+ function toArray(bytes: Uint8Array): number[] {
11
+ return Array.from(bytes);
12
+ }
13
+
14
+ describe('object-convert', () => {
15
+ it('compares arrays', () => {
16
+ expect(arrayCompare([1, 2], [1, 2])).toBe(true);
17
+ expect(arrayCompare([1], [1, 2])).toBe(false);
18
+ expect(arrayCompare([1, 2], [1, 3])).toBe(false);
19
+ });
20
+
21
+ it('merges Uint8Arrays', () => {
22
+ const merged = arrayMerge(new Uint8Array([1, 2]), new Uint8Array([3]));
23
+ expect(toArray(merged)).toEqual([1, 2, 3]);
24
+ });
25
+
26
+ it('serializes objects to bytes and base64url', () => {
27
+ const obj = { a: 1, b: true };
28
+ const bytes = objectToBytes(obj);
29
+ expect(bytesToStringUTF8(bytes)).toBe(JSON.stringify(obj));
30
+
31
+ const encoded = objectToRawBase64UrlSafe(obj);
32
+ expect(base64UrlSafeToJSON(encoded)).toEqual(obj);
33
+ });
34
+
35
+ it('throws on undefined base64 input', () => {
36
+ expect(() => base64UrlSafeToJSON(undefined)).toThrow(/undefined/);
37
+ });
38
+ });
@@ -0,0 +1,20 @@
1
+ import { bytesToStringASCII, bytesToStringUTF8, stringToBytesUTF8 } from '../src/utils/string-convert.js';
2
+
3
+ describe('string-convert', () => {
4
+ it('round-trips UTF-8 strings', () => {
5
+ const bytes = stringToBytesUTF8('hola');
6
+ expect(bytesToStringUTF8(bytes)).toBe('hola');
7
+ });
8
+
9
+ it('decodes ASCII/UTF-8 bytes permissively', () => {
10
+ const value = '\\u00f1';
11
+ const bytes = stringToBytesUTF8(value);
12
+ expect(bytesToStringASCII(bytes)).toBe(value);
13
+ });
14
+
15
+ it('decodes 3-byte UTF-8 sequences', () => {
16
+ const value = '\\u20ac';
17
+ const bytes = stringToBytesUTF8(value);
18
+ expect(bytesToStringASCII(bytes)).toBe(value);
19
+ });
20
+ });
@@ -0,0 +1,25 @@
1
+ import { capitalize, sanitizeString, stringToASCII, stringToBytesArrayOfNumbers } from '../src/utils/string-utils.js';
2
+
3
+ describe('string-utils', () => {
4
+ it('capitalizes the first character', () => {
5
+ expect(capitalize('hello')).toBe('Hello');
6
+ });
7
+
8
+ it('sanitizes disallowed characters', () => {
9
+ expect(sanitizeString('Hello!!@#$% World')).toBe('Hello World');
10
+ });
11
+
12
+ it('converts a string to ASCII codes', () => {
13
+ expect(stringToASCII('AZ')).toBe('6590');
14
+ });
15
+
16
+ it('converts a string to byte array numbers', () => {
17
+ const bytes = stringToBytesArrayOfNumbers('A');
18
+ expect(bytes).toEqual([65]);
19
+ });
20
+
21
+ it('handles multi-byte and surrogate pairs', () => {
22
+ expect(stringToBytesArrayOfNumbers('\u00f1')).toEqual([195, 177]);
23
+ expect(stringToBytesArrayOfNumbers('\ud83d\ude00')).toEqual([240, 159, 152, 128]);
24
+ });
25
+ });
@@ -0,0 +1,21 @@
1
+ import { safelyJoinUrl, splitUrl } from '../src/utils/url.js';
2
+
3
+ describe('url utilities', () => {
4
+ it('joins URL parts safely', () => {
5
+ expect(safelyJoinUrl('https://example.com/', '/path')).toBe('https://example.com/path');
6
+ expect(safelyJoinUrl('https://example.com', 'path')).toBe('https://example.com/path');
7
+ });
8
+
9
+ it('splits a valid URL', () => {
10
+ expect(splitUrl('https://www.example.com/some/path?query=1')).toEqual({
11
+ domain: 'www.example.com',
12
+ path: '/some/path',
13
+ });
14
+ });
15
+
16
+ it('returns null for invalid URLs', () => {
17
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
18
+ expect(splitUrl('not-a-url')).toBeNull();
19
+ spy.mockRestore();
20
+ });
21
+ });
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ presets: [
3
+ ['@babel/preset-env', { targets: { node: 'current' } }],
4
+ ],
5
+ };
package/jest.config.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { JestConfigWithTsJest } from 'ts-jest';
2
+
3
+ const config: JestConfigWithTsJest = {
4
+ testEnvironment: 'node',
5
+ clearMocks: true,
6
+ extensionsToTreatAsEsm: ['.ts'],
7
+
8
+ roots: ['<rootDir>/__tests__'],
9
+ testMatch: ['**/*.test.ts'],
10
+
11
+ transform: {
12
+ '^.+\\.tsx?$': [
13
+ 'ts-jest',
14
+ {
15
+ useESM: true,
16
+ tsconfig: {
17
+ module: 'ESNext',
18
+ moduleResolution: 'bundler',
19
+ allowSyntheticDefaultImports: true,
20
+ esModuleInterop: true,
21
+ },
22
+ },
23
+ ],
24
+ '^.+\\.(mjs|js)$': 'babel-jest',
25
+ },
26
+
27
+ moduleNameMapper: {
28
+ '^(\\.{1,2}/.*)\\.js$': '$1',
29
+ },
30
+
31
+ // Allow-list ESM deps in node_modules that must be transformed.
32
+ // Note: we need 4 backslashes in the template literal so the runtime string contains 2.
33
+ transformIgnorePatterns: [
34
+ `[/\\\\]node_modules[/\\\\](?!(@noble|@stablelib|multiformats))`,
35
+ ],
36
+
37
+ collectCoverage: true,
38
+ collectCoverageFrom: [
39
+ 'src/**/*.ts',
40
+ '!src/**/*.txt',
41
+ '!**/*.test.ts',
42
+ '!**/node_modules/**',
43
+ ],
44
+ };
45
+
46
+ export default config;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "gdc-common-utils-ts",
3
+ "version": "1.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "dependencies": {
9
+ "@noble/post-quantum": "^0.8.0",
10
+ "@stablelib/base64": "^1.0.1",
11
+ "@stablelib/utf8": "^1.0.2",
12
+ "base-x": "^4.0.0",
13
+ "multiformats": "^13.0.0",
14
+ "pako": "^2.1.0"
15
+ },
16
+ "scripts": {
17
+ "test": "jest",
18
+ "test:coverage": "jest --coverage",
19
+ "typecheck": "tsc -p tsconfig.json --noEmit",
20
+ "prepublishOnly": "npm run typecheck && npm test -- --watchman=false"
21
+ },
22
+ "exports": {
23
+ ".": "./src/index.ts",
24
+ "./AesManager": "./src/AesManager.ts",
25
+ "./CryptographyService": "./src/CryptographyService.ts",
26
+ "./hmac": "./src/hmac.ts",
27
+ "./constants/*": "./src/constants/*.ts",
28
+ "./models/*": "./src/models/*.ts",
29
+ "./utils/*": "./src/utils/*.ts",
30
+ "./interfaces/*": "./src/interfaces/*.ts",
31
+ "./constants": "./src/constants/index.ts",
32
+ "./models": "./src/models/index.ts",
33
+ "./utils": "./src/utils/index.ts",
34
+ "./interfaces": "./src/interfaces/index.ts"
35
+ }
36
+ }
@@ -0,0 +1,82 @@
1
+ // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ // File: crypto-ts/AesManager.ts
3
+
4
+ import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
5
+ import { Content } from './utils/content';
6
+ import { ProtectedDataAES } from './models/aes';
7
+
8
+ /**
9
+ * Manages AES-GCM encryption and decryption using Node's native crypto module.
10
+ * This class handles the core cryptography and the base64url serialization required for JWE.
11
+ */
12
+ export class AesManager {
13
+ private readonly ALGORITHM = 'aes-256-gcm';
14
+ private readonly KEY_SIZE = 32; // 256-bit key
15
+ private readonly IV_GENERATION_SIZE = 12; // 96-bit IV, recommended by NIST for GCM
16
+ private readonly TAG_SIZE_BYTES = 16; // 128-bit authentication tag
17
+
18
+ /**
19
+ * Encrypts a plaintext string and returns the components as base64url strings.
20
+ * @param plaintext The stringified data to encrypt (e.g. a stringified object or ASCII string from raw bytes)
21
+ * @param cekBytes The 32-byte Content Encryption Key.
22
+ * @param aad The Additional Authenticated Data string for integrity protection.
23
+ * @returns A promise resolving to the JWE-compatible encrypted components.
24
+ */
25
+ async encrypt(plaintext: string, cekBytes: Uint8Array, aad: string): Promise<ProtectedDataAES> {
26
+ if (cekBytes.length !== this.KEY_SIZE) {
27
+ throw new Error(`Invalid key length: a ${this.KEY_SIZE}-byte key is required.`);
28
+ }
29
+
30
+ const iv = randomBytes(this.IV_GENERATION_SIZE);
31
+ const aadBytes = Content.stringToBytesUTF8(aad);
32
+
33
+ const cipher = createCipheriv(this.ALGORITHM, cekBytes, iv, {
34
+ authTagLength: this.TAG_SIZE_BYTES
35
+ });
36
+
37
+ cipher.setAAD(aadBytes);
38
+
39
+ const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
40
+ const tag = cipher.getAuthTag();
41
+
42
+ return {
43
+ ciphertext: Content.bytesToRawBase64UrlSafe(ciphertext),
44
+ iv: Content.bytesToRawBase64UrlSafe(iv),
45
+ tag: Content.bytesToRawBase64UrlSafe(tag),
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Decrypts JWE-compatible encrypted components back to a plaintext string.
51
+ * @param encryptedData The object containing the base64url-encoded ciphertext, iv, and tag.
52
+ * @param cekBytes The 32-byte Content Encryption Key.
53
+ * @param aad The Additional Authenticated Data for integrity verification.
54
+ * @returns A promise resolving to the decrypted plaintext string.
55
+ */
56
+ async decrypt(
57
+ encryptedData: ProtectedDataAES,
58
+ cekBytes: Uint8Array,
59
+ aad: string
60
+ ): Promise<string> {
61
+ if (cekBytes.length !== this.KEY_SIZE) {
62
+ throw new Error(`Invalid key length: a ${this.KEY_SIZE}-byte key is required.`);
63
+ }
64
+
65
+ const ciphertextBytes = Content.base64ToBytes(encryptedData.ciphertext);
66
+ const tagBytes = Content.base64ToBytes(encryptedData.tag);
67
+ const ivBytes = Content.base64ToBytes(encryptedData.iv);
68
+ const aadBytes = Content.stringToBytesUTF8(aad);
69
+
70
+ const decipher = createDecipheriv(this.ALGORITHM, cekBytes, ivBytes, {
71
+ authTagLength: this.TAG_SIZE_BYTES
72
+ });
73
+
74
+ decipher.setAuthTag(tagBytes);
75
+ decipher.setAAD(aadBytes);
76
+
77
+ const decryptedBytes = Buffer.concat([decipher.update(ciphertextBytes), decipher.final()]);
78
+
79
+ return Content.bytesToStringUTF8(decryptedBytes);
80
+ }
81
+ }
82
+