gdc-common-utils-ts 2.3.28 → 2.3.29

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.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalizes the extensible `<service-category>:<membership-profile>` contract
3
+ * used by dataspace onboarding.
4
+ *
5
+ * This intentionally does not embed a product-specific sector allowlist.
6
+ * Governance deployments may supply `allowedScopes`; otherwise syntactically
7
+ * valid future categories remain interoperable.
8
+ */
9
+ export declare function normalizeDataspaceMembershipScope(rawScope: string, options?: {
10
+ defaultProfile?: string;
11
+ allowedScopes?: readonly string[];
12
+ }): string;
13
+ /** Parses and normalizes a comma-separated deployment allowlist. */
14
+ export declare function parseDataspaceMembershipScopeCsv(rawScopes: string | undefined): string[];
@@ -0,0 +1,38 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ const SCOPE_SEGMENT_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
3
+ /**
4
+ * Normalizes the extensible `<service-category>:<membership-profile>` contract
5
+ * used by dataspace onboarding.
6
+ *
7
+ * This intentionally does not embed a product-specific sector allowlist.
8
+ * Governance deployments may supply `allowedScopes`; otherwise syntactically
9
+ * valid future categories remain interoperable.
10
+ */
11
+ export function normalizeDataspaceMembershipScope(rawScope, options = {}) {
12
+ const defaultProfile = (options.defaultProfile || 'provider').trim().toLowerCase();
13
+ let normalized = String(rawScope || '').trim().toLowerCase();
14
+ if (!normalized)
15
+ throw new Error('Dataspace membership scope cannot be empty.');
16
+ if (!normalized.includes(':'))
17
+ normalized = `${normalized}:${defaultProfile}`;
18
+ const parts = normalized.split(':');
19
+ if (parts.length !== 2 || parts.some((part) => !SCOPE_SEGMENT_PATTERN.test(part))) {
20
+ throw new Error(`Invalid dataspace membership scope "${rawScope}". Expected <service-category>:<membership-profile>.`);
21
+ }
22
+ if (options.allowedScopes?.length) {
23
+ const allowed = new Set(options.allowedScopes.map((scope) => scope.trim().toLowerCase()).filter(Boolean));
24
+ if (!allowed.has(normalized)) {
25
+ throw new Error(`Dataspace membership scope "${normalized}" is not allowed by deployment policy.`);
26
+ }
27
+ }
28
+ return normalized;
29
+ }
30
+ /** Parses and normalizes a comma-separated deployment allowlist. */
31
+ export function parseDataspaceMembershipScopeCsv(rawScopes) {
32
+ if (!rawScopes?.trim())
33
+ return [];
34
+ return Array.from(new Set(rawScopes
35
+ .split(',')
36
+ .map((scope) => normalizeDataspaceMembershipScope(scope))
37
+ .filter(Boolean)));
38
+ }
@@ -0,0 +1,51 @@
1
+ export type DeterministicSeedEcAlgorithm = 'ES384' | 'ES256K';
2
+ export type DeterministicSeedEcCurve = 'P-384' | 'secp256k1';
3
+ export type ScryptDerivationProfile = {
4
+ profile: string;
5
+ log2N: number;
6
+ N: number;
7
+ r: number;
8
+ p: number;
9
+ dkLen: number;
10
+ };
11
+ export type DeterministicEcPemKeyMaterial = {
12
+ privateKeyPem: string;
13
+ publicJwk: {
14
+ kty: 'EC';
15
+ crv: DeterministicSeedEcCurve;
16
+ x: string;
17
+ y: string;
18
+ };
19
+ kidRfc7638: string;
20
+ };
21
+ /** Parses the portable `<log2N>:<r>:<p>:<dkLen>` scrypt profile. */
22
+ export declare function parseScryptDerivationProfile(rawProfile: string | undefined, fallbackProfile?: string): ScryptDerivationProfile;
23
+ /** Decodes a seed salt as hexadecimal when unambiguous, otherwise as UTF-8. */
24
+ export declare function parseDeterministicSeedSalt(rawSalt: string | undefined, fallbackSalt: string): {
25
+ salt: Uint8Array;
26
+ raw: string;
27
+ encoding: 'hex' | 'utf8';
28
+ };
29
+ /**
30
+ * Derives the compatibility EC PEM/JWK pair used by ICA seed profiles.
31
+ *
32
+ * The byte-for-byte algorithm is deliberately stable: changing its SHA-512
33
+ * expansion would silently change every key regenerated from an existing
34
+ * private seed.
35
+ */
36
+ export declare function deriveDeterministicEcPemKeyPair(seed: string, curve: DeterministicSeedEcCurve): DeterministicEcPemKeyMaterial;
37
+ /**
38
+ * Applies scrypt and explicit domain separation before deterministic EC key
39
+ * expansion. Existing `profile + salt + separationTag` values are a durable
40
+ * key identity contract and must remain recorded with encrypted seed custody.
41
+ */
42
+ export declare function deriveScryptSeparatedEcPemKeyPair(input: {
43
+ passphrase: string;
44
+ salt: Uint8Array;
45
+ profile: ScryptDerivationProfile;
46
+ alg: DeterministicSeedEcAlgorithm;
47
+ separationTag: string;
48
+ }): DeterministicEcPemKeyMaterial & {
49
+ deterministicSeed: string;
50
+ separatedSeedHex: string;
51
+ };
@@ -0,0 +1,88 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { createECDH, createHash, createPrivateKey, scryptSync } from 'node:crypto';
3
+ import { computeRfc7638JwkThumbprint } from './jwk-thumbprint.js';
4
+ /** Parses the portable `<log2N>:<r>:<p>:<dkLen>` scrypt profile. */
5
+ export function parseScryptDerivationProfile(rawProfile, fallbackProfile = '17:8:1:48') {
6
+ const profile = (rawProfile || fallbackProfile).trim();
7
+ const values = profile.split(':').map((value) => Number.parseInt(value, 10));
8
+ if (values.length !== 4 || values.some((value) => !Number.isFinite(value) || value <= 0)) {
9
+ throw new Error('Invalid scrypt profile. Expected <log2N>:<r>:<p>:<dkLen>, e.g. 17:8:1:48.');
10
+ }
11
+ const [log2N, r, p, dkLen] = values;
12
+ if (log2N < 10 || log2N > 24) {
13
+ throw new Error('scrypt log2N must be between 10 and 24.');
14
+ }
15
+ return { profile, log2N, N: 2 ** log2N, r, p, dkLen };
16
+ }
17
+ /** Decodes a seed salt as hexadecimal when unambiguous, otherwise as UTF-8. */
18
+ export function parseDeterministicSeedSalt(rawSalt, fallbackSalt) {
19
+ const value = (rawSalt || '').trim() || fallbackSalt;
20
+ const isHex = /^[0-9a-fA-F]+$/.test(value) && value.length % 2 === 0;
21
+ return {
22
+ salt: Buffer.from(value, isHex ? 'hex' : 'utf8'),
23
+ raw: isHex ? value.toLowerCase() : value,
24
+ encoding: isHex ? 'hex' : 'utf8',
25
+ };
26
+ }
27
+ /**
28
+ * Derives the compatibility EC PEM/JWK pair used by ICA seed profiles.
29
+ *
30
+ * The byte-for-byte algorithm is deliberately stable: changing its SHA-512
31
+ * expansion would silently change every key regenerated from an existing
32
+ * private seed.
33
+ */
34
+ export function deriveDeterministicEcPemKeyPair(seed, curve) {
35
+ const nodeCurve = curve === 'P-384' ? 'secp384r1' : 'secp256k1';
36
+ const keyLength = curve === 'P-384' ? 48 : 32;
37
+ for (let counter = 0; counter < 256; counter += 1) {
38
+ const material = createHash('sha512').update(`${seed}:${curve}:${counter}`).digest();
39
+ const candidate = material.subarray(0, keyLength);
40
+ try {
41
+ const ecdh = createECDH(nodeCurve);
42
+ ecdh.setPrivateKey(candidate);
43
+ const privateBytes = ecdh.getPrivateKey();
44
+ const publicBytes = ecdh.getPublicKey(undefined, 'uncompressed');
45
+ const x = publicBytes.subarray(1, 1 + keyLength).toString('base64url');
46
+ const y = publicBytes.subarray(1 + keyLength, 1 + (2 * keyLength)).toString('base64url');
47
+ const publicJwk = { kty: 'EC', crv: curve, x, y };
48
+ const privateKey = createPrivateKey({
49
+ key: { ...publicJwk, d: privateBytes.toString('base64url') },
50
+ format: 'jwk',
51
+ });
52
+ return {
53
+ privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
54
+ publicJwk,
55
+ kidRfc7638: computeRfc7638JwkThumbprint(publicJwk),
56
+ };
57
+ }
58
+ catch {
59
+ // Retry the next deterministic candidate until the curve accepts it.
60
+ }
61
+ }
62
+ throw new Error(`Unable to derive deterministic ${curve} key from seed.`);
63
+ }
64
+ /**
65
+ * Applies scrypt and explicit domain separation before deterministic EC key
66
+ * expansion. Existing `profile + salt + separationTag` values are a durable
67
+ * key identity contract and must remain recorded with encrypted seed custody.
68
+ */
69
+ export function deriveScryptSeparatedEcPemKeyPair(input) {
70
+ const derivedSeed = scryptSync(input.passphrase, input.salt, input.profile.dkLen, {
71
+ N: input.profile.N,
72
+ r: input.profile.r,
73
+ p: input.profile.p,
74
+ maxmem: 128 * input.profile.N * input.profile.r * 2,
75
+ });
76
+ const separatedSeedHex = createHash('sha256')
77
+ .update(derivedSeed)
78
+ .update(Buffer.from('|'))
79
+ .update(Buffer.from(input.separationTag, 'utf8'))
80
+ .digest('hex');
81
+ const deterministicSeed = `scrypt:${input.profile.profile}:${separatedSeedHex}`;
82
+ const curve = input.alg === 'ES384' ? 'P-384' : 'secp256k1';
83
+ return {
84
+ ...deriveDeterministicEcPemKeyPair(deterministicSeed, curve),
85
+ deterministicSeed,
86
+ separatedSeedHex,
87
+ };
88
+ }
@@ -21,6 +21,8 @@ export * from './dataspace-discovery';
21
21
  export * from './dataspace-discovery-defaults';
22
22
  export * from './dataspace-protocol';
23
23
  export * from './deterministic-jwk';
24
+ export * from './deterministic-seed-key';
25
+ export * from './dataspace-membership-scope';
24
26
  export * from './employee';
25
27
  export * from './evidence-blockchain-references';
26
28
  export * from './didcomm';
@@ -21,6 +21,8 @@ export * from './dataspace-discovery.js';
21
21
  export * from './dataspace-discovery-defaults.js';
22
22
  export * from './dataspace-protocol.js';
23
23
  export * from './deterministic-jwk.js';
24
+ export * from './deterministic-seed-key.js';
25
+ export * from './dataspace-membership-scope.js';
24
26
  export * from './employee.js';
25
27
  export * from './evidence-blockchain-references.js';
26
28
  export * from './didcomm.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.3.28",
3
+ "version": "2.3.29",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },