gdc-common-utils-ts 2.1.0 → 2.3.0

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 (175) hide show
  1. package/README.md +22 -2
  2. package/dist/claims/claims-helpers-allergy-intolerance.d.ts +8 -0
  3. package/dist/claims/claims-helpers-allergy-intolerance.js +30 -12
  4. package/dist/claims/claims-helpers-condition.d.ts +8 -0
  5. package/dist/claims/claims-helpers-condition.js +30 -12
  6. package/dist/claims/claims-helpers-consent.d.ts +4 -0
  7. package/dist/claims/claims-helpers-consent.js +15 -1
  8. package/dist/claims/claims-helpers-diagnostic-report.d.ts +4 -0
  9. package/dist/claims/claims-helpers-diagnostic-report.js +17 -2
  10. package/dist/claims/claims-helpers-medication-statement.d.ts +10 -2
  11. package/dist/claims/claims-helpers-medication-statement.js +32 -14
  12. package/dist/constants/data-collections.js +0 -1
  13. package/dist/constants/didcomm.d.ts +22 -0
  14. package/dist/constants/didcomm.js +22 -0
  15. package/dist/constants/fhir-resource-types.d.ts +16 -0
  16. package/dist/constants/fhir-resource-types.js +7 -0
  17. package/dist/constants/healthcare.d.ts +348 -193
  18. package/dist/constants/healthcare.js +99 -37
  19. package/dist/constants/hl7-roles.d.ts +38 -4
  20. package/dist/constants/hl7-roles.js +92 -4
  21. package/dist/constants/index.d.ts +1 -0
  22. package/dist/constants/index.js +1 -0
  23. package/dist/constants/lifecycle.d.ts +2 -0
  24. package/dist/constants/lifecycle.js +2 -0
  25. package/dist/constants/verifiable-credentials.d.ts +9 -0
  26. package/dist/constants/verifiable-credentials.js +9 -0
  27. package/dist/examples/bundle-didcomm-payload.js +4 -2
  28. package/dist/examples/communication-attached-bundle-session.js +27 -27
  29. package/dist/examples/communication-didcomm-payload.d.ts +8 -4
  30. package/dist/examples/communication-didcomm-payload.js +17 -11
  31. package/dist/examples/consent-access.js +2 -1
  32. package/dist/examples/employee.d.ts +21 -0
  33. package/dist/examples/employee.js +21 -0
  34. package/dist/examples/individual-controller.d.ts +27 -0
  35. package/dist/examples/individual-controller.js +28 -1
  36. package/dist/examples/inter-tenant-access-contract.js +3 -2
  37. package/dist/examples/ips-bundle.js +17 -16
  38. package/dist/examples/lifecycle.js +13 -11
  39. package/dist/examples/profile-manager-mem.js +2 -1
  40. package/dist/examples/related-person.d.ts +14 -0
  41. package/dist/examples/related-person.js +18 -4
  42. package/dist/examples/shared.d.ts +12 -3
  43. package/dist/examples/shared.js +13 -4
  44. package/dist/examples/vital-signs.js +1 -1
  45. package/dist/examples/wallet-mem.js +4 -2
  46. package/dist/i18n/role-codes.i18n.d.ts +9 -1
  47. package/dist/i18n/role-codes.i18n.js +12 -1
  48. package/dist/models/authority-resolution.d.ts +56 -0
  49. package/dist/models/authority-resolution.js +2 -0
  50. package/dist/models/bundle-editor-types.d.ts +89 -0
  51. package/dist/models/bundle-editor-types.js +45 -0
  52. package/dist/models/bundle.d.ts +5 -0
  53. package/dist/models/communication-attached-bundle-session.d.ts +6 -4
  54. package/dist/models/communication-attached-bundle-session.js +6 -4
  55. package/dist/models/confidential-storage.d.ts +2 -1
  56. package/dist/models/consent-rule.d.ts +11 -1
  57. package/dist/models/consent-rule.js +9 -1
  58. package/dist/models/device-license.d.ts +20 -0
  59. package/dist/models/index.d.ts +2 -0
  60. package/dist/models/index.js +2 -0
  61. package/dist/models/individual-onboarding.d.ts +18 -0
  62. package/dist/models/interoperable-claims/allergy-intolerance-claims.d.ts +12 -1
  63. package/dist/models/interoperable-claims/allergy-intolerance-claims.js +12 -1
  64. package/dist/models/interoperable-claims/condition-claims.d.ts +12 -1
  65. package/dist/models/interoperable-claims/condition-claims.js +12 -1
  66. package/dist/models/interoperable-claims/diagnostic-report-claims.d.ts +13 -2
  67. package/dist/models/interoperable-claims/diagnostic-report-claims.js +12 -1
  68. package/dist/models/interoperable-claims/document-reference-claims.d.ts +1 -0
  69. package/dist/models/interoperable-claims/document-reference-claims.js +3 -0
  70. package/dist/models/interoperable-claims/medication-statement-claims.d.ts +17 -0
  71. package/dist/models/interoperable-claims/medication-statement-claims.js +17 -0
  72. package/dist/models/interoperable-claims/related-person-claims.d.ts +13 -0
  73. package/dist/models/interoperable-claims/related-person-claims.js +16 -0
  74. package/dist/utils/allergy-intolerance-entry-editor.d.ts +51 -0
  75. package/dist/utils/allergy-intolerance-entry-editor.js +55 -0
  76. package/dist/utils/authority-resolution.d.ts +37 -0
  77. package/dist/utils/authority-resolution.js +112 -0
  78. package/dist/utils/backend-message-manager-mem.d.ts +11 -0
  79. package/dist/utils/backend-message-manager-mem.js +11 -0
  80. package/dist/utils/bundle-didcomm-payload.js +3 -2
  81. package/dist/utils/bundle-document-builder.d.ts +36 -0
  82. package/dist/utils/bundle-document-builder.js +310 -14
  83. package/dist/utils/bundle-editor-core.d.ts +181 -0
  84. package/dist/utils/bundle-editor-core.js +467 -0
  85. package/dist/utils/bundle-editor-helpers.d.ts +53 -0
  86. package/dist/utils/bundle-editor-helpers.js +146 -0
  87. package/dist/utils/bundle-editor-registry.d.ts +6 -0
  88. package/dist/utils/bundle-editor-registry.js +14 -0
  89. package/dist/utils/bundle-editor.d.ts +21 -722
  90. package/dist/utils/bundle-editor.js +21 -1601
  91. package/dist/utils/bundle-entry-editor.d.ts +96 -0
  92. package/dist/utils/bundle-entry-editor.js +305 -0
  93. package/dist/utils/bundle-query.d.ts +1 -0
  94. package/dist/utils/bundle-query.js +12 -3
  95. package/dist/utils/bundle-reader.d.ts +48 -0
  96. package/dist/utils/bundle-reader.js +175 -1
  97. package/dist/utils/care-plan-entry-editor.d.ts +43 -0
  98. package/dist/utils/care-plan-entry-editor.js +47 -0
  99. package/dist/utils/client-assertion.d.ts +38 -0
  100. package/dist/utils/client-assertion.js +113 -0
  101. package/dist/utils/clinical-impression-entry-editor.d.ts +43 -0
  102. package/dist/utils/clinical-impression-entry-editor.js +47 -0
  103. package/dist/utils/clinical-resource-entry-editor.d.ts +123 -0
  104. package/dist/utils/clinical-resource-entry-editor.js +296 -0
  105. package/dist/utils/clinical-resource-view.d.ts +16 -0
  106. package/dist/utils/clinical-resource-view.js +267 -33
  107. package/dist/utils/communication-attached-bundle-session-helpers.js +8 -6
  108. package/dist/utils/communication-attached-bundle-session.d.ts +29 -3
  109. package/dist/utils/communication-attached-bundle-session.js +62 -3
  110. package/dist/utils/communication-consent-access-editor.d.ts +8 -4
  111. package/dist/utils/communication-consent-access-editor.js +8 -4
  112. package/dist/utils/communication-didcomm-payload.d.ts +5 -0
  113. package/dist/utils/communication-didcomm-payload.js +28 -18
  114. package/dist/utils/communication-document-reference.d.ts +23 -0
  115. package/dist/utils/communication-document-reference.js +89 -0
  116. package/dist/utils/communication-editor.d.ts +53 -0
  117. package/dist/utils/communication-editor.js +97 -0
  118. package/dist/utils/condition-entry-editor.d.ts +30 -0
  119. package/dist/utils/condition-entry-editor.js +34 -0
  120. package/dist/utils/consent-claim-helpers.d.ts +4 -0
  121. package/dist/utils/consent-claim-helpers.js +15 -1
  122. package/dist/utils/consent-duplicate-rules.js +1 -1
  123. package/dist/utils/coverage-entry-editor.d.ts +51 -0
  124. package/dist/utils/coverage-entry-editor.js +55 -0
  125. package/dist/utils/device-entry-editor.d.ts +59 -0
  126. package/dist/utils/device-entry-editor.js +63 -0
  127. package/dist/utils/device-use-statement-entry-editor.d.ts +43 -0
  128. package/dist/utils/device-use-statement-entry-editor.js +47 -0
  129. package/dist/utils/diagnostic-report-entry-editor.d.ts +74 -0
  130. package/dist/utils/diagnostic-report-entry-editor.js +136 -0
  131. package/dist/utils/did.d.ts +8 -0
  132. package/dist/utils/did.js +22 -0
  133. package/dist/utils/document-reference-entry-editor.d.ts +62 -0
  134. package/dist/utils/document-reference-entry-editor.js +66 -0
  135. package/dist/utils/employee-entry-editor.d.ts +36 -0
  136. package/dist/utils/employee-entry-editor.js +88 -0
  137. package/dist/utils/employee.d.ts +1 -0
  138. package/dist/utils/employee.js +2 -1
  139. package/dist/utils/encounter-entry-editor.d.ts +51 -0
  140. package/dist/utils/encounter-entry-editor.js +55 -0
  141. package/dist/utils/flag-entry-editor.d.ts +47 -0
  142. package/dist/utils/flag-entry-editor.js +51 -0
  143. package/dist/utils/immunization-entry-editor.d.ts +87 -0
  144. package/dist/utils/immunization-entry-editor.js +169 -0
  145. package/dist/utils/index.d.ts +7 -0
  146. package/dist/utils/index.js +7 -0
  147. package/dist/utils/individual-onboarding-acceptance-credential.d.ts +35 -0
  148. package/dist/utils/individual-onboarding-acceptance-credential.js +55 -0
  149. package/dist/utils/individual-smart.d.ts +59 -0
  150. package/dist/utils/individual-smart.js +135 -0
  151. package/dist/utils/license-list-search.d.ts +5 -0
  152. package/dist/utils/license-list-search.js +9 -0
  153. package/dist/utils/license.d.ts +45 -1
  154. package/dist/utils/license.js +86 -2
  155. package/dist/utils/medication-statement-entry-editor.d.ts +90 -0
  156. package/dist/utils/medication-statement-entry-editor.js +94 -0
  157. package/dist/utils/observation-component-entry-editor.d.ts +57 -0
  158. package/dist/utils/observation-component-entry-editor.js +105 -0
  159. package/dist/utils/observation-entry-editor.d.ts +29 -0
  160. package/dist/utils/observation-entry-editor.js +68 -0
  161. package/dist/utils/procedure-entry-editor.d.ts +71 -0
  162. package/dist/utils/procedure-entry-editor.js +75 -0
  163. package/dist/utils/professional-smart.d.ts +87 -0
  164. package/dist/utils/professional-smart.js +118 -4
  165. package/dist/utils/related-person-list.d.ts +4 -0
  166. package/dist/utils/related-person-list.js +26 -6
  167. package/dist/utils/same-as.d.ts +40 -0
  168. package/dist/utils/same-as.js +72 -0
  169. package/dist/utils/unified-health-id.d.ts +51 -0
  170. package/dist/utils/unified-health-id.js +92 -0
  171. package/dist/utils/vital-sign-day-batch.d.ts +25 -0
  172. package/dist/utils/vital-sign-day-batch.js +115 -0
  173. package/dist/utils/vital-sign-entry-editor.d.ts +61 -0
  174. package/dist/utils/vital-sign-entry-editor.js +152 -0
  175. package/package.json +2 -1
@@ -0,0 +1,112 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { buildOrganizationDidWeb, getBaseUrlFromDidWeb } from './did.js';
3
+ function normalizeString(value) {
4
+ return typeof value === 'string' ? value.trim() : '';
5
+ }
6
+ function encodeDidWebAuthority(authority) {
7
+ return authority.replace(/:/g, '%3A').toLowerCase();
8
+ }
9
+ function normalizeAuthorityHost(value) {
10
+ const raw = normalizeString(value).replace(/\/+$/, '');
11
+ if (!raw)
12
+ return '';
13
+ if (/^https?:\/\//i.test(raw)) {
14
+ try {
15
+ return new URL(raw).host;
16
+ }
17
+ catch {
18
+ return raw.replace(/^https?:\/\//i, '');
19
+ }
20
+ }
21
+ return raw;
22
+ }
23
+ function protocolForAuthority(authority) {
24
+ const normalized = authority.trim().toLowerCase();
25
+ return normalized.startsWith('localhost')
26
+ || normalized.startsWith('127.0.0.1')
27
+ || normalized.startsWith('[::1]')
28
+ ? 'http'
29
+ : 'https';
30
+ }
31
+ function inferMatchedBy(input) {
32
+ if (normalizeString(input.subjectDid))
33
+ return 'subject-did';
34
+ if (normalizeString(input.subjectSameAs))
35
+ return 'subject-same-as';
36
+ if (normalizeString(input.authorityDidWeb))
37
+ return 'authority-did';
38
+ if (normalizeString(input.authorityBaseUrl))
39
+ return 'authority-base-url';
40
+ return 'tenant-context';
41
+ }
42
+ /**
43
+ * Builds one authority/root `did:web` from a bare host authority or URL.
44
+ *
45
+ * Examples:
46
+ * - `https://gw.example.org` -> `did:web:gw.example.org`
47
+ * - `localhost:3300` -> `did:web:localhost%3A3300`
48
+ */
49
+ export function buildAuthorityDidWeb(input) {
50
+ const authority = normalizeAuthorityHost(input.authority);
51
+ if (!authority)
52
+ throw new Error('buildAuthorityDidWeb requires authority.');
53
+ return `did:web:${encodeDidWebAuthority(authority)}`;
54
+ }
55
+ /**
56
+ * Normalizes one authority base URL from either an explicit URL or one
57
+ * authority/root `did:web`.
58
+ */
59
+ export function resolveAuthorityBaseUrl(input) {
60
+ const direct = normalizeString(input.authorityBaseUrl);
61
+ if (direct) {
62
+ const host = normalizeAuthorityHost(direct);
63
+ return host ? `${protocolForAuthority(host)}://${host}/` : undefined;
64
+ }
65
+ const did = normalizeString(input.authorityDidWeb);
66
+ if (!did)
67
+ return undefined;
68
+ return getBaseUrlFromDidWeb(did);
69
+ }
70
+ /**
71
+ * Builds one compatibility authority resolution when the caller only knows:
72
+ * - tenant context
73
+ * - one base URL or one root authority `did:web`
74
+ *
75
+ * This helper intentionally keeps the current hosted-DID fallback in one place
76
+ * so app code does not have to hand-invent `did:web` strings.
77
+ */
78
+ export function buildLegacyAuthorityResolution(input) {
79
+ const authorityDidWeb = normalizeString(input.authorityDidWeb)
80
+ || buildAuthorityDidWeb({ authority: normalizeString(input.authorityBaseUrl) });
81
+ const authorityBaseUrl = resolveAuthorityBaseUrl({
82
+ authorityBaseUrl: input.authorityBaseUrl,
83
+ authorityDidWeb,
84
+ });
85
+ const tenantDidWeb = buildOrganizationDidWeb({
86
+ hostDidWeb: authorityDidWeb,
87
+ tenantId: input.tenantId,
88
+ jurisdiction: input.jurisdiction,
89
+ version: normalizeString(input.version) || 'v1',
90
+ sector: input.sector,
91
+ });
92
+ return {
93
+ authorityDidWeb,
94
+ authorityBaseUrl,
95
+ tenantDidWeb,
96
+ metadataUrl: normalizeString(input.metadataUrl) || undefined,
97
+ source: 'legacy',
98
+ matchedBy: inferMatchedBy(input),
99
+ };
100
+ }
101
+ /**
102
+ * Converts one resolution into a catalog-style record so runtimes can preload
103
+ * legacy fallback data into a shared resolver cache.
104
+ */
105
+ export function authorityResolutionToCatalogRecord(resolution) {
106
+ return {
107
+ authorityDidWeb: resolution.authorityDidWeb,
108
+ authorityBaseUrl: resolution.authorityBaseUrl,
109
+ tenantDidWeb: resolution.tenantDidWeb,
110
+ metadataUrl: resolution.metadataUrl,
111
+ };
112
+ }
@@ -17,6 +17,17 @@ export type BackendMessageManagerMemOptions = Readonly<{
17
17
  *
18
18
  * This is the lowest reusable BFF/proxy slice before actor-specific SDK
19
19
  * facades appear.
20
+ *
21
+ * Normal deployment story:
22
+ * - the injected wallet is the unlocked wallet of the user profile runtime
23
+ * - that same profile wallet usually encrypts outbound messages and decrypts
24
+ * replies
25
+ *
26
+ * Optional deployment story:
27
+ * - one BFF/proxy may also own a separate service wallet for its own
28
+ * envelopes, signatures, confidential storage, or tenant-level messaging
29
+ * - that service wallet is a different actor and must not be confused with
30
+ * the user-profile wallet handled here
20
31
  */
21
32
  export declare class BackendMessageManagerMem {
22
33
  private readonly wallet;
@@ -12,6 +12,17 @@ function clone(value) {
12
12
  *
13
13
  * This is the lowest reusable BFF/proxy slice before actor-specific SDK
14
14
  * facades appear.
15
+ *
16
+ * Normal deployment story:
17
+ * - the injected wallet is the unlocked wallet of the user profile runtime
18
+ * - that same profile wallet usually encrypts outbound messages and decrypts
19
+ * replies
20
+ *
21
+ * Optional deployment story:
22
+ * - one BFF/proxy may also own a separate service wallet for its own
23
+ * envelopes, signatures, confidential storage, or tenant-level messaging
24
+ * - that service wallet is a different actor and must not be confused with
25
+ * the user-profile wallet handled here
15
26
  */
16
27
  export class BackendMessageManagerMem {
17
28
  wallet;
@@ -1,7 +1,8 @@
1
1
  // Copyright 2026 Conectate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
2
+ import { DidcommMessageTypes } from '../constants/didcomm.js';
2
3
  export const BundleDidcommEntryTypes = Object.freeze({
3
- Batch: 'Bundle-batch-request-v1.0',
4
- Document: 'Bundle-document-request-v1.0',
4
+ Batch: DidcommMessageTypes.BundleBatchRequest,
5
+ Document: DidcommMessageTypes.BundleDocumentRequest,
5
6
  });
6
7
  function clone(value) {
7
8
  return JSON.parse(JSON.stringify(value));
@@ -4,6 +4,7 @@ export type BuildBundleDocumentFromClaimsInput = Readonly<{
4
4
  claimsList: readonly BundleDocumentClaims[];
5
5
  subjectDid?: string;
6
6
  compositionType?: string;
7
+ compositionClaims?: BundleDocumentClaims;
7
8
  }>;
8
9
  export type ValidateBundleDocumentResult = Readonly<{
9
10
  ok: boolean;
@@ -13,8 +14,43 @@ export type BundleDocumentResourceFilter = Readonly<{
13
14
  section?: string;
14
15
  resourceType?: string;
15
16
  }>;
17
+ export type PrepareBundleDocumentForSubjectOptions = Readonly<{
18
+ context?: string;
19
+ }>;
16
20
  export declare function getSimpleClaimAttributeName(key: string): string;
17
21
  export declare function extractFlatClaimValue(record: Record<string, any> | undefined, key: string): string;
22
+ /**
23
+ * Validates and prepares an imported FHIR document for one authorized subject.
24
+ *
25
+ * The input is cloned. Every resource receives a deterministic flat
26
+ * `meta.claims` projection. Resources referenced by `Composition.section`
27
+ * receive the canonical section tokens in `Composition.section`, and patient
28
+ * subject references are rebound to the selected `did:web` subject. Existing
29
+ * claims are retained, except subject/patient values are forced to the
30
+ * selected subject so an imported local `Patient/{id}` cannot escape the
31
+ * controller-authorized index scope. The source object is never mutated.
32
+ *
33
+ * This is a conversion primitive only. It neither confirms that a mismatched
34
+ * Patient is the right person nor persists anything; confirmation belongs to
35
+ * the product UI and transport/authorization remain GW concerns.
36
+ */
37
+ export declare function prepareBundleDocumentForSubject(bundle: Record<string, any>, subjectDid: string, options?: PrepareBundleDocumentForSubjectOptions): Record<string, any>;
38
+ /**
39
+ * Returns the Patient Full Name addressed by the document Composition.
40
+ *
41
+ * Resolution prefers the Patient referenced by `Composition.subject`, then
42
+ * falls back to the first Patient for compatibility with incomplete imports.
43
+ * Name selection prefers `use=official`, then `usual`, then the first name;
44
+ * `HumanName.text` wins over assembling prefix/given/family/suffix.
45
+ */
46
+ export declare function getBundleDocumentPatientFullName(bundle: Record<string, any>): string | undefined;
47
+ /**
48
+ * Temporary Full Name comparison key: whitespace folding plus uppercase only.
49
+ * Diacritics and punctuation are intentionally preserved until the shared
50
+ * ICAO transliteration contract exists, so `JOSE` and `JOSÉ` do not compare as
51
+ * equal by accident.
52
+ */
53
+ export declare function normalizeFullNameForComparison(value: unknown): string;
18
54
  export declare function detectClaimsResourceType(claims: BundleDocumentClaims): string | undefined;
19
55
  export declare function convertClaimsToFhirResource(claims: BundleDocumentClaims, version?: 'r4'): FhirResource;
20
56
  /**
@@ -1,4 +1,10 @@
1
1
  import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
2
+ import { AllergyIntoleranceClaim } from '../models/interoperable-claims/allergy-intolerance-claims.js';
3
+ import { CompositionClaim } from '../models/interoperable-claims/composition-claims.js';
4
+ import { ConditionClaim } from '../models/interoperable-claims/condition-claims.js';
5
+ import { DiagnosticReportClaim } from '../models/interoperable-claims/diagnostic-report-claims.js';
6
+ import { MedicationStatementClaim } from '../models/interoperable-claims/medication-statement-claims.js';
7
+ import { ClaimConsent } from '../models/consent-rule.js';
2
8
  import { allergyIntoleranceFlatToFhirR4, appointmentFlatToFhirR4, carePlanFlatToFhirR4, clinicalImpressionFlatToFhirR4, compositionFlatToFhirR4, conditionFlatToFhirR4, convertFhirResourceToClaims, coverageFlatToFhirR4, deviceFlatToFhirR4, documentReferenceFlatToFhirR4, diagnosticReportFlatToFhirR4, encounterFlatToFhirR4, flagFlatToFhirR4, flatClaimsToFhirResource, immunizationFlatToFhirR4, locationFlatToFhirR4, medicationStatementFlatToFhirR4, observationFromFlatToFhirR4, organizationFlatToFhirR4, procedureFlatToFhirR4, relatedPersonFlatToFhirR4, } from './clinical-resource-converters.js';
3
9
  function asTrimmedString(value) {
4
10
  if (value === undefined || value === null)
@@ -93,6 +99,219 @@ function normalizeSectionCode(value) {
93
99
  return { code: system };
94
100
  return { system, code };
95
101
  }
102
+ function toCanonicalSectionToken(system, code) {
103
+ const normalizedSystem = asTrimmedString(system);
104
+ const normalizedCode = asTrimmedString(code);
105
+ if (!normalizedCode)
106
+ return '';
107
+ if (!normalizedSystem)
108
+ return normalizedCode;
109
+ return `${normalizedSystem === 'http://loinc.org' ? 'LOINC' : normalizedSystem}|${normalizedCode}`;
110
+ }
111
+ function resourceReferenceAliases(entry) {
112
+ const resource = entry?.resource;
113
+ const resourceType = asTrimmedString(resource?.resourceType);
114
+ const resourceId = asTrimmedString(resource?.id);
115
+ const fullUrl = asTrimmedString(entry?.fullUrl);
116
+ return Array.from(new Set([
117
+ fullUrl,
118
+ resourceType && resourceId ? `${resourceType}/${resourceId}` : '',
119
+ resourceId,
120
+ fullUrl ? fullUrl.split('/').slice(-2).join('/') : '',
121
+ ].filter(Boolean)));
122
+ }
123
+ function sectionMembershipByReference(composition) {
124
+ const memberships = new Map();
125
+ const visit = (sections) => {
126
+ if (!Array.isArray(sections))
127
+ return;
128
+ for (const section of sections) {
129
+ const coding = Array.isArray(section?.code?.coding) ? section.code.coding[0] : undefined;
130
+ const token = toCanonicalSectionToken(coding?.system, coding?.code);
131
+ if (token && Array.isArray(section?.entry)) {
132
+ for (const item of section.entry) {
133
+ const reference = asTrimmedString(item?.reference);
134
+ if (!reference)
135
+ continue;
136
+ const aliases = [reference, reference.split('/').slice(-2).join('/'), reference.split('/').pop() || ''];
137
+ for (const alias of aliases.filter(Boolean)) {
138
+ memberships.set(alias, Array.from(new Set([...(memberships.get(alias) || []), token])));
139
+ }
140
+ }
141
+ }
142
+ visit(section?.section);
143
+ }
144
+ };
145
+ visit(composition?.section);
146
+ return memberships;
147
+ }
148
+ function allCompositionSectionTokens(composition) {
149
+ const tokens = [];
150
+ const visit = (sections) => {
151
+ if (!Array.isArray(sections))
152
+ return;
153
+ for (const section of sections) {
154
+ const coding = Array.isArray(section?.code?.coding) ? section.code.coding[0] : undefined;
155
+ const token = toCanonicalSectionToken(coding?.system, coding?.code);
156
+ if (token)
157
+ tokens.push(token);
158
+ visit(section?.section);
159
+ }
160
+ };
161
+ visit(composition?.section);
162
+ return Array.from(new Set(tokens));
163
+ }
164
+ function replacePatientSubjectReference(resource, subjectDid) {
165
+ const resourceType = asTrimmedString(resource?.resourceType);
166
+ if (resourceType === ResourceTypesFhirR4.Composition || resource?.subject?.reference) {
167
+ resource.subject = { ...(resource.subject || {}), reference: subjectDid };
168
+ }
169
+ if (resource?.patient?.reference) {
170
+ resource.patient = { ...(resource.patient || {}), reference: subjectDid };
171
+ }
172
+ }
173
+ /**
174
+ * Validates and prepares an imported FHIR document for one authorized subject.
175
+ *
176
+ * The input is cloned. Every resource receives a deterministic flat
177
+ * `meta.claims` projection. Resources referenced by `Composition.section`
178
+ * receive the canonical section tokens in `Composition.section`, and patient
179
+ * subject references are rebound to the selected `did:web` subject. Existing
180
+ * claims are retained, except subject/patient values are forced to the
181
+ * selected subject so an imported local `Patient/{id}` cannot escape the
182
+ * controller-authorized index scope. The source object is never mutated.
183
+ *
184
+ * This is a conversion primitive only. It neither confirms that a mismatched
185
+ * Patient is the right person nor persists anything; confirmation belongs to
186
+ * the product UI and transport/authorization remain GW concerns.
187
+ */
188
+ export function prepareBundleDocumentForSubject(bundle, subjectDid, options = {}) {
189
+ const validation = validateBundleDocumentBasic(bundle);
190
+ if (!validation.ok)
191
+ throw new Error(validation.issues.join(' '));
192
+ const normalizedSubjectDid = asTrimmedString(subjectDid);
193
+ if (!normalizedSubjectDid)
194
+ throw new Error('A subject DID is required.');
195
+ const prepared = JSON.parse(JSON.stringify(bundle));
196
+ const entries = prepared.entry;
197
+ const composition = entries[0]?.resource;
198
+ const membership = sectionMembershipByReference(composition);
199
+ const allSections = allCompositionSectionTokens(composition);
200
+ for (const entry of entries) {
201
+ const resource = entry?.resource;
202
+ if (!resource || typeof resource !== 'object')
203
+ continue;
204
+ replacePatientSubjectReference(resource, normalizedSubjectDid);
205
+ const generated = convertFhirResourceToClaims(resource, options.context || 'org.hl7.fhir.r4');
206
+ const existing = resource?.meta?.claims;
207
+ const claims = {
208
+ ...generated,
209
+ ...(existing && typeof existing === 'object' && !Array.isArray(existing) ? existing : {}),
210
+ };
211
+ const resourceType = asTrimmedString(resource.resourceType);
212
+ if (resourceType !== 'Patient') {
213
+ if (resource?.subject?.reference)
214
+ claims[`${resourceType}.subject`] = normalizedSubjectDid;
215
+ if (resource?.patient?.reference)
216
+ claims[`${resourceType}.patient`] = normalizedSubjectDid;
217
+ }
218
+ const resourceSections = resourceType === ResourceTypesFhirR4.Composition
219
+ ? allSections
220
+ : resourceReferenceAliases(entry).flatMap((alias) => membership.get(alias) || []);
221
+ if (resourceSections.length > 0) {
222
+ claims[CompositionClaim.Section] = Array.from(new Set(resourceSections)).join(',');
223
+ }
224
+ resource.meta = {
225
+ ...(resource.meta && typeof resource.meta === 'object' ? resource.meta : {}),
226
+ claims,
227
+ };
228
+ }
229
+ return prepared;
230
+ }
231
+ /**
232
+ * Returns the Patient Full Name addressed by the document Composition.
233
+ *
234
+ * Resolution prefers the Patient referenced by `Composition.subject`, then
235
+ * falls back to the first Patient for compatibility with incomplete imports.
236
+ * Name selection prefers `use=official`, then `usual`, then the first name;
237
+ * `HumanName.text` wins over assembling prefix/given/family/suffix.
238
+ */
239
+ export function getBundleDocumentPatientFullName(bundle) {
240
+ const entries = Array.isArray(bundle?.entry) ? bundle.entry : [];
241
+ const composition = entries[0]?.resource;
242
+ const subjectReference = asTrimmedString(composition?.subject?.reference);
243
+ const patientEntry = entries.find((entry) => {
244
+ if (entry?.resource?.resourceType !== 'Patient')
245
+ return false;
246
+ if (!subjectReference)
247
+ return true;
248
+ return resourceReferenceAliases(entry).some((alias) => alias === subjectReference
249
+ || alias === subjectReference.split('/').slice(-2).join('/')
250
+ || alias === subjectReference.split('/').pop());
251
+ }) || entries.find((entry) => entry?.resource?.resourceType === 'Patient');
252
+ const names = Array.isArray(patientEntry?.resource?.name) ? patientEntry.resource.name : [];
253
+ const name = names.find((item) => item?.use === 'official')
254
+ || names.find((item) => item?.use === 'usual')
255
+ || names[0];
256
+ if (!name)
257
+ return undefined;
258
+ const text = asTrimmedString(name.text);
259
+ if (text)
260
+ return text;
261
+ const parts = [
262
+ ...(Array.isArray(name.prefix) ? name.prefix : []),
263
+ ...(Array.isArray(name.given) ? name.given : []),
264
+ name.family,
265
+ ...(Array.isArray(name.suffix) ? name.suffix : []),
266
+ ].map(asTrimmedString).filter(Boolean);
267
+ return parts.length > 0 ? parts.join(' ') : undefined;
268
+ }
269
+ /**
270
+ * Temporary Full Name comparison key: whitespace folding plus uppercase only.
271
+ * Diacritics and punctuation are intentionally preserved until the shared
272
+ * ICAO transliteration contract exists, so `JOSE` and `JOSÉ` do not compare as
273
+ * equal by accident.
274
+ */
275
+ export function normalizeFullNameForComparison(value) {
276
+ return asTrimmedString(value).replace(/\s+/g, ' ').toUpperCase();
277
+ }
278
+ function resolveContainedReferenceListClaimKey(resourceType) {
279
+ switch (resourceType) {
280
+ case ResourceTypesFhirR4.MedicationStatement:
281
+ return MedicationStatementClaim.ContainedReferenceList;
282
+ case ResourceTypesFhirR4.AllergyIntolerance:
283
+ return AllergyIntoleranceClaim.ContainedReferenceList;
284
+ case ResourceTypesFhirR4.Condition:
285
+ return ConditionClaim.ContainedReferenceList;
286
+ case ResourceTypesFhirR4.DiagnosticReport:
287
+ return DiagnosticReportClaim.ContainedReferenceList;
288
+ case ResourceTypesFhirR4.Consent:
289
+ return ClaimConsent.containedReferenceList;
290
+ default:
291
+ return undefined;
292
+ }
293
+ }
294
+ function resolveContainedFlagClaimKey(resourceType) {
295
+ const normalized = asTrimmedString(resourceType);
296
+ return normalized ? `${normalized}.is-contained` : undefined;
297
+ }
298
+ function resolveContainedParentReferenceClaimKey(resourceType) {
299
+ const normalized = asTrimmedString(resourceType);
300
+ return normalized ? `${normalized}.contained-parent-reference` : undefined;
301
+ }
302
+ function mergeContainedResourceReferenceList(claims, references) {
303
+ const resourceType = detectClaimsResourceType(claims);
304
+ const claimKey = resourceType ? resolveContainedReferenceListClaimKey(resourceType) : undefined;
305
+ if (!claimKey || references.length === 0) {
306
+ return claims;
307
+ }
308
+ const current = splitCsv(claims[claimKey]);
309
+ const next = Array.from(new Set([...current, ...references].map((item) => asTrimmedString(item)).filter(Boolean)));
310
+ return {
311
+ ...claims,
312
+ [claimKey]: next.join(','),
313
+ };
314
+ }
96
315
  export function detectClaimsResourceType(claims) {
97
316
  const keys = Object.keys(claims || {});
98
317
  if (keys.some((key) => key.startsWith('MedicationStatement.')))
@@ -181,12 +400,47 @@ export function extractBundleDocumentClaimsList(bundle, context = 'org.hl7.fhir.
181
400
  .map((entry) => entry?.resource)
182
401
  .filter((resource) => resource && typeof resource === 'object')
183
402
  .filter((resource) => !ignoredResourceTypes.has(String(resource.resourceType || '')))
184
- .map((resource) => {
185
- const metaClaims = resource?.meta?.claims;
186
- if (metaClaims && typeof metaClaims === 'object' && !Array.isArray(metaClaims)) {
187
- return ensureClaimsIdentifier({ ...metaClaims }, resource);
188
- }
189
- return ensureClaimsIdentifier(convertFhirResourceToClaims(resource, context), resource);
403
+ .flatMap((resource) => {
404
+ const resourceRecord = resource;
405
+ const resourceReference = `${asTrimmedString(resourceRecord.resourceType)}/${asTrimmedString(resourceRecord.id)}`;
406
+ const metaClaims = resourceRecord?.meta?.claims;
407
+ const baseClaims = metaClaims && typeof metaClaims === 'object' && !Array.isArray(metaClaims)
408
+ ? ensureClaimsIdentifier({ ...metaClaims }, resourceRecord)
409
+ : ensureClaimsIdentifier(convertFhirResourceToClaims(resourceRecord, context), resourceRecord);
410
+ const containedResources = Array.isArray(resourceRecord.contained)
411
+ ? resourceRecord.contained
412
+ : [];
413
+ const containedClaimsList = containedResources.map((containedResource, index) => {
414
+ const containedMetaClaims = (containedResource?.meta?.claims);
415
+ const containedClaims = containedMetaClaims && typeof containedMetaClaims === 'object' && !Array.isArray(containedMetaClaims)
416
+ ? ensureClaimsIdentifier({ ...containedMetaClaims }, containedResource)
417
+ : ensureClaimsIdentifier(convertFhirResourceToClaims(containedResource, context), containedResource);
418
+ const containedResourceType = asTrimmedString(containedResource.resourceType);
419
+ const containedFlagClaimKey = resolveContainedFlagClaimKey(containedResourceType);
420
+ const containedParentReferenceClaimKey = resolveContainedParentReferenceClaimKey(containedResourceType);
421
+ return {
422
+ ...containedClaims,
423
+ ...(containedFlagClaimKey ? { [containedFlagClaimKey]: true } : {}),
424
+ ...(containedParentReferenceClaimKey ? { [containedParentReferenceClaimKey]: resourceReference } : {}),
425
+ ...(asTrimmedString(containedResource.id) ? {} : {
426
+ [`${containedResource.resourceType}.identifier`]: `${resourceReference}#contained-${index + 1}`,
427
+ }),
428
+ };
429
+ });
430
+ const containedReferences = containedResources
431
+ .map((containedResource, index) => {
432
+ const containedType = asTrimmedString(containedResource.resourceType);
433
+ const containedId = asTrimmedString(containedResource.id) || `${resourceReference}#contained-${index + 1}`;
434
+ if (!containedType || !containedId) {
435
+ return '';
436
+ }
437
+ return `${containedType}/${containedId}`;
438
+ })
439
+ .filter(Boolean);
440
+ return [
441
+ mergeContainedResourceReferenceList(baseClaims, containedReferences),
442
+ ...containedClaimsList,
443
+ ];
190
444
  });
191
445
  }
192
446
  export function resolveClaimsSectionList(claims) {
@@ -202,9 +456,17 @@ export function resolveClaimsSectionList(claims) {
202
456
  return Array.from(new Set(values));
203
457
  }
204
458
  export function buildBundleDocumentFromClaims(input) {
205
- const compositionType = asTrimmedString(input.compositionType) || 'http://loinc.org|60591-5';
459
+ const compositionClaims = { ...(input.compositionClaims || {}) };
460
+ const compositionType = asTrimmedString(input.compositionType || compositionClaims[CompositionClaim.Type]) || 'http://loinc.org|60591-5';
461
+ const compositionSubject = asTrimmedString(input.subjectDid || compositionClaims[CompositionClaim.Subject]);
462
+ const compositionIdentifier = asTrimmedString(compositionClaims[CompositionClaim.Identifier]);
463
+ const compositionTitle = asTrimmedString(compositionClaims[CompositionClaim.Title]);
464
+ const compositionDate = asTrimmedString(compositionClaims[CompositionClaim.Date]);
465
+ const compositionAuthorList = splitCsv(compositionClaims[CompositionClaim.Author]);
206
466
  const compositionSections = new Map();
207
- const entries = [];
467
+ const visibleEntries = [];
468
+ const resourceByReference = new Map();
469
+ const containedChildrenByParentReference = new Map();
208
470
  input.claimsList.forEach((claims, index) => {
209
471
  const resource = convertClaimsToFhirResource(claims);
210
472
  resource.meta = {
@@ -212,8 +474,23 @@ export function buildBundleDocumentFromClaims(input) {
212
474
  claims: { ...claims },
213
475
  };
214
476
  ensureResourceIdentifier(resource, claims, index);
215
- entries.push({ resource });
216
- const resourceRef = `${resource.resourceType}/${resource.id}`;
477
+ const resourceReference = `${resource.resourceType}/${asTrimmedString(resource.id)}`;
478
+ const containedFlagClaimKey = resolveContainedFlagClaimKey(resource.resourceType);
479
+ const containedParentReferenceClaimKey = resolveContainedParentReferenceClaimKey(resource.resourceType);
480
+ const isContained = containedFlagClaimKey
481
+ ? (claims[containedFlagClaimKey] === true || claims[containedFlagClaimKey] === 'true')
482
+ : false;
483
+ const containedParentReference = containedParentReferenceClaimKey
484
+ ? asTrimmedString(claims[containedParentReferenceClaimKey])
485
+ : '';
486
+ if (isContained && containedParentReference) {
487
+ const currentChildren = containedChildrenByParentReference.get(containedParentReference) || [];
488
+ containedChildrenByParentReference.set(containedParentReference, [...currentChildren, resource]);
489
+ return;
490
+ }
491
+ visibleEntries.push({ resource });
492
+ resourceByReference.set(resourceReference, resource);
493
+ const resourceRef = resourceReference;
217
494
  resolveClaimsSectionList(claims).forEach((sectionValue) => {
218
495
  const normalizedSection = asTrimmedString(sectionValue);
219
496
  if (!normalizedSection)
@@ -230,6 +507,13 @@ export function buildBundleDocumentFromClaims(input) {
230
507
  compositionSections.set(normalizedSection, section);
231
508
  });
232
509
  });
510
+ containedChildrenByParentReference.forEach((children, parentReference) => {
511
+ const parent = resourceByReference.get(parentReference);
512
+ if (!parent) {
513
+ return;
514
+ }
515
+ parent.contained = [...(Array.isArray(parent.contained) ? parent.contained : []), ...children];
516
+ });
233
517
  const [compositionSystem, compositionCode] = compositionType.split('|');
234
518
  return {
235
519
  resourceType: ResourceTypesFhirR4.Bundle,
@@ -238,8 +522,14 @@ export function buildBundleDocumentFromClaims(input) {
238
522
  {
239
523
  resource: {
240
524
  resourceType: ResourceTypesFhirR4.Composition,
525
+ ...(compositionIdentifier ? { id: compositionIdentifier } : {}),
241
526
  status: 'final',
242
- subject: input.subjectDid ? { reference: input.subjectDid } : undefined,
527
+ ...(compositionIdentifier ? {
528
+ identifier: [{ value: compositionIdentifier }],
529
+ } : {}),
530
+ ...(compositionSubject ? {
531
+ subject: { reference: compositionSubject },
532
+ } : {}),
243
533
  type: {
244
534
  coding: [
245
535
  compositionCode
@@ -247,18 +537,24 @@ export function buildBundleDocumentFromClaims(input) {
247
537
  : { code: compositionSystem },
248
538
  ],
249
539
  },
540
+ ...(compositionTitle ? { title: compositionTitle } : {}),
541
+ ...(compositionDate ? { date: compositionDate } : {}),
542
+ ...(compositionAuthorList.length > 0 ? {
543
+ author: compositionAuthorList.map((reference) => ({ reference })),
544
+ } : {}),
545
+ meta: { claims: compositionClaims },
250
546
  section: Array.from(compositionSections.values()),
251
547
  },
252
548
  },
253
- ...(input.subjectDid
549
+ ...(compositionSubject
254
550
  ? [{
255
551
  resource: {
256
552
  resourceType: 'Patient',
257
- id: input.subjectDid,
553
+ id: compositionSubject,
258
554
  },
259
555
  }]
260
556
  : []),
261
- ...entries,
557
+ ...visibleEntries,
262
558
  ],
263
559
  };
264
560
  }