gdc-common-utils-ts 2.3.27 → 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.
- package/dist/claims/claims-helpers-related-person.js +0 -1
- package/dist/examples/communication-didcomm-payload.d.ts +2 -2
- package/dist/examples/related-person.d.ts +9 -9
- package/dist/models/consent-rule.d.ts +1 -1
- package/dist/models/consent-rule.js +1 -1
- package/dist/models/indexing.d.ts +1 -1
- package/dist/models/interoperable-claims/appointment-claims.d.ts +3 -3
- package/dist/models/interoperable-claims/appointment-claims.js +3 -3
- package/dist/models/interoperable-claims/clinical-impression-claims.d.ts +1 -1
- package/dist/models/interoperable-claims/clinical-impression-claims.js +1 -1
- package/dist/models/interoperable-claims/document-reference-claims.d.ts +1 -1
- package/dist/models/interoperable-claims/document-reference-claims.js +1 -1
- package/dist/models/interoperable-claims/related-person-claims.d.ts +3 -2
- package/dist/models/interoperable-claims/related-person-claims.js +3 -2
- package/dist/utils/dataspace-membership-scope.d.ts +14 -0
- package/dist/utils/dataspace-membership-scope.js +38 -0
- package/dist/utils/deterministic-seed-key.d.ts +51 -0
- package/dist/utils/deterministic-seed-key.js +88 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/package.json +1 -1
|
@@ -34,6 +34,6 @@ export declare const EXAMPLE_APPOINTMENT_CLAIMS: {
|
|
|
34
34
|
readonly "Appointment.description": "Medication follow-up visit.";
|
|
35
35
|
readonly "Appointment.start": "2026-07-01T09:00:00Z";
|
|
36
36
|
readonly "Appointment.end": "2026-07-01T09:30:00Z";
|
|
37
|
-
readonly "Appointment.
|
|
38
|
-
readonly "Appointment.
|
|
37
|
+
readonly "Appointment.actor": `${string},${string}`;
|
|
38
|
+
readonly "Appointment.part-status": AppointmentParticipantStatus.Accepted;
|
|
39
39
|
};
|
|
@@ -28,7 +28,7 @@ export declare const EXAMPLE_INDIVIDUAL_MEMBER_IDENTITY: Readonly<{
|
|
|
28
28
|
export declare const EXAMPLE_RELATED_PERSON_DISABLE_INPUT: {
|
|
29
29
|
readonly memberClaims: {
|
|
30
30
|
readonly '@context': "org.hl7.fhir.api";
|
|
31
|
-
readonly "RelatedPerson.identifier
|
|
31
|
+
readonly "RelatedPerson.identifier": "rel-001";
|
|
32
32
|
readonly "RelatedPerson.patient": string;
|
|
33
33
|
readonly "RelatedPerson.telecom": "mailto:parent.guardian@example.org";
|
|
34
34
|
readonly "RelatedPerson.relationship": "v3-RoleCode|RESPRSN";
|
|
@@ -64,7 +64,7 @@ export declare const EXAMPLE_RELATED_PERSON_DISABLE_BUNDLE_ENTRY: {
|
|
|
64
64
|
readonly meta: {
|
|
65
65
|
readonly claims: {
|
|
66
66
|
readonly '@context': "org.hl7.fhir.api";
|
|
67
|
-
readonly "RelatedPerson.identifier
|
|
67
|
+
readonly "RelatedPerson.identifier": "rel-001";
|
|
68
68
|
readonly "RelatedPerson.patient": string;
|
|
69
69
|
readonly "RelatedPerson.telecom": "mailto:parent.guardian@example.org";
|
|
70
70
|
readonly "RelatedPerson.relationship": "v3-RoleCode|RESPRSN";
|
|
@@ -98,7 +98,7 @@ export declare const EXAMPLE_RELATED_PERSON_DISABLE_BUNDLE_PAYLOAD: {
|
|
|
98
98
|
readonly meta: {
|
|
99
99
|
readonly claims: {
|
|
100
100
|
readonly '@context': "org.hl7.fhir.api";
|
|
101
|
-
readonly "RelatedPerson.identifier
|
|
101
|
+
readonly "RelatedPerson.identifier": "rel-001";
|
|
102
102
|
readonly "RelatedPerson.patient": string;
|
|
103
103
|
readonly "RelatedPerson.telecom": "mailto:parent.guardian@example.org";
|
|
104
104
|
readonly "RelatedPerson.relationship": "v3-RoleCode|RESPRSN";
|
|
@@ -139,7 +139,7 @@ export declare const EXAMPLE_RELATED_PERSON_PURGE_BUNDLE_ENTRY: {
|
|
|
139
139
|
readonly meta: {
|
|
140
140
|
readonly claims: {
|
|
141
141
|
readonly '@context': "org.hl7.fhir.api";
|
|
142
|
-
readonly "RelatedPerson.identifier
|
|
142
|
+
readonly "RelatedPerson.identifier": "rel-001";
|
|
143
143
|
readonly "RelatedPerson.patient": string;
|
|
144
144
|
readonly "RelatedPerson.telecom": "mailto:parent.guardian@example.org";
|
|
145
145
|
readonly "RelatedPerson.relationship": "v3-RoleCode|RESPRSN";
|
|
@@ -171,7 +171,7 @@ export declare const EXAMPLE_RELATED_PERSON_PURGE_BUNDLE_PAYLOAD: {
|
|
|
171
171
|
readonly meta: {
|
|
172
172
|
readonly claims: {
|
|
173
173
|
readonly '@context': "org.hl7.fhir.api";
|
|
174
|
-
readonly "RelatedPerson.identifier
|
|
174
|
+
readonly "RelatedPerson.identifier": "rel-001";
|
|
175
175
|
readonly "RelatedPerson.patient": string;
|
|
176
176
|
readonly "RelatedPerson.telecom": "mailto:parent.guardian@example.org";
|
|
177
177
|
readonly "RelatedPerson.relationship": "v3-RoleCode|RESPRSN";
|
|
@@ -292,7 +292,7 @@ export declare const EXAMPLE_RELATED_PERSON_LIST_RECORD_ACTIVE: {
|
|
|
292
292
|
readonly meta: {
|
|
293
293
|
readonly claims: {
|
|
294
294
|
readonly '@context': "org.hl7.fhir.api";
|
|
295
|
-
readonly "RelatedPerson.identifier
|
|
295
|
+
readonly "RelatedPerson.identifier": "rel-001";
|
|
296
296
|
readonly "RelatedPerson.patient": string;
|
|
297
297
|
readonly "RelatedPerson.telecom": "mailto:parent.guardian@example.org";
|
|
298
298
|
readonly "RelatedPerson.relationship": "v3-RoleCode|RESPRSN";
|
|
@@ -313,7 +313,7 @@ export declare const EXAMPLE_RELATED_PERSON_LIST_RECORD_INACTIVE: {
|
|
|
313
313
|
readonly meta: {
|
|
314
314
|
readonly claims: {
|
|
315
315
|
readonly '@context': "org.hl7.fhir.api";
|
|
316
|
-
readonly "RelatedPerson.identifier
|
|
316
|
+
readonly "RelatedPerson.identifier": "urn:uuid:related-person-002";
|
|
317
317
|
readonly "RelatedPerson.patient": string;
|
|
318
318
|
readonly "RelatedPerson.telecom": "mailto:caregiver.two@example.org";
|
|
319
319
|
readonly "RelatedPerson.relationship": "http://terminology.hl7.org/CodeSystem/v3-RoleCode|NMTH";
|
|
@@ -336,7 +336,7 @@ export declare const EXAMPLE_RELATED_PERSON_LIST_RESPONSE_BODY: {
|
|
|
336
336
|
readonly meta: {
|
|
337
337
|
readonly claims: {
|
|
338
338
|
readonly '@context': "org.hl7.fhir.api";
|
|
339
|
-
readonly "RelatedPerson.identifier
|
|
339
|
+
readonly "RelatedPerson.identifier": "rel-001";
|
|
340
340
|
readonly "RelatedPerson.patient": string;
|
|
341
341
|
readonly "RelatedPerson.telecom": "mailto:parent.guardian@example.org";
|
|
342
342
|
readonly "RelatedPerson.relationship": "v3-RoleCode|RESPRSN";
|
|
@@ -356,7 +356,7 @@ export declare const EXAMPLE_RELATED_PERSON_LIST_RESPONSE_BODY: {
|
|
|
356
356
|
readonly meta: {
|
|
357
357
|
readonly claims: {
|
|
358
358
|
readonly '@context': "org.hl7.fhir.api";
|
|
359
|
-
readonly "RelatedPerson.identifier
|
|
359
|
+
readonly "RelatedPerson.identifier": "urn:uuid:related-person-002";
|
|
360
360
|
readonly "RelatedPerson.patient": string;
|
|
361
361
|
readonly "RelatedPerson.telecom": "mailto:caregiver.two@example.org";
|
|
362
362
|
readonly "RelatedPerson.relationship": "http://terminology.hl7.org/CodeSystem/v3-RoleCode|NMTH";
|
|
@@ -5,7 +5,7 @@ export declare enum ClaimConsent {
|
|
|
5
5
|
'category' = "Consent.category",
|
|
6
6
|
'categoryText' = "Consent.category-text",
|
|
7
7
|
'categoryDisplay' = "Consent.category-display",
|
|
8
|
-
'resourceType' = "Consent.
|
|
8
|
+
'resourceType' = "Consent.resource-type",
|
|
9
9
|
'subject' = "Consent.subject",
|
|
10
10
|
'patient' = "Consent.patient",
|
|
11
11
|
'actorIdentifier' = "Consent.actor-identifier",
|
|
@@ -7,7 +7,7 @@ export var ClaimConsent;
|
|
|
7
7
|
ClaimConsent["category"] = "Consent.category";
|
|
8
8
|
ClaimConsent["categoryText"] = "Consent.category-text";
|
|
9
9
|
ClaimConsent["categoryDisplay"] = "Consent.category-display";
|
|
10
|
-
ClaimConsent["resourceType"] = "Consent.
|
|
10
|
+
ClaimConsent["resourceType"] = "Consent.resource-type";
|
|
11
11
|
ClaimConsent["subject"] = "Consent.subject";
|
|
12
12
|
ClaimConsent["patient"] = "Consent.patient";
|
|
13
13
|
ClaimConsent["actorIdentifier"] = "Consent.actor-identifier";
|
|
@@ -45,7 +45,7 @@ export declare const AllowedIndexableClaims: {
|
|
|
45
45
|
readonly General: readonly ["MedicationStatement.identifier", "MedicationStatement.subject", "MedicationStatement.patient", "MedicationStatement.status", "MedicationStatement.category", "MedicationStatement.effective", "MedicationStatement.code", "MedicationStatement.medication", "MedicationStatement.part-of", "MedicationStatement.source", "MedicationStatement.medication-identifier", "MedicationStatement.medication-serial-number", "MedicationStatement.medication-expiration-date"];
|
|
46
46
|
};
|
|
47
47
|
readonly DocumentReference: {
|
|
48
|
-
readonly General: readonly ["DocumentReference.identifier", "DocumentReference.subject", "DocumentReference.author", "DocumentReference.attester", "DocumentReference.
|
|
48
|
+
readonly General: readonly ["DocumentReference.identifier", "DocumentReference.subject", "DocumentReference.author", "DocumentReference.attester", "DocumentReference.based-on", "DocumentReference.category", "DocumentReference.contenthash", "DocumentReference.contenttype", "DocumentReference.context", "DocumentReference.creation", "DocumentReference.date", "DocumentReference.event-code", "DocumentReference.event-reference", "DocumentReference.format-uri", "DocumentReference.language", "DocumentReference.location", "DocumentReference.modality", "DocumentReference.relatesto", "DocumentReference.relation", "DocumentReference.type"];
|
|
49
49
|
};
|
|
50
50
|
};
|
|
51
51
|
export type AllowedIndexableClaimsKey = keyof typeof AllowedIndexableClaims;
|
|
@@ -181,9 +181,9 @@ export declare const AppointmentClaim: {
|
|
|
181
181
|
readonly NoteText: "Appointment.note-text";
|
|
182
182
|
readonly PatientInstruction: "Appointment.patient-instruction";
|
|
183
183
|
readonly BasedOn: "Appointment.based-on";
|
|
184
|
-
readonly ParticipantActor: "Appointment.
|
|
185
|
-
readonly ParticipantStatus: "Appointment.
|
|
186
|
-
readonly ParticipantType: "Appointment.participant
|
|
184
|
+
readonly ParticipantActor: "Appointment.actor";
|
|
185
|
+
readonly ParticipantStatus: "Appointment.part-status";
|
|
186
|
+
readonly ParticipantType: "Appointment.participant-type";
|
|
187
187
|
};
|
|
188
188
|
export type AppointmentClaimKey = typeof AppointmentClaim[keyof typeof AppointmentClaim];
|
|
189
189
|
/**
|
|
@@ -170,9 +170,9 @@ export const AppointmentClaim = {
|
|
|
170
170
|
NoteText: 'Appointment.note-text',
|
|
171
171
|
PatientInstruction: 'Appointment.patient-instruction',
|
|
172
172
|
BasedOn: 'Appointment.based-on',
|
|
173
|
-
ParticipantActor: 'Appointment.
|
|
174
|
-
ParticipantStatus: 'Appointment.
|
|
175
|
-
ParticipantType: 'Appointment.participant
|
|
173
|
+
ParticipantActor: 'Appointment.actor',
|
|
174
|
+
ParticipantStatus: 'Appointment.part-status',
|
|
175
|
+
ParticipantType: 'Appointment.participant-type',
|
|
176
176
|
};
|
|
177
177
|
// src/models/fhir/Appointment.values.ts
|
|
178
178
|
/**
|
|
@@ -5,7 +5,7 @@ export declare const ClinicalImpressionClaim: {
|
|
|
5
5
|
readonly Description: "ClinicalImpression.description";
|
|
6
6
|
readonly Subject: "ClinicalImpression.subject";
|
|
7
7
|
readonly Encounter: "ClinicalImpression.encounter";
|
|
8
|
-
readonly EffectiveDateTime: "ClinicalImpression.
|
|
8
|
+
readonly EffectiveDateTime: "ClinicalImpression.effective-datetime";
|
|
9
9
|
readonly Date: "ClinicalImpression.date";
|
|
10
10
|
readonly Assessor: "ClinicalImpression.assessor";
|
|
11
11
|
readonly Problem: "ClinicalImpression.problem";
|
|
@@ -6,7 +6,7 @@ export const ClinicalImpressionClaim = {
|
|
|
6
6
|
Description: 'ClinicalImpression.description',
|
|
7
7
|
Subject: 'ClinicalImpression.subject',
|
|
8
8
|
Encounter: 'ClinicalImpression.encounter',
|
|
9
|
-
EffectiveDateTime: 'ClinicalImpression.
|
|
9
|
+
EffectiveDateTime: 'ClinicalImpression.effective-datetime',
|
|
10
10
|
Date: 'ClinicalImpression.date',
|
|
11
11
|
Assessor: 'ClinicalImpression.assessor',
|
|
12
12
|
Problem: 'ClinicalImpression.problem',
|
|
@@ -2,7 +2,7 @@ import type { ClaimSpec } from './types';
|
|
|
2
2
|
export declare const DocumentReferenceClaim: {
|
|
3
3
|
readonly Attester: "DocumentReference.attester";
|
|
4
4
|
readonly Author: "DocumentReference.author";
|
|
5
|
-
readonly BasedOn: "DocumentReference.
|
|
5
|
+
readonly BasedOn: "DocumentReference.based-on";
|
|
6
6
|
readonly Category: "DocumentReference.category";
|
|
7
7
|
readonly ContentData: "DocumentReference.contentdata";
|
|
8
8
|
readonly ContentHash: "DocumentReference.contenthash";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export const DocumentReferenceClaim = {
|
|
5
5
|
Attester: 'DocumentReference.attester',
|
|
6
6
|
Author: 'DocumentReference.author',
|
|
7
|
-
BasedOn: 'DocumentReference.
|
|
7
|
+
BasedOn: 'DocumentReference.based-on',
|
|
8
8
|
Category: 'DocumentReference.category',
|
|
9
9
|
ContentData: 'DocumentReference.contentdata',
|
|
10
10
|
ContentHash: 'DocumentReference.contenthash',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ClaimSpec } from './types';
|
|
2
2
|
export declare const RelatedPersonClaim: {
|
|
3
|
-
/**
|
|
3
|
+
/** Canonical FHIR API identifier search parameter. */
|
|
4
4
|
readonly Identifier: "RelatedPerson.identifier";
|
|
5
|
-
|
|
5
|
+
/** @deprecated Use `Identifier`; retained as a source-compatible property alias. */
|
|
6
|
+
readonly IdentifierValue: "RelatedPerson.identifier";
|
|
6
7
|
readonly Active: "RelatedPerson.active";
|
|
7
8
|
readonly Patient: "RelatedPerson.patient";
|
|
8
9
|
readonly Relationship: "RelatedPerson.relationship";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Copyright 2026 Conéctate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
|
|
2
2
|
// File: src/models/interoperable-claims/related-person-claims.ts
|
|
3
3
|
export const RelatedPersonClaim = {
|
|
4
|
-
/**
|
|
4
|
+
/** Canonical FHIR API identifier search parameter. */
|
|
5
5
|
Identifier: 'RelatedPerson.identifier',
|
|
6
|
-
|
|
6
|
+
/** @deprecated Use `Identifier`; retained as a source-compatible property alias. */
|
|
7
|
+
IdentifierValue: 'RelatedPerson.identifier',
|
|
7
8
|
Active: 'RelatedPerson.active',
|
|
8
9
|
Patient: 'RelatedPerson.patient',
|
|
9
10
|
Relationship: 'RelatedPerson.relationship',
|
|
@@ -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
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/utils/index.js
CHANGED
|
@@ -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';
|