gdc-common-utils-ts 2.3.3 → 2.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants/actor-session.d.ts +1 -0
- package/dist/constants/actor-session.js +7 -0
- package/dist/constants/verifiable-credentials.d.ts +8 -0
- package/dist/constants/verifiable-credentials.js +8 -0
- package/dist/examples/index.d.ts +1 -0
- package/dist/examples/index.js +1 -0
- package/dist/examples/professional.js +1 -112
- package/dist/examples/shared.d.ts +2 -0
- package/dist/examples/shared.js +2 -0
- package/dist/examples/subject-identity-binding.d.ts +9 -0
- package/dist/examples/subject-identity-binding.js +20 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/indexing.js +1 -1
- package/dist/models/subject-identity-binding.d.ts +22 -0
- package/dist/models/subject-identity-binding.js +7 -0
- package/dist/utils/bundle-entry-editor.js +2 -2
- package/dist/utils/bundle-query.d.ts +17 -0
- package/dist/utils/bundle-query.js +9 -2
- package/dist/utils/bundle-reader.d.ts +16 -0
- package/dist/utils/bundle-reader.js +49 -1
- package/dist/utils/client-assertion.d.ts +2 -2
- package/dist/utils/clinical-resource-filter-editor.d.ts +39 -0
- package/dist/utils/clinical-resource-filter-editor.js +84 -0
- package/dist/utils/communication-bundle-document-request.d.ts +9 -8
- package/dist/utils/communication-bundle-document-request.js +14 -9
- package/dist/utils/communication-fhir-r4.js +24 -17
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/legal-organization-onboarding-editor.d.ts +13 -0
- package/dist/utils/legal-organization-onboarding-editor.js +14 -3
- package/dist/utils/subject-identity-binding.d.ts +28 -0
- package/dist/utils/subject-identity-binding.js +136 -0
- package/package.json +1 -1
|
@@ -38,6 +38,7 @@ export declare const ActorCapabilities: Readonly<{
|
|
|
38
38
|
readonly IndividualImportIps: "individual.import_ips";
|
|
39
39
|
readonly IndividualGenerateDigitalTwin: "individual.generate_digital_twin";
|
|
40
40
|
readonly IndividualIngestCommunication: "individual.ingest_communication";
|
|
41
|
+
readonly IndividualReadClinicalSummary: "individual.read_clinical_summary";
|
|
41
42
|
readonly IndividualUpsertRelatedPerson: "individual.upsert_related_person";
|
|
42
43
|
readonly IndividualMemberDisable: "individual_member.disable";
|
|
43
44
|
readonly IndividualMemberPurge: "individual_member.purge";
|
|
@@ -40,6 +40,7 @@ export const ActorCapabilities = Object.freeze({
|
|
|
40
40
|
IndividualImportIps: 'individual.import_ips',
|
|
41
41
|
IndividualGenerateDigitalTwin: 'individual.generate_digital_twin',
|
|
42
42
|
IndividualIngestCommunication: 'individual.ingest_communication',
|
|
43
|
+
IndividualReadClinicalSummary: 'individual.read_clinical_summary',
|
|
43
44
|
IndividualUpsertRelatedPerson: 'individual.upsert_related_person',
|
|
44
45
|
IndividualMemberDisable: 'individual_member.disable',
|
|
45
46
|
IndividualMemberPurge: 'individual_member.purge',
|
|
@@ -161,6 +162,12 @@ export const ActorCapabilityDocs = Object.freeze({
|
|
|
161
162
|
programmingHint: 'Choose the route family carefully (`api`, `didcomm-plain`, `legacy-fhir`) to match the runtime transport profile.',
|
|
162
163
|
relatedMethods: ['ingestCommunicationAndUpdateIndex'],
|
|
163
164
|
},
|
|
165
|
+
[ActorCapabilities.IndividualReadClinicalSummary]: {
|
|
166
|
+
actorKind: ActorKinds.IndividualController,
|
|
167
|
+
summary: 'Reads the clinical summary currently available for one subject through an auditable Communication.',
|
|
168
|
+
programmingHint: 'Use requestClinicalSummary with Subject/$summary and attached FHIR Parameters. Do not route reads through ingestion methods.',
|
|
169
|
+
relatedMethods: ['requestClinicalSummary'],
|
|
170
|
+
},
|
|
164
171
|
[ActorCapabilities.IndividualUpsertRelatedPerson]: {
|
|
165
172
|
actorKind: ActorKinds.IndividualController,
|
|
166
173
|
summary: 'Creates or updates one related-person/member relationship for the individual scope.',
|
|
@@ -44,6 +44,14 @@ export declare const IndividualCredentialTypes: Readonly<{
|
|
|
44
44
|
IndividualControllerCredential: "IndividualControllerCredential";
|
|
45
45
|
IndividualMemberCredential: "IndividualMemberCredential";
|
|
46
46
|
IndividualSubjectCredential: "IndividualSubjectCredential";
|
|
47
|
+
/**
|
|
48
|
+
* Credential issued by a trusted portal/identity authority to bind two or
|
|
49
|
+
* more DIDs that identify the same individual.
|
|
50
|
+
*
|
|
51
|
+
* A physical card/support DID is not an individual alias: clients resolve
|
|
52
|
+
* the support document's `subject` first and bind that subject DID instead.
|
|
53
|
+
*/
|
|
54
|
+
SubjectIdentityBindingCredential: "SubjectIdentityBindingCredential";
|
|
47
55
|
}>;
|
|
48
56
|
/**
|
|
49
57
|
* Canonical credential subtype names used by inter-tenant authorization
|
|
@@ -46,6 +46,14 @@ export const IndividualCredentialTypes = Object.freeze({
|
|
|
46
46
|
IndividualControllerCredential: 'IndividualControllerCredential',
|
|
47
47
|
IndividualMemberCredential: 'IndividualMemberCredential',
|
|
48
48
|
IndividualSubjectCredential: 'IndividualSubjectCredential',
|
|
49
|
+
/**
|
|
50
|
+
* Credential issued by a trusted portal/identity authority to bind two or
|
|
51
|
+
* more DIDs that identify the same individual.
|
|
52
|
+
*
|
|
53
|
+
* A physical card/support DID is not an individual alias: clients resolve
|
|
54
|
+
* the support document's `subject` first and bind that subject DID instead.
|
|
55
|
+
*/
|
|
56
|
+
SubjectIdentityBindingCredential: 'SubjectIdentityBindingCredential',
|
|
49
57
|
});
|
|
50
58
|
/**
|
|
51
59
|
* Canonical credential subtype names used by inter-tenant authorization
|
package/dist/examples/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from './employee';
|
|
|
11
11
|
export * from './license';
|
|
12
12
|
export * from './invoice';
|
|
13
13
|
export * from './inter-tenant-access-contract';
|
|
14
|
+
export * from './subject-identity-binding';
|
|
14
15
|
export * from './related-person';
|
|
15
16
|
export * from './consent-access';
|
|
16
17
|
export * from './relationship-access';
|
package/dist/examples/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export * from './employee.js';
|
|
|
11
11
|
export * from './license.js';
|
|
12
12
|
export * from './invoice.js';
|
|
13
13
|
export * from './inter-tenant-access-contract.js';
|
|
14
|
+
export * from './subject-identity-binding.js';
|
|
14
15
|
export * from './related-person.js';
|
|
15
16
|
export * from './consent-access.js';
|
|
16
17
|
export * from './relationship-access.js';
|
|
@@ -210,115 +210,4 @@ function buildConsentDecisionScenario(params) {
|
|
|
210
210
|
* consent state + actor target + role + purpose + requested scope.
|
|
211
211
|
*/
|
|
212
212
|
export const EXAMPLE_PROFESSIONAL_CONSENT_SCENARIOS = Object.freeze({
|
|
213
|
-
|
|
214
|
-
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
215
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
216
|
-
purpose: HealthcareConsentPurposes.Treatment,
|
|
217
|
-
consentActions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
|
|
218
|
-
requestedSections: HealthcareBasicSections.AllergiesAndIntolerances.claim,
|
|
219
|
-
includedTypes: [
|
|
220
|
-
ResourceTypesFhirR4.Composition,
|
|
221
|
-
ResourceTypesFhirR4.AllergyIntolerance,
|
|
222
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
223
|
-
],
|
|
224
|
-
expectedSmartTokenDecision: 'allowed',
|
|
225
|
-
reason: 'physician is targeted directly by email and role for continuous care over allergies section',
|
|
226
|
-
}),
|
|
227
|
-
physicianByEmailEmergencySummaryAllowed: buildConsentDecisionScenario({
|
|
228
|
-
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
229
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
230
|
-
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
231
|
-
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
232
|
-
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
233
|
-
includedTypes: [
|
|
234
|
-
ResourceTypesFhirR4.Composition,
|
|
235
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
236
|
-
ResourceTypesFhirR4.Observation,
|
|
237
|
-
],
|
|
238
|
-
expectedSmartTokenDecision: 'allowed',
|
|
239
|
-
reason: 'physician is targeted directly by email and role for emergency summary access',
|
|
240
|
-
}),
|
|
241
|
-
physicianByOrganizationResultsAllowed: buildConsentDecisionScenario({
|
|
242
|
-
actorId: { organizationUrl: EXAMPLE_PROVIDER_ORG_URL },
|
|
243
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
244
|
-
purpose: HealthcareConsentPurposes.Treatment,
|
|
245
|
-
consentActions: [HealthcareBasicSections.Results.claim],
|
|
246
|
-
requestedSections: HealthcareBasicSections.Results.claim,
|
|
247
|
-
includedTypes: [
|
|
248
|
-
ResourceTypesFhirR4.Composition,
|
|
249
|
-
ResourceTypesFhirR4.DiagnosticReport,
|
|
250
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
251
|
-
],
|
|
252
|
-
expectedSmartTokenDecision: 'allowed',
|
|
253
|
-
reason: 'consent is granted to a physician role within a given organization for continuous care results access',
|
|
254
|
-
}),
|
|
255
|
-
physicianByJurisdictionEmergencySummaryAllowed: buildConsentDecisionScenario({
|
|
256
|
-
actorId: EXAMPLE_JURISDICTION,
|
|
257
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
258
|
-
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
259
|
-
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
260
|
-
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
261
|
-
includedTypes: [
|
|
262
|
-
ResourceTypesFhirR4.Composition,
|
|
263
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
264
|
-
ResourceTypesFhirR4.Observation,
|
|
265
|
-
],
|
|
266
|
-
expectedSmartTokenDecision: 'allowed',
|
|
267
|
-
reason: 'consent is granted at jurisdiction level for physician emergency access',
|
|
268
|
-
}),
|
|
269
|
-
nursingByOrganizationMedicationHistoryAllowed: buildConsentDecisionScenario({
|
|
270
|
-
actorId: { organizationUrl: EXAMPLE_PROVIDER_ORG_URL },
|
|
271
|
-
actorRole: HealthcareActorRoles.NursingProfessional,
|
|
272
|
-
purpose: HealthcareConsentPurposes.Treatment,
|
|
273
|
-
consentActions: [HealthcareBasicSections.HistoryOfMedicationUse.claim],
|
|
274
|
-
requestedSections: HealthcareBasicSections.HistoryOfMedicationUse.claim,
|
|
275
|
-
includedTypes: [
|
|
276
|
-
ResourceTypesFhirR4.Composition,
|
|
277
|
-
ResourceTypesFhirR4.MedicationStatement,
|
|
278
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
279
|
-
],
|
|
280
|
-
expectedSmartTokenDecision: 'allowed',
|
|
281
|
-
reason: 'nursing professional is allowed to read medication history for treatment through organization-scoped consent',
|
|
282
|
-
}),
|
|
283
|
-
paramedicByJurisdictionEmergencySummaryAllowed: buildConsentDecisionScenario({
|
|
284
|
-
actorId: EXAMPLE_JURISDICTION,
|
|
285
|
-
actorRole: HealthcareActorRoles.Paramedic,
|
|
286
|
-
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
287
|
-
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
288
|
-
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
289
|
-
includedTypes: [
|
|
290
|
-
ResourceTypesFhirR4.Composition,
|
|
291
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
292
|
-
ResourceTypesFhirR4.Observation,
|
|
293
|
-
],
|
|
294
|
-
expectedSmartTokenDecision: 'allowed',
|
|
295
|
-
reason: 'paramedic receives emergency-only jurisdiction-scoped access to patient summary',
|
|
296
|
-
}),
|
|
297
|
-
physicianObstetricianDeniedWhenOnlyAllergiesConsent: buildConsentDecisionScenario({
|
|
298
|
-
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
299
|
-
actorRole: `${HealthcareActorRoles.Physician}:obstetrician`,
|
|
300
|
-
purpose: HealthcareConsentPurposes.Treatment,
|
|
301
|
-
consentActions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
|
|
302
|
-
requestedSections: HealthcareBasicSections.Results.claim,
|
|
303
|
-
includedTypes: [
|
|
304
|
-
ResourceTypesFhirR4.Composition,
|
|
305
|
-
ResourceTypesFhirR4.DiagnosticReport,
|
|
306
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
307
|
-
],
|
|
308
|
-
expectedSmartTokenDecision: 'denied',
|
|
309
|
-
reason: 'requested SMART scope targets results but active consent only covers allergies section',
|
|
310
|
-
}),
|
|
311
|
-
physicianByEmailDeniedWhenConsentRevokedAndNoOrgNorJurisdictionConsentIsActive: buildConsentDecisionScenario({
|
|
312
|
-
actorId: EXAMPLE_PHYSICIAN_EMAIL,
|
|
313
|
-
actorRole: HealthcareActorRoles.Physician,
|
|
314
|
-
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
315
|
-
consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
316
|
-
requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
|
|
317
|
-
includedTypes: [
|
|
318
|
-
ResourceTypesFhirR4.Composition,
|
|
319
|
-
ResourceTypesFhirR4.DocumentReference,
|
|
320
|
-
],
|
|
321
|
-
expectedSmartTokenDecision: 'denied',
|
|
322
|
-
reason: 'matching consent rule existed before but is no longer active after controller deactivation/revocation',
|
|
323
|
-
}),
|
|
324
|
-
});
|
|
213
|
+
physicianB
|
|
@@ -117,6 +117,8 @@ export declare const EXAMPLE_GATEWAY_PUBLIC_ORIGIN: "https://gateway.example.com
|
|
|
117
117
|
export declare const EXAMPLE_HOST_PUBLIC_HOSTNAME: "host.example.com";
|
|
118
118
|
export declare const EXAMPLE_PROVIDER_TAX_ID: "VATES-B00112233";
|
|
119
119
|
export declare const EXAMPLE_PROVIDER_DOMAIN: "health-care.provider.example.org";
|
|
120
|
+
/** Synthetic public portal domain used to derive organization path DIDs. */
|
|
121
|
+
export declare const EXAMPLE_PUBLIC_PORTAL_DOMAIN: "portal.example.org";
|
|
120
122
|
export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID: string;
|
|
121
123
|
export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_SECONDARY: string;
|
|
122
124
|
export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_TERTIARY: string;
|
package/dist/examples/shared.js
CHANGED
|
@@ -136,6 +136,8 @@ export const EXAMPLE_GATEWAY_PUBLIC_ORIGIN = 'https://gateway.example.com';
|
|
|
136
136
|
export const EXAMPLE_HOST_PUBLIC_HOSTNAME = 'host.example.com';
|
|
137
137
|
export const EXAMPLE_PROVIDER_TAX_ID = 'VATES-B00112233';
|
|
138
138
|
export const EXAMPLE_PROVIDER_DOMAIN = 'health-care.provider.example.org';
|
|
139
|
+
/** Synthetic public portal domain used to derive organization path DIDs. */
|
|
140
|
+
export const EXAMPLE_PUBLIC_PORTAL_DOMAIN = 'portal.example.org';
|
|
139
141
|
export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID = encodeHexToMultibase58btc('a87e5b15aea444759c7c40aa88354b6f');
|
|
140
142
|
export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_SECONDARY = encodeHexToMultibase58btc('b98f6c24bfb545849d8d51bb99465c7e');
|
|
141
143
|
export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_TERTIARY = encodeHexToMultibase58btc('c39f7d35c0c65695ae9e62cca0576d8f');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Synthetic trusted portal DID used by binding examples and tests. */
|
|
2
|
+
export declare const EXAMPLE_TRUSTED_HEALTH_PORTAL_DID: "did:web:portal.example.org";
|
|
3
|
+
/** Synthetic individual DID exposed by one health portal. */
|
|
4
|
+
export declare const EXAMPLE_PORTAL_INDIVIDUAL_DID: "did:web:portal.example.org:health-care:individual:multibase:zSubjectExample";
|
|
5
|
+
/** Synthetic individual DID exposed by a second, independent portal. */
|
|
6
|
+
export declare const EXAMPLE_ALTERNATE_PORTAL_INDIVIDUAL_DID: "did:web:cards.example.org:individual:multibase:zSubjectExample";
|
|
7
|
+
/** Physical support/card DID used only to demonstrate that it is not an auth alias. */
|
|
8
|
+
export declare const EXAMPLE_PHYSICAL_SUPPORT_DID: "did:web:cards.example.org:card:personal:000-000-000-001";
|
|
9
|
+
export declare const EXAMPLE_SUBJECT_IDENTITY_BINDING_CREDENTIAL: Readonly<import("..").VerifiableCredentialV2>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
+
import { DataspaceSectors } from '../constants/sectors.js';
|
|
3
|
+
import { buildSubjectIdentityBindingCredential } from '../utils/subject-identity-binding.js';
|
|
4
|
+
/** Synthetic trusted portal DID used by binding examples and tests. */
|
|
5
|
+
export const EXAMPLE_TRUSTED_HEALTH_PORTAL_DID = 'did:web:portal.example.org';
|
|
6
|
+
/** Synthetic individual DID exposed by one health portal. */
|
|
7
|
+
export const EXAMPLE_PORTAL_INDIVIDUAL_DID = 'did:web:portal.example.org:health-care:individual:multibase:zSubjectExample';
|
|
8
|
+
/** Synthetic individual DID exposed by a second, independent portal. */
|
|
9
|
+
export const EXAMPLE_ALTERNATE_PORTAL_INDIVIDUAL_DID = 'did:web:cards.example.org:individual:multibase:zSubjectExample';
|
|
10
|
+
/** Physical support/card DID used only to demonstrate that it is not an auth alias. */
|
|
11
|
+
export const EXAMPLE_PHYSICAL_SUPPORT_DID = 'did:web:cards.example.org:card:personal:000-000-000-001';
|
|
12
|
+
export const EXAMPLE_SUBJECT_IDENTITY_BINDING_CREDENTIAL = Object.freeze(buildSubjectIdentityBindingCredential({
|
|
13
|
+
id: 'urn:uuid:subject-identity-binding-001',
|
|
14
|
+
issuerDid: EXAMPLE_TRUSTED_HEALTH_PORTAL_DID,
|
|
15
|
+
subjectDid: EXAMPLE_PORTAL_INDIVIDUAL_DID,
|
|
16
|
+
aliasDids: [EXAMPLE_ALTERNATE_PORTAL_INDIVIDUAL_DID],
|
|
17
|
+
sectors: [DataspaceSectors.HealthCare],
|
|
18
|
+
validFrom: '2026-01-01T00:00:00.000Z',
|
|
19
|
+
validUntil: '2027-01-01T00:00:00.000Z',
|
|
20
|
+
}));
|
package/dist/models/index.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export * from './resource-document';
|
|
|
48
48
|
export * from './relationship-access';
|
|
49
49
|
export * from './response';
|
|
50
50
|
export * from './subject-identifier-ledger';
|
|
51
|
+
export * from './subject-identity-binding';
|
|
51
52
|
export * from './urlPath';
|
|
52
53
|
export * from './verifiable-credential';
|
|
53
54
|
export * from './wallet';
|
package/dist/models/index.js
CHANGED
|
@@ -48,6 +48,7 @@ export * from './resource-document.js';
|
|
|
48
48
|
export * from './relationship-access.js';
|
|
49
49
|
export * from './response.js';
|
|
50
50
|
export * from './subject-identifier-ledger.js';
|
|
51
|
+
export * from './subject-identity-binding.js';
|
|
51
52
|
export * from './urlPath.js';
|
|
52
53
|
export * from './verifiable-credential.js';
|
|
53
54
|
export * from './wallet.js';
|
package/dist/models/indexing.js
CHANGED
|
@@ -193,7 +193,7 @@ export function buildIndexParametersFromClaims(claims, allowedClaims) {
|
|
|
193
193
|
const parameter = {
|
|
194
194
|
name: claimKey,
|
|
195
195
|
value: value,
|
|
196
|
-
type: inferParameterTypeFromClaimKey(claimKey, rawValue),
|
|
196
|
+
type: inferPa type: inferParameterTypeFromClaimKey(claimKey, rawValue),
|
|
197
197
|
...(claimKey === ObservationClaim.ValueQuantityUnit && typeof rawValue === 'string'
|
|
198
198
|
? { unit: rawValue }
|
|
199
199
|
: {}),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Canonical JSON members of a subject identity binding credential. */
|
|
2
|
+
export declare const SubjectIdentityBindingClaims: Readonly<{
|
|
3
|
+
SubjectId: "id";
|
|
4
|
+
SameAs: "sameAs";
|
|
5
|
+
Sector: "sector";
|
|
6
|
+
}>;
|
|
7
|
+
/** Normalized, security-relevant projection of a subject identity binding VC. */
|
|
8
|
+
export type SubjectIdentityBindingSummary = Readonly<{
|
|
9
|
+
issuerDid: string;
|
|
10
|
+
subjectDid: string;
|
|
11
|
+
aliasDids: readonly string[];
|
|
12
|
+
sectors: readonly string[];
|
|
13
|
+
validFrom?: string;
|
|
14
|
+
validUntil?: string;
|
|
15
|
+
}>;
|
|
16
|
+
/** Criteria used by a verifier after the enclosing VP has been verified. */
|
|
17
|
+
export type SubjectIdentityBindingMatchCriteria = Readonly<{
|
|
18
|
+
trustedIssuerDids: readonly string[];
|
|
19
|
+
requiredSubjectDids: readonly string[];
|
|
20
|
+
sector?: string;
|
|
21
|
+
now?: string | Date;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
+
/** Canonical JSON members of a subject identity binding credential. */
|
|
3
|
+
export const SubjectIdentityBindingClaims = Object.freeze({
|
|
4
|
+
SubjectId: 'id',
|
|
5
|
+
SameAs: 'sameAs',
|
|
6
|
+
Sector: 'sector',
|
|
7
|
+
});
|
|
@@ -234,7 +234,7 @@ export class BundleEntryEditor {
|
|
|
234
234
|
resourceType: this.bundleEditor.getAllowedResourceType() || EmployeeResourceTypes.employee,
|
|
235
235
|
meta: { claims: {} },
|
|
236
236
|
};
|
|
237
|
-
entry.resource.meta = entry.resource.meta || {};
|
|
237
|
+
entry.resource.meta = entry.resource.meta || { claims: {} };
|
|
238
238
|
entry.resource.meta.claims = {
|
|
239
239
|
...(entry.resource.meta.claims || {}),
|
|
240
240
|
[String(key).trim()]: cloneClaimValue(value),
|
|
@@ -264,7 +264,7 @@ export class BundleEntryEditor {
|
|
|
264
264
|
resourceType: this.bundleEditor.getAllowedResourceType() || EmployeeResourceTypes.employee,
|
|
265
265
|
meta: { claims: {} },
|
|
266
266
|
};
|
|
267
|
-
entry.resource.meta = entry.resource.meta || {};
|
|
267
|
+
entry.resource.meta = entry.resource.meta || { claims: {} };
|
|
268
268
|
entry.resource.meta.claims = claims;
|
|
269
269
|
return this;
|
|
270
270
|
}
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import type { BundleEntry, BundleJsonApi } from '../models/bundle.js';
|
|
2
|
+
/** Canonical date interval shared by clinical Bundle read filters. */
|
|
3
|
+
export type BundleResourceDateFilter = Readonly<{
|
|
4
|
+
start?: string;
|
|
5
|
+
end?: string;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Filters Bundle resources by section, resource type and clinical date.
|
|
9
|
+
*
|
|
10
|
+
* Use `types` and `date` in new code. The flat names remain temporary
|
|
11
|
+
* compatibility aliases for callers created before the document readers were
|
|
12
|
+
* aligned with `FhirDocumentFacade`.
|
|
13
|
+
*/
|
|
2
14
|
export type BundleResourceIdFilters = Readonly<{
|
|
3
15
|
sections?: string | readonly string[];
|
|
16
|
+
types?: string | readonly string[];
|
|
17
|
+
date?: BundleResourceDateFilter;
|
|
18
|
+
/** @deprecated Use `types`. */
|
|
4
19
|
resourceTypes?: string | readonly string[];
|
|
20
|
+
/** @deprecated Use `date.start`. */
|
|
5
21
|
dateFrom?: string;
|
|
22
|
+
/** @deprecated Use `date.end`. */
|
|
6
23
|
dateTo?: string;
|
|
7
24
|
}>;
|
|
8
25
|
/**
|
|
@@ -82,7 +82,7 @@ export class BundleQuery {
|
|
|
82
82
|
}
|
|
83
83
|
matchesResourceFilters(entry, filters) {
|
|
84
84
|
const resourceType = asTrimmedString(entry?.resource?.resourceType);
|
|
85
|
-
const resourceTypeFilters = normalizeTokenInput(filters.resourceTypes);
|
|
85
|
+
const resourceTypeFilters = normalizeTokenInput(filters.types !== undefined ? filters.types : filters.resourceTypes);
|
|
86
86
|
if (resourceTypeFilters.length > 0 && !resourceTypeFilters.includes(resourceType)) {
|
|
87
87
|
return false;
|
|
88
88
|
}
|
|
@@ -92,7 +92,9 @@ export class BundleQuery {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
const entryDate = this.resolveEntryDate(claims);
|
|
95
|
-
|
|
95
|
+
const dateFrom = filters.date !== undefined ? filters.date.start : filters.dateFrom;
|
|
96
|
+
const dateTo = filters.date !== undefined ? filters.date.end : filters.dateTo;
|
|
97
|
+
if (!this.matchesDateRange(entryDate, dateFrom, dateTo)) {
|
|
96
98
|
return false;
|
|
97
99
|
}
|
|
98
100
|
return true;
|
|
@@ -116,6 +118,11 @@ export class BundleQuery {
|
|
|
116
118
|
const normalized = String(key || '').toLowerCase();
|
|
117
119
|
if (normalized.endsWith('.date')
|
|
118
120
|
|| normalized.endsWith('.effective')
|
|
121
|
+
|| normalized.endsWith('.effective-datetime')
|
|
122
|
+
|| normalized.endsWith('.effective-period-start')
|
|
123
|
+
|| normalized.endsWith('.onset-datetime')
|
|
124
|
+
|| normalized.endsWith('.occurrence-datetime')
|
|
125
|
+
|| normalized.endsWith('.recorded-date')
|
|
119
126
|
|| normalized.endsWith('.sent')
|
|
120
127
|
|| normalized.endsWith('.authored-on')) {
|
|
121
128
|
const dateValue = asTrimmedString(value);
|
|
@@ -114,6 +114,21 @@ export declare class BundleReader {
|
|
|
114
114
|
getDocumentSectionResourceCount(sectionCodeOrClaim: string): number;
|
|
115
115
|
/** Returns bundle resource references listed under one document section. */
|
|
116
116
|
getDocumentSectionResourceReferences(sectionCodeOrClaim: string): string[];
|
|
117
|
+
/**
|
|
118
|
+
* Returns stable resource IDs that both belong to one Composition section
|
|
119
|
+
* and match the optional resource-type/date filters.
|
|
120
|
+
*
|
|
121
|
+
* Use this after `$summary` when a screen or channel needs the concrete
|
|
122
|
+
* resources for one section. `getDocumentSectionResourceCount(...)` counts
|
|
123
|
+
* declared Composition references; this method resolves those references
|
|
124
|
+
* against the returned Bundle and can narrow the result.
|
|
125
|
+
*/
|
|
126
|
+
getDocumentSectionResourceIds(sectionCodeOrClaim: string, filters?: BundleResourceIdFilters): string[];
|
|
127
|
+
/**
|
|
128
|
+
* Returns cloned Bundle entries for the resources selected from one
|
|
129
|
+
* Composition section, optionally filtered by type and inclusive date range.
|
|
130
|
+
*/
|
|
131
|
+
getDocumentSectionResourceEntries(sectionCodeOrClaim: string, filters?: BundleResourceIdFilters): BundleReaderEntry[];
|
|
117
132
|
/** Returns the active entry response status when present. */
|
|
118
133
|
getEntryResponseStatus(): string | undefined;
|
|
119
134
|
/** Returns all active entry issue severities. */
|
|
@@ -150,6 +165,7 @@ export declare class BundleReader {
|
|
|
150
165
|
private buildEntrySummary;
|
|
151
166
|
private buildSeverityBucket;
|
|
152
167
|
private resolveEntryIdentifier;
|
|
168
|
+
private resolveEntryReferenceCandidates;
|
|
153
169
|
}
|
|
154
170
|
export declare function unwrapBundleLikeResponseBody(input: unknown): Record<string, unknown>;
|
|
155
171
|
export declare function readFirstBundleResourceFromResponseBody(input: unknown): Record<string, unknown> | undefined;
|
|
@@ -235,7 +235,9 @@ export class BundleReader {
|
|
|
235
235
|
if (!normalized) {
|
|
236
236
|
return undefined;
|
|
237
237
|
}
|
|
238
|
-
|
|
238
|
+
const normalizedClaim = normalizeSectionClaim(normalized);
|
|
239
|
+
return this.getDocumentSections().find((section) => section.code === normalized
|
|
240
|
+
|| (section.claim !== undefined && normalizeSectionClaim(section.claim) === normalizedClaim));
|
|
239
241
|
}
|
|
240
242
|
/** Returns the number of resource references inside one document section. */
|
|
241
243
|
getDocumentSectionResourceCount(sectionCodeOrClaim) {
|
|
@@ -245,6 +247,30 @@ export class BundleReader {
|
|
|
245
247
|
getDocumentSectionResourceReferences(sectionCodeOrClaim) {
|
|
246
248
|
return [...(this.getDocumentSectionByCode(sectionCodeOrClaim)?.entryReferences || [])];
|
|
247
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Returns stable resource IDs that both belong to one Composition section
|
|
252
|
+
* and match the optional resource-type/date filters.
|
|
253
|
+
*
|
|
254
|
+
* Use this after `$summary` when a screen or channel needs the concrete
|
|
255
|
+
* resources for one section. `getDocumentSectionResourceCount(...)` counts
|
|
256
|
+
* declared Composition references; this method resolves those references
|
|
257
|
+
* against the returned Bundle and can narrow the result.
|
|
258
|
+
*/
|
|
259
|
+
getDocumentSectionResourceIds(sectionCodeOrClaim, filters = {}) {
|
|
260
|
+
const references = new Set(this.getDocumentSectionResourceReferences(sectionCodeOrClaim));
|
|
261
|
+
if (references.size === 0) {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
return this.getResourceIds(filters).filter((resourceId) => this.getEntriesByIds([resourceId]).some((entry) => this.resolveEntryReferenceCandidates(entry)
|
|
265
|
+
.some((reference) => references.has(reference))));
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Returns cloned Bundle entries for the resources selected from one
|
|
269
|
+
* Composition section, optionally filtered by type and inclusive date range.
|
|
270
|
+
*/
|
|
271
|
+
getDocumentSectionResourceEntries(sectionCodeOrClaim, filters = {}) {
|
|
272
|
+
return this.getEntriesByIds(this.getDocumentSectionResourceIds(sectionCodeOrClaim, filters));
|
|
273
|
+
}
|
|
248
274
|
/** Returns the active entry response status when present. */
|
|
249
275
|
getEntryResponseStatus() {
|
|
250
276
|
const entry = this.getRequiredActiveEntry();
|
|
@@ -434,10 +460,32 @@ export class BundleReader {
|
|
|
434
460
|
.find((key) => String(key || '').toLowerCase().endsWith('.identifier'));
|
|
435
461
|
return identifierKey ? normalizeOptionalString(claims[identifierKey]) : undefined;
|
|
436
462
|
}
|
|
463
|
+
resolveEntryReferenceCandidates(entry) {
|
|
464
|
+
const resource = asRecord(entry.resource);
|
|
465
|
+
const resourceType = asNonEmptyString(resource.resourceType);
|
|
466
|
+
const resourceId = asNonEmptyString(resource.id);
|
|
467
|
+
return Array.from(new Set([
|
|
468
|
+
asNonEmptyString(entry.id),
|
|
469
|
+
asNonEmptyString(entry.fullUrl),
|
|
470
|
+
resourceId,
|
|
471
|
+
resourceType && resourceId ? `${resourceType}/${resourceId}` : undefined,
|
|
472
|
+
this.resolveEntryIdentifier(entry),
|
|
473
|
+
].filter((value) => Boolean(value))));
|
|
474
|
+
}
|
|
437
475
|
}
|
|
438
476
|
function normalizeOptionalString(value) {
|
|
439
477
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
440
478
|
}
|
|
479
|
+
function normalizeSectionClaim(value) {
|
|
480
|
+
const [system, ...codeParts] = value.split('|');
|
|
481
|
+
if (codeParts.length === 0) {
|
|
482
|
+
return value.trim().toLowerCase();
|
|
483
|
+
}
|
|
484
|
+
const normalizedSystem = system.trim().toLowerCase() === 'http://loinc.org'
|
|
485
|
+
? 'loinc'
|
|
486
|
+
: system.trim().toLowerCase();
|
|
487
|
+
return `${normalizedSystem}|${codeParts.join('|').trim().toLowerCase()}`;
|
|
488
|
+
}
|
|
441
489
|
export function unwrapBundleLikeResponseBody(input) {
|
|
442
490
|
const body = input && typeof input === 'object' ? input : {};
|
|
443
491
|
const nested = body.body && typeof body.body === 'object' ? body.body : undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type JWK } from 'jose';
|
|
2
2
|
export type ClientAssertionJwtAlgorithm = 'ES256' | 'ES384' | 'ES512' | 'EdDSA';
|
|
3
3
|
export type BuildClientAssertionJwtInput = {
|
|
4
4
|
clientId: string;
|
|
@@ -34,5 +34,5 @@ export declare function buildClientAssertionJwt(input: BuildClientAssertionJwtIn
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function buildClientAssertionFixture(input: BuildClientAssertionJwtInput): Promise<{
|
|
36
36
|
jwt: string;
|
|
37
|
-
publicJwk:
|
|
37
|
+
publicJwk: JWK;
|
|
38
38
|
}>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BundleResourceDateFilter, BundleResourceIdFilters } from './bundle-query';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical high-level filter accepted by both Bundle section readers and
|
|
4
|
+
* `FhirDocumentFacade`.
|
|
5
|
+
*/
|
|
6
|
+
export type ClinicalResourceFilter = Readonly<{
|
|
7
|
+
sections?: string[];
|
|
8
|
+
types?: string[];
|
|
9
|
+
date?: BundleResourceDateFilter;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Chainable editor for one clinical document resource filter.
|
|
13
|
+
*
|
|
14
|
+
* The built value can be passed unchanged to:
|
|
15
|
+
*
|
|
16
|
+
* - `BundleReader.getDocumentSectionResourceIds(...)`
|
|
17
|
+
* - `BundleReader.getDocumentSectionResourceEntries(...)`
|
|
18
|
+
* - `FhirDocumentFacade.getResourcesByFilter(...)`
|
|
19
|
+
* - `FhirDocumentFacade.getResourceCount(...)`
|
|
20
|
+
*/
|
|
21
|
+
export declare class ClinicalResourceFilterEditor {
|
|
22
|
+
private sections?;
|
|
23
|
+
private types?;
|
|
24
|
+
private date?;
|
|
25
|
+
/** Replaces the selected Composition sections. An empty list means all. */
|
|
26
|
+
setSections(values: readonly string[]): this;
|
|
27
|
+
/** Replaces the selected FHIR resource types. An empty list means all. */
|
|
28
|
+
setTypes(values: readonly string[]): this;
|
|
29
|
+
/** Sets the complete clinical date interval. Either boundary may be omitted. */
|
|
30
|
+
setPeriod(start?: string, end?: string): this;
|
|
31
|
+
/** Sets or clears only the start boundary of the clinical date interval. */
|
|
32
|
+
setPeriodStart(start?: string): this;
|
|
33
|
+
/** Sets or clears only the end boundary of the clinical date interval. */
|
|
34
|
+
setPeriodEnd(end?: string): this;
|
|
35
|
+
/** Removes both clinical date boundaries. */
|
|
36
|
+
clearPeriod(): this;
|
|
37
|
+
/** Returns a detached canonical filter safe for either public reader. */
|
|
38
|
+
build(): ClinicalResourceFilter & BundleResourceIdFilters;
|
|
39
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
+
/**
|
|
3
|
+
* Chainable editor for one clinical document resource filter.
|
|
4
|
+
*
|
|
5
|
+
* The built value can be passed unchanged to:
|
|
6
|
+
*
|
|
7
|
+
* - `BundleReader.getDocumentSectionResourceIds(...)`
|
|
8
|
+
* - `BundleReader.getDocumentSectionResourceEntries(...)`
|
|
9
|
+
* - `FhirDocumentFacade.getResourcesByFilter(...)`
|
|
10
|
+
* - `FhirDocumentFacade.getResourceCount(...)`
|
|
11
|
+
*/
|
|
12
|
+
export class ClinicalResourceFilterEditor {
|
|
13
|
+
sections;
|
|
14
|
+
types;
|
|
15
|
+
date;
|
|
16
|
+
/** Replaces the selected Composition sections. An empty list means all. */
|
|
17
|
+
setSections(values) {
|
|
18
|
+
this.sections = normalizeTokens(values);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
/** Replaces the selected FHIR resource types. An empty list means all. */
|
|
22
|
+
setTypes(values) {
|
|
23
|
+
this.types = normalizeTokens(values);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
/** Sets the complete clinical date interval. Either boundary may be omitted. */
|
|
27
|
+
setPeriod(start, end) {
|
|
28
|
+
this.date = buildDateFilter(start, end);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
/** Sets or clears only the start boundary of the clinical date interval. */
|
|
32
|
+
setPeriodStart(start) {
|
|
33
|
+
this.date = buildDateFilter(start, this.date?.end);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
/** Sets or clears only the end boundary of the clinical date interval. */
|
|
37
|
+
setPeriodEnd(end) {
|
|
38
|
+
this.date = buildDateFilter(this.date?.start, end);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
/** Removes both clinical date boundaries. */
|
|
42
|
+
clearPeriod() {
|
|
43
|
+
this.date = undefined;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/** Returns a detached canonical filter safe for either public reader. */
|
|
47
|
+
build() {
|
|
48
|
+
return {
|
|
49
|
+
...(this.sections?.length ? { sections: [...this.sections] } : {}),
|
|
50
|
+
...(this.types?.length ? { types: [...this.types] } : {}),
|
|
51
|
+
...(this.date ? { date: { ...this.date } } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function normalizeTokens(values) {
|
|
56
|
+
const normalized = [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
|
|
57
|
+
return normalized.length ? normalized : undefined;
|
|
58
|
+
}
|
|
59
|
+
function buildDateFilter(start, end) {
|
|
60
|
+
const normalizedStart = normalizeDate(start, 'start');
|
|
61
|
+
const normalizedEnd = normalizeDate(end, 'end');
|
|
62
|
+
if (normalizedStart && normalizedEnd) {
|
|
63
|
+
if (Date.parse(normalizedStart) > Date.parse(normalizedEnd)) {
|
|
64
|
+
throw new RangeError('Clinical resource filter period start must not be after end.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!normalizedStart && !normalizedEnd) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...(normalizedStart ? { start: normalizedStart } : {}),
|
|
72
|
+
...(normalizedEnd ? { end: normalizedEnd } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function normalizeDate(value, boundary) {
|
|
76
|
+
const normalized = String(value || '').trim();
|
|
77
|
+
if (!normalized) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (!Number.isFinite(Date.parse(normalized))) {
|
|
81
|
+
throw new TypeError(`Clinical resource filter period ${boundary} must be a valid date.`);
|
|
82
|
+
}
|
|
83
|
+
return normalized;
|
|
84
|
+
}
|
|
@@ -150,9 +150,10 @@ export declare function buildBundleSearchReferenceUrl(input: Readonly<{
|
|
|
150
150
|
/**
|
|
151
151
|
* Creates the canonical semantic parameters for an IPS summary-style request.
|
|
152
152
|
*
|
|
153
|
-
* These parameters are the source of truth.
|
|
154
|
-
*
|
|
155
|
-
* them
|
|
153
|
+
* These parameters are the source of truth. The canonical `$summary` read
|
|
154
|
+
* attaches them as one FHIR `Parameters` resource to an auditable
|
|
155
|
+
* `Communication`. Flattening them into a `Bundle/_search` reference is a
|
|
156
|
+
* compatibility path and must not be taught as the primary 101 read flow.
|
|
156
157
|
*/
|
|
157
158
|
export declare function createSummaryOperationRequestParameters(subjectIdOrInput: string | CreateSummaryOperationParametersInput, filterSections?: string[]): ReadonlyArray<ParameterData>;
|
|
158
159
|
/**
|
|
@@ -161,8 +162,8 @@ export declare function createSummaryOperationRequestParameters(subjectIdOrInput
|
|
|
161
162
|
*/
|
|
162
163
|
export declare function createSummaryOperationRequestReferencePath(parameters: ReadonlyArray<ParameterData>): string;
|
|
163
164
|
/**
|
|
164
|
-
* Builds the
|
|
165
|
-
*
|
|
165
|
+
* Builds the canonical FHIR `Parameters` body attached to a `$summary`
|
|
166
|
+
* request `Communication`.
|
|
166
167
|
*/
|
|
167
168
|
export declare function createSummaryOperationRequestParametersResource(parameters: ReadonlyArray<ParameterData>): FhirParametersResource;
|
|
168
169
|
export declare function buildCommunicationRequestOperationWithAttachedParametersClaims(input: CreateSummaryOperationCommunicationInput): Record<string, unknown>;
|
|
@@ -188,11 +189,11 @@ export declare const flattenParametersToSearchReference: typeof createSummaryOpe
|
|
|
188
189
|
* carried inside `Communication`.
|
|
189
190
|
*
|
|
190
191
|
* Current split:
|
|
191
|
-
* - `
|
|
192
|
-
* search-url contract
|
|
193
|
-
* - `setRequestSummaryOperation(...)` builds the operation contract where
|
|
192
|
+
* - `setRequestSummaryOperation(...)` is the canonical 101 read contract where
|
|
194
193
|
* `content-reference` points to the operation path and
|
|
195
194
|
* `content-attachment-data` carries the serialized FHIR `Parameters`
|
|
195
|
+
* - `newSearchWithReferencePath(...)` keeps the older flattened `_search`
|
|
196
|
+
* compatibility contract
|
|
196
197
|
*/
|
|
197
198
|
export declare const communication: Readonly<{
|
|
198
199
|
/**
|
|
@@ -217,9 +217,10 @@ export function buildBundleSearchReferenceUrl(input) {
|
|
|
217
217
|
/**
|
|
218
218
|
* Creates the canonical semantic parameters for an IPS summary-style request.
|
|
219
219
|
*
|
|
220
|
-
* These parameters are the source of truth.
|
|
221
|
-
*
|
|
222
|
-
* them
|
|
220
|
+
* These parameters are the source of truth. The canonical `$summary` read
|
|
221
|
+
* attaches them as one FHIR `Parameters` resource to an auditable
|
|
222
|
+
* `Communication`. Flattening them into a `Bundle/_search` reference is a
|
|
223
|
+
* compatibility path and must not be taught as the primary 101 read flow.
|
|
223
224
|
*/
|
|
224
225
|
export function createSummaryOperationRequestParameters(subjectIdOrInput, filterSections) {
|
|
225
226
|
const input = typeof subjectIdOrInput === 'string'
|
|
@@ -234,10 +235,14 @@ export function createSummaryOperationRequestParameters(subjectIdOrInput, filter
|
|
|
234
235
|
if (!documentTypeDescriptor) {
|
|
235
236
|
throw new Error(`Unsupported documentType: ${String(documentType)}`);
|
|
236
237
|
}
|
|
238
|
+
const sections = normalizeStringArray(input.filterSections);
|
|
239
|
+
if (sections.includes('*')) {
|
|
240
|
+
throw new Error('Omit filterSections to request all available sections; "*" is reserved for SMART permission scopes.');
|
|
241
|
+
}
|
|
237
242
|
return [
|
|
238
243
|
buildSubjectParameter(subjectDid),
|
|
239
244
|
buildDocumentTypeParameter(documentTypeDescriptor.id, documentTypeDescriptor.attributeValue),
|
|
240
|
-
...buildSectionParameters(
|
|
245
|
+
...buildSectionParameters(sections),
|
|
241
246
|
];
|
|
242
247
|
}
|
|
243
248
|
/**
|
|
@@ -267,8 +272,8 @@ export function createSummaryOperationRequestReferencePath(parameters) {
|
|
|
267
272
|
return `individual/org.hl7.fhir.r4/Bundle/_search?${params.filter(Boolean).join('&')}`;
|
|
268
273
|
}
|
|
269
274
|
/**
|
|
270
|
-
* Builds the
|
|
271
|
-
*
|
|
275
|
+
* Builds the canonical FHIR `Parameters` body attached to a `$summary`
|
|
276
|
+
* request `Communication`.
|
|
272
277
|
*/
|
|
273
278
|
export function createSummaryOperationRequestParametersResource(parameters) {
|
|
274
279
|
return buildFhirParametersResourceFromParameterData(parameters);
|
|
@@ -348,11 +353,11 @@ export const flattenParametersToSearchReference = createSummaryOperationRequestR
|
|
|
348
353
|
* carried inside `Communication`.
|
|
349
354
|
*
|
|
350
355
|
* Current split:
|
|
351
|
-
* - `
|
|
352
|
-
* search-url contract
|
|
353
|
-
* - `setRequestSummaryOperation(...)` builds the operation contract where
|
|
356
|
+
* - `setRequestSummaryOperation(...)` is the canonical 101 read contract where
|
|
354
357
|
* `content-reference` points to the operation path and
|
|
355
358
|
* `content-attachment-data` carries the serialized FHIR `Parameters`
|
|
359
|
+
* - `newSearchWithReferencePath(...)` keeps the older flattened `_search`
|
|
360
|
+
* compatibility contract
|
|
356
361
|
*/
|
|
357
362
|
export const communication = Object.freeze({
|
|
358
363
|
/**
|
|
@@ -35,7 +35,8 @@ export function transformCommunicationClaimsToResourceFhirR4(communicationClaims
|
|
|
35
35
|
const hasReference = Boolean(payloadReference);
|
|
36
36
|
const hasCode = Boolean(payloadCodeRaw);
|
|
37
37
|
const payloadKinds = [hasAttachment, hasReference, hasCode].filter(Boolean).length;
|
|
38
|
-
|
|
38
|
+
const isOperationReferenceWithParameters = hasAttachment && hasReference && !hasCode;
|
|
39
|
+
if (payloadKinds > 1 && !isOperationReferenceWithParameters) {
|
|
39
40
|
const msg = `Communication[${index}] has more than one payload kind (attachment/reference/code).`;
|
|
40
41
|
if (mode === 'strict')
|
|
41
42
|
throw new Error(msg);
|
|
@@ -50,10 +51,10 @@ export function transformCommunicationClaimsToResourceFhirR4(communicationClaims
|
|
|
50
51
|
throw new Error(msg);
|
|
51
52
|
warnings.push(`${msg} Keeping first note only.`);
|
|
52
53
|
}
|
|
53
|
-
const payload =
|
|
54
|
+
const payload = buildPayloads({
|
|
54
55
|
hasAttachment,
|
|
55
|
-
hasReference,
|
|
56
|
-
hasCode,
|
|
56
|
+
hasReference: isOperationReferenceWithParameters || (!hasAttachment && hasReference),
|
|
57
|
+
hasCode: !hasAttachment && !hasReference && hasCode,
|
|
57
58
|
payloadAttachmentData,
|
|
58
59
|
payloadAttachmentType,
|
|
59
60
|
payloadAttachmentTitle,
|
|
@@ -95,8 +96,8 @@ export function transformCommunicationClaimsToResourceFhirR4(communicationClaims
|
|
|
95
96
|
resource['sender'] = { reference: sender };
|
|
96
97
|
if (partOf)
|
|
97
98
|
resource['partOf'] = [{ reference: partOf }];
|
|
98
|
-
if (payload)
|
|
99
|
-
resource['payload'] =
|
|
99
|
+
if (payload.length)
|
|
100
|
+
resource['payload'] = payload;
|
|
100
101
|
if (noteValues.length)
|
|
101
102
|
resource['note'] = [{ text: noteValues[0] }];
|
|
102
103
|
return resource;
|
|
@@ -126,10 +127,13 @@ export function extractCommunicationClaimsFromResourceFhirR4(resource, options =
|
|
|
126
127
|
const partOfRef = resource?.partOf?.[0]?.reference;
|
|
127
128
|
const noteText = resource?.note?.[0]?.text;
|
|
128
129
|
const categoryCoding = resource?.category?.[0]?.coding?.[0];
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
130
|
+
const payloads = resource?.payload || [];
|
|
131
|
+
const referencePayload = payloads.find((payload) => payload.contentReference !== undefined);
|
|
132
|
+
const attachmentPayload = payloads.find((payload) => payload.contentAttachment !== undefined);
|
|
133
|
+
const codePayload = payloads.find((payload) => payload.contentCodeableConcept !== undefined);
|
|
134
|
+
const contentReference = referencePayload?.contentReference?.reference;
|
|
135
|
+
const contentAttachment = attachmentPayload?.contentAttachment;
|
|
136
|
+
const contentCodeableConcept = codePayload?.contentCodeableConcept?.coding?.[0];
|
|
133
137
|
setIf(claims, CommunicationClaim.Identifier, identifierValue);
|
|
134
138
|
setIf(claims, CommunicationClaim.Status, status);
|
|
135
139
|
setIf(claims, CommunicationClaim.Sent, sent);
|
|
@@ -177,8 +181,12 @@ function normalizeNoteValues(raw) {
|
|
|
177
181
|
}
|
|
178
182
|
return [];
|
|
179
183
|
}
|
|
180
|
-
function
|
|
184
|
+
function buildPayloads(input) {
|
|
181
185
|
const { hasAttachment, hasReference, hasCode, payloadAttachmentData, payloadAttachmentType, payloadAttachmentTitle, payloadAttachmentUrl, payloadReference, payloadCodeRaw, } = input;
|
|
186
|
+
const payloads = [];
|
|
187
|
+
if (hasReference) {
|
|
188
|
+
payloads.push({ contentReference: { reference: payloadReference } });
|
|
189
|
+
}
|
|
182
190
|
if (hasAttachment) {
|
|
183
191
|
const value = {};
|
|
184
192
|
if (payloadAttachmentData)
|
|
@@ -189,13 +197,12 @@ function buildPayload(input) {
|
|
|
189
197
|
value['title'] = payloadAttachmentTitle;
|
|
190
198
|
if (payloadAttachmentUrl)
|
|
191
199
|
value['url'] = payloadAttachmentUrl;
|
|
192
|
-
|
|
200
|
+
payloads.push({ contentAttachment: value });
|
|
201
|
+
}
|
|
202
|
+
if (hasCode && payloadCodeRaw) {
|
|
203
|
+
payloads.push({ contentCodeableConcept: { coding: [parseSystemCode(payloadCodeRaw)] } });
|
|
193
204
|
}
|
|
194
|
-
|
|
195
|
-
return { contentReference: { reference: payloadReference } };
|
|
196
|
-
if (hasCode && payloadCodeRaw)
|
|
197
|
-
return { contentCodeableConcept: { coding: [parseSystemCode(payloadCodeRaw)] } };
|
|
198
|
-
return undefined;
|
|
205
|
+
return payloads;
|
|
199
206
|
}
|
|
200
207
|
function parseSystemCode(value) {
|
|
201
208
|
const trimmed = String(value || '').trim();
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -99,6 +99,7 @@ export * from './permission-templates';
|
|
|
99
99
|
export * from './smart-scope';
|
|
100
100
|
export * from './service-act-reasons';
|
|
101
101
|
export * from './same-as';
|
|
102
|
+
export * from './subject-identity-binding';
|
|
102
103
|
export * from './activation-request';
|
|
103
104
|
export * from './vp-token';
|
|
104
105
|
export * from './vital-sign-day-batch';
|
package/dist/utils/index.js
CHANGED
|
@@ -99,6 +99,7 @@ export * from './permission-templates.js';
|
|
|
99
99
|
export * from './smart-scope.js';
|
|
100
100
|
export * from './service-act-reasons.js';
|
|
101
101
|
export * from './same-as.js';
|
|
102
|
+
export * from './subject-identity-binding.js';
|
|
102
103
|
export * from './activation-request.js';
|
|
103
104
|
export * from './vp-token.js';
|
|
104
105
|
export * from './vital-sign-day-batch.js';
|
|
@@ -33,6 +33,19 @@ export type LegalOrganizationOnboardingDraftResult = Readonly<{
|
|
|
33
33
|
export type LegalOrganizationGatewayVerificationSignatureFlow = 'certificate' | 'otp';
|
|
34
34
|
export type LegalOrganizationGatewayVerificationRequestInput = Readonly<{
|
|
35
35
|
controller: LegalOrganizationVerificationTransactionController;
|
|
36
|
+
/**
|
|
37
|
+
* Explicit public organization DID controlled by the registering portal.
|
|
38
|
+
* This is independent from the GW/service DID and from its network address.
|
|
39
|
+
*/
|
|
40
|
+
publicOrganizationDid?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Public portal domain used to derive the canonical path DID when
|
|
43
|
+
* `publicOrganizationDid` is omitted.
|
|
44
|
+
*
|
|
45
|
+
* Result:
|
|
46
|
+
* `did:web:<domain>:<sector>:organization:taxid:<tax-id>`
|
|
47
|
+
*/
|
|
48
|
+
publicOrganizationDomain?: string;
|
|
36
49
|
signatureFlow?: LegalOrganizationGatewayVerificationSignatureFlow;
|
|
37
50
|
representativeSameAs?: string;
|
|
38
51
|
verificationResourceType?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ClaimsOrganizationSchemaorg, ClaimsPersonSchemaorg, ClaimsServiceSchemaorg, } from '../constants/schemaorg.js';
|
|
2
2
|
import { validateLegalOrganizationOnboardingClaims, } from './legal-organization-onboarding.js';
|
|
3
|
+
import { buildHostedProviderDidWeb } from './did.js';
|
|
3
4
|
function normalizeText(value) {
|
|
4
5
|
return typeof value === 'string' ? value.trim() : '';
|
|
5
6
|
}
|
|
@@ -275,16 +276,26 @@ export function createLegalOrganizationOnboardingFacade() {
|
|
|
275
276
|
},
|
|
276
277
|
buildGatewayVerificationRequest(fields, input) {
|
|
277
278
|
const controllerEmail = facade.getControllerEmail(fields);
|
|
278
|
-
const serviceIdentifier = facade.getServiceIdentifier(fields);
|
|
279
279
|
const serviceUrl = facade.getServiceUrl(fields);
|
|
280
|
+
const publicOrganizationDid = normalizeOptionalText(input.publicOrganizationDid);
|
|
281
|
+
const publicOrganizationDomain = normalizeOptionalText(input.publicOrganizationDomain);
|
|
282
|
+
const organizationDid = publicOrganizationDid || (publicOrganizationDomain
|
|
283
|
+
&& facade.getServiceCategory(fields)
|
|
284
|
+
&& facade.getTaxId(fields)
|
|
285
|
+
? buildHostedProviderDidWeb({
|
|
286
|
+
hostDomain: publicOrganizationDomain,
|
|
287
|
+
sector: facade.getServiceCategory(fields),
|
|
288
|
+
providerTaxId: facade.getTaxId(fields),
|
|
289
|
+
})
|
|
290
|
+
: undefined);
|
|
280
291
|
const signatureFlow = normalizeText(input.signatureFlow || 'certificate').toLowerCase();
|
|
281
292
|
const representativeSameAs = normalizeOptionalText(input.representativeSameAs || controllerEmail);
|
|
282
293
|
const signedTermsPdfUrl = normalizeOptionalText(input.signedTermsPdfUrl);
|
|
283
294
|
return facade.buildVerificationTransactionInput(fields, {
|
|
284
295
|
controller: input.controller,
|
|
285
|
-
organization:
|
|
296
|
+
organization: organizationDid || serviceUrl
|
|
286
297
|
? {
|
|
287
|
-
...(
|
|
298
|
+
...(organizationDid ? { did: organizationDid } : {}),
|
|
288
299
|
...(serviceUrl ? { url: serviceUrl } : {}),
|
|
289
300
|
}
|
|
290
301
|
: undefined,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type SubjectIdentityBindingMatchCriteria, type SubjectIdentityBindingSummary } from '../models/subject-identity-binding';
|
|
2
|
+
import type { VerifiableCredentialV2 } from '../models/verifiable-credential';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the unsigned JSON form of a subject identity binding VC.
|
|
5
|
+
*
|
|
6
|
+
* This helper only builds the canonical payload. The issuer must sign it using
|
|
7
|
+
* the normal VC/VP signing flow before a verifier treats it as evidence.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildSubjectIdentityBindingCredential(input: Readonly<{
|
|
10
|
+
issuerDid: string;
|
|
11
|
+
subjectDid: string;
|
|
12
|
+
aliasDids: readonly string[];
|
|
13
|
+
sectors: readonly string[];
|
|
14
|
+
validFrom: string;
|
|
15
|
+
validUntil?: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
}>): VerifiableCredentialV2;
|
|
18
|
+
/** Returns the normalized identity set asserted by one binding VC. */
|
|
19
|
+
export declare function summarizeSubjectIdentityBinding(credential: unknown): SubjectIdentityBindingSummary | undefined;
|
|
20
|
+
/** Checks issuer trust, validity, sector and exact DID membership. */
|
|
21
|
+
export declare function matchesSubjectIdentityBinding(summary: SubjectIdentityBindingSummary | undefined, criteria: SubjectIdentityBindingMatchCriteria): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Finds a matching binding credential in an already-verified VP token.
|
|
24
|
+
*
|
|
25
|
+
* This function decodes and matches claims; it does not verify signatures.
|
|
26
|
+
* Callers must first verify the enclosing VP/VC proof chain.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getMatchingSubjectIdentityBindingFromVpToken(vpToken: string, criteria: SubjectIdentityBindingMatchCriteria): SubjectIdentityBindingSummary | undefined;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
+
import { IndividualCredentialTypes, W3cCredentialContexts, W3cCredentialTypes, } from '../constants/verifiable-credentials.js';
|
|
3
|
+
import { SubjectIdentityBindingClaims, } from '../models/subject-identity-binding.js';
|
|
4
|
+
import { getVpCredentials } from './vp-token.js';
|
|
5
|
+
function uniqueStrings(value) {
|
|
6
|
+
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
7
|
+
return Array.from(new Set(values.map((item) => String(item || '').trim()).filter(Boolean)));
|
|
8
|
+
}
|
|
9
|
+
function isDidWeb(value) {
|
|
10
|
+
return /^did:web:[^:\s]+(?::[^:\s]+)*$/.test(value);
|
|
11
|
+
}
|
|
12
|
+
function isPhysicalSupportDid(value) {
|
|
13
|
+
const segments = value.toLowerCase().split(':');
|
|
14
|
+
return segments.includes('card') || segments.includes('petd');
|
|
15
|
+
}
|
|
16
|
+
function isIndividualIdentityDid(value) {
|
|
17
|
+
return isDidWeb(value) && !isPhysicalSupportDid(value);
|
|
18
|
+
}
|
|
19
|
+
function includesCredentialType(credential) {
|
|
20
|
+
const types = uniqueStrings(credential?.type);
|
|
21
|
+
return types.includes(IndividualCredentialTypes.SubjectIdentityBindingCredential);
|
|
22
|
+
}
|
|
23
|
+
function normalizeNow(input) {
|
|
24
|
+
if (input instanceof Date)
|
|
25
|
+
return input.getTime();
|
|
26
|
+
if (input)
|
|
27
|
+
return new Date(input).getTime();
|
|
28
|
+
return Date.now();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Builds the unsigned JSON form of a subject identity binding VC.
|
|
32
|
+
*
|
|
33
|
+
* This helper only builds the canonical payload. The issuer must sign it using
|
|
34
|
+
* the normal VC/VP signing flow before a verifier treats it as evidence.
|
|
35
|
+
*/
|
|
36
|
+
export function buildSubjectIdentityBindingCredential(input) {
|
|
37
|
+
const issuerDid = String(input.issuerDid || '').trim();
|
|
38
|
+
const subjectDid = String(input.subjectDid || '').trim();
|
|
39
|
+
const aliasDids = uniqueStrings(input.aliasDids).filter((did) => did !== subjectDid);
|
|
40
|
+
const sectors = uniqueStrings(input.sectors);
|
|
41
|
+
if (!isDidWeb(issuerDid))
|
|
42
|
+
throw new Error('issuerDid must be a did:web identifier.');
|
|
43
|
+
if (!isIndividualIdentityDid(subjectDid)) {
|
|
44
|
+
throw new Error('subjectDid must be a did:web individual identifier, not a physical support DID.');
|
|
45
|
+
}
|
|
46
|
+
if (aliasDids.length === 0 || aliasDids.some((did) => !isIndividualIdentityDid(did))) {
|
|
47
|
+
throw new Error('aliasDids must contain at least one distinct did:web individual identifier and no physical support DID.');
|
|
48
|
+
}
|
|
49
|
+
if (sectors.length === 0)
|
|
50
|
+
throw new Error('sectors must contain at least one sector.');
|
|
51
|
+
if (!String(input.validFrom || '').trim() || Number.isNaN(Date.parse(input.validFrom))) {
|
|
52
|
+
throw new Error('validFrom must be an ISO date-time.');
|
|
53
|
+
}
|
|
54
|
+
if (input.validUntil && Number.isNaN(Date.parse(input.validUntil))) {
|
|
55
|
+
throw new Error('validUntil must be an ISO date-time.');
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
'@context': [W3cCredentialContexts.V2],
|
|
59
|
+
...(input.id ? { id: String(input.id).trim() } : {}),
|
|
60
|
+
type: [
|
|
61
|
+
W3cCredentialTypes.VerifiableCredential,
|
|
62
|
+
IndividualCredentialTypes.SubjectIdentityBindingCredential,
|
|
63
|
+
],
|
|
64
|
+
issuer: issuerDid,
|
|
65
|
+
validFrom: String(input.validFrom).trim(),
|
|
66
|
+
...(input.validUntil ? { validUntil: String(input.validUntil).trim() } : {}),
|
|
67
|
+
credentialSubject: {
|
|
68
|
+
[SubjectIdentityBindingClaims.SubjectId]: subjectDid,
|
|
69
|
+
[SubjectIdentityBindingClaims.SameAs]: aliasDids,
|
|
70
|
+
[SubjectIdentityBindingClaims.Sector]: sectors,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Returns the normalized identity set asserted by one binding VC. */
|
|
75
|
+
export function summarizeSubjectIdentityBinding(credential) {
|
|
76
|
+
if (!credential || typeof credential !== 'object' || !includesCredentialType(credential))
|
|
77
|
+
return undefined;
|
|
78
|
+
const source = credential;
|
|
79
|
+
const issuerDid = String(source.issuer?.id || source.issuer || '').trim();
|
|
80
|
+
const subject = source.credentialSubject || {};
|
|
81
|
+
const subjectDid = String(subject[SubjectIdentityBindingClaims.SubjectId] || '').trim();
|
|
82
|
+
const aliasDids = uniqueStrings(subject[SubjectIdentityBindingClaims.SameAs])
|
|
83
|
+
.filter((did) => did !== subjectDid);
|
|
84
|
+
const sectors = uniqueStrings(subject[SubjectIdentityBindingClaims.Sector]);
|
|
85
|
+
if (!isDidWeb(issuerDid) || !isIndividualIdentityDid(subjectDid) || aliasDids.length === 0)
|
|
86
|
+
return undefined;
|
|
87
|
+
if (aliasDids.some((did) => !isIndividualIdentityDid(did)) || sectors.length === 0)
|
|
88
|
+
return undefined;
|
|
89
|
+
return {
|
|
90
|
+
issuerDid,
|
|
91
|
+
subjectDid,
|
|
92
|
+
aliasDids,
|
|
93
|
+
sectors,
|
|
94
|
+
validFrom: String(source.validFrom || '').trim() || undefined,
|
|
95
|
+
validUntil: String(source.validUntil || '').trim() || undefined,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Checks issuer trust, validity, sector and exact DID membership. */
|
|
99
|
+
export function matchesSubjectIdentityBinding(summary, criteria) {
|
|
100
|
+
if (!summary)
|
|
101
|
+
return false;
|
|
102
|
+
if (!criteria.trustedIssuerDids.includes(summary.issuerDid))
|
|
103
|
+
return false;
|
|
104
|
+
const now = normalizeNow(criteria.now);
|
|
105
|
+
if (Number.isNaN(now))
|
|
106
|
+
return false;
|
|
107
|
+
if (summary.validFrom) {
|
|
108
|
+
const start = Date.parse(summary.validFrom);
|
|
109
|
+
if (Number.isNaN(start) || start > now)
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (summary.validUntil) {
|
|
113
|
+
const end = Date.parse(summary.validUntil);
|
|
114
|
+
if (Number.isNaN(end) || end < now)
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (criteria.sector && !summary.sectors.includes(criteria.sector))
|
|
118
|
+
return false;
|
|
119
|
+
const identities = new Set([summary.subjectDid, ...summary.aliasDids]);
|
|
120
|
+
const required = uniqueStrings(criteria.requiredSubjectDids);
|
|
121
|
+
return required.length > 0 && required.every((did) => identities.has(did));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Finds a matching binding credential in an already-verified VP token.
|
|
125
|
+
*
|
|
126
|
+
* This function decodes and matches claims; it does not verify signatures.
|
|
127
|
+
* Callers must first verify the enclosing VP/VC proof chain.
|
|
128
|
+
*/
|
|
129
|
+
export function getMatchingSubjectIdentityBindingFromVpToken(vpToken, criteria) {
|
|
130
|
+
for (const credential of getVpCredentials(vpToken)) {
|
|
131
|
+
const summary = summarizeSubjectIdentityBinding(credential);
|
|
132
|
+
if (matchesSubjectIdentityBinding(summary, criteria))
|
|
133
|
+
return summary;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|