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.
Files changed (34) hide show
  1. package/dist/constants/actor-session.d.ts +1 -0
  2. package/dist/constants/actor-session.js +7 -0
  3. package/dist/constants/verifiable-credentials.d.ts +8 -0
  4. package/dist/constants/verifiable-credentials.js +8 -0
  5. package/dist/examples/index.d.ts +1 -0
  6. package/dist/examples/index.js +1 -0
  7. package/dist/examples/professional.js +1 -112
  8. package/dist/examples/shared.d.ts +2 -0
  9. package/dist/examples/shared.js +2 -0
  10. package/dist/examples/subject-identity-binding.d.ts +9 -0
  11. package/dist/examples/subject-identity-binding.js +20 -0
  12. package/dist/models/index.d.ts +1 -0
  13. package/dist/models/index.js +1 -0
  14. package/dist/models/indexing.js +1 -1
  15. package/dist/models/subject-identity-binding.d.ts +22 -0
  16. package/dist/models/subject-identity-binding.js +7 -0
  17. package/dist/utils/bundle-entry-editor.js +2 -2
  18. package/dist/utils/bundle-query.d.ts +17 -0
  19. package/dist/utils/bundle-query.js +9 -2
  20. package/dist/utils/bundle-reader.d.ts +16 -0
  21. package/dist/utils/bundle-reader.js +49 -1
  22. package/dist/utils/client-assertion.d.ts +2 -2
  23. package/dist/utils/clinical-resource-filter-editor.d.ts +39 -0
  24. package/dist/utils/clinical-resource-filter-editor.js +84 -0
  25. package/dist/utils/communication-bundle-document-request.d.ts +9 -8
  26. package/dist/utils/communication-bundle-document-request.js +14 -9
  27. package/dist/utils/communication-fhir-r4.js +24 -17
  28. package/dist/utils/index.d.ts +1 -0
  29. package/dist/utils/index.js +1 -0
  30. package/dist/utils/legal-organization-onboarding-editor.d.ts +13 -0
  31. package/dist/utils/legal-organization-onboarding-editor.js +14 -3
  32. package/dist/utils/subject-identity-binding.d.ts +28 -0
  33. package/dist/utils/subject-identity-binding.js +136 -0
  34. 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
@@ -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';
@@ -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
- physicianByEmailContinuousCareAllergiesAllowed: buildConsentDecisionScenario({
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;
@@ -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
+ }));
@@ -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';
@@ -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';
@@ -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
- if (!this.matchesDateRange(entryDate, filters.dateFrom, filters.dateTo)) {
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
- return this.getDocumentSections().find((section) => section.claim === normalized || section.code === normalized);
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 JsonWebKey } from 'node:crypto';
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: JsonWebKey;
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. Current search flows flatten them
154
- * to `Communication.content-reference`, while future operation flows may attach
155
- * them directly as FHIR `Parameters`.
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 preferred FHIR `Parameters` body for the same semantic summary
165
- * search represented by `createSummaryOperationRequestReferencePath(...)`.
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
- * - `newSearchWithReferencePath(...)` keeps the existing `content-reference`
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. Current search flows flatten them
221
- * to `Communication.content-reference`, while future operation flows may attach
222
- * them directly as FHIR `Parameters`.
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(input.filterSections),
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 preferred FHIR `Parameters` body for the same semantic summary
271
- * search represented by `createSummaryOperationRequestReferencePath(...)`.
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
- * - `newSearchWithReferencePath(...)` keeps the existing `content-reference`
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
- if (payloadKinds > 1) {
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 = buildPayload({
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'] = [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 payload = resource?.payload?.[0];
130
- const contentReference = payload?.contentReference?.reference;
131
- const contentAttachment = payload?.contentAttachment;
132
- const contentCodeableConcept = payload?.contentCodeableConcept?.coding?.[0];
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 buildPayload(input) {
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
- return { contentAttachment: value };
200
+ payloads.push({ contentAttachment: value });
201
+ }
202
+ if (hasCode && payloadCodeRaw) {
203
+ payloads.push({ contentCodeableConcept: { coding: [parseSystemCode(payloadCodeRaw)] } });
193
204
  }
194
- if (hasReference)
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();
@@ -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';
@@ -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: serviceIdentifier || serviceUrl
296
+ organization: organizationDid || serviceUrl
286
297
  ? {
287
- ...(serviceIdentifier ? { did: serviceIdentifier } : {}),
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },