gdc-common-utils-ts 2.0.17 → 2.1.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 (71) hide show
  1. package/README.md +0 -1
  2. package/dist/constants/verifiable-credentials.d.ts +7 -0
  3. package/dist/constants/verifiable-credentials.js +7 -0
  4. package/dist/examples/bundle-didcomm-payload.d.ts +11 -0
  5. package/dist/examples/bundle-didcomm-payload.js +13 -0
  6. package/dist/examples/communication-attached-bundle-session.js +2 -1
  7. package/dist/examples/communication-didcomm-payload.d.ts +35 -0
  8. package/dist/examples/communication-didcomm-payload.js +38 -0
  9. package/dist/examples/contract-examples.d.ts +1 -0
  10. package/dist/examples/contract-examples.js +1 -0
  11. package/dist/examples/index.d.ts +5 -0
  12. package/dist/examples/index.js +5 -0
  13. package/dist/examples/inter-tenant-access-contract.d.ts +124 -0
  14. package/dist/examples/inter-tenant-access-contract.js +134 -0
  15. package/dist/examples/profile-manager-mem.d.ts +45 -0
  16. package/dist/examples/profile-manager-mem.js +29 -0
  17. package/dist/examples/shared.d.ts +6 -0
  18. package/dist/examples/shared.js +6 -0
  19. package/dist/examples/wallet-mem.d.ts +38 -0
  20. package/dist/examples/wallet-mem.js +40 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/interfaces/IProfileOutboxRepository.d.ts +36 -0
  24. package/dist/interfaces/IProfileOutboxRepository.js +2 -0
  25. package/dist/interfaces/IWallet.d.ts +74 -0
  26. package/dist/interfaces/IWallet.js +2 -0
  27. package/dist/interfaces/IWalletQueue.d.ts +30 -0
  28. package/dist/interfaces/IWalletQueue.js +2 -0
  29. package/dist/interfaces/index.d.ts +8 -0
  30. package/dist/interfaces/index.js +8 -0
  31. package/dist/models/communication-attached-bundle-session.d.ts +193 -0
  32. package/dist/models/communication-attached-bundle-session.js +34 -0
  33. package/dist/models/index.d.ts +4 -0
  34. package/dist/models/index.js +4 -0
  35. package/dist/models/inter-tenant-access-contract.d.ts +105 -0
  36. package/dist/models/inter-tenant-access-contract.js +78 -0
  37. package/dist/models/interoperable-claims/diagnostic-report-claims.d.ts +1 -2
  38. package/dist/models/interoperable-claims/diagnostic-report-claims.js +1 -2
  39. package/dist/models/profile-manager.d.ts +85 -0
  40. package/dist/models/profile-manager.js +2 -0
  41. package/dist/models/wallet.d.ts +59 -0
  42. package/dist/models/wallet.js +22 -0
  43. package/dist/utils/backend-message-manager-mem.d.ts +67 -0
  44. package/dist/utils/backend-message-manager-mem.js +227 -0
  45. package/dist/utils/bundle-didcomm-payload.d.ts +29 -0
  46. package/dist/utils/bundle-didcomm-payload.js +48 -0
  47. package/dist/utils/communication-attached-bundle-session-helpers.d.ts +54 -0
  48. package/dist/utils/communication-attached-bundle-session-helpers.js +517 -0
  49. package/dist/utils/communication-attached-bundle-session.d.ts +59 -302
  50. package/dist/utils/communication-attached-bundle-session.js +114 -839
  51. package/dist/utils/communication-consent-access-editor.d.ts +85 -0
  52. package/dist/utils/communication-consent-access-editor.js +244 -0
  53. package/dist/utils/communication-didcomm-payload.d.ts +33 -0
  54. package/dist/utils/communication-didcomm-payload.js +75 -0
  55. package/dist/utils/evidence-blockchain-references.d.ts +2 -0
  56. package/dist/utils/evidence-blockchain-references.js +4 -0
  57. package/dist/utils/index.d.ts +10 -0
  58. package/dist/utils/index.js +10 -0
  59. package/dist/utils/inter-tenant-access-contract.d.ts +44 -0
  60. package/dist/utils/inter-tenant-access-contract.js +358 -0
  61. package/dist/utils/organization-authorization-urn.d.ts +43 -0
  62. package/dist/utils/organization-authorization-urn.js +87 -0
  63. package/dist/utils/profile-manager-mem.d.ts +69 -0
  64. package/dist/utils/profile-manager-mem.js +79 -0
  65. package/dist/utils/profile-outbox-memory-repository.d.ts +34 -0
  66. package/dist/utils/profile-outbox-memory-repository.js +57 -0
  67. package/dist/utils/wallet-mem.d.ts +93 -0
  68. package/dist/utils/wallet-mem.js +277 -0
  69. package/dist/utils/wallet-memory-queue.d.ts +32 -0
  70. package/dist/utils/wallet-memory-queue.js +82 -0
  71. package/package.json +1 -1
@@ -0,0 +1,54 @@
1
+ import { type HealthcareCanonicalSectionFamily } from '../constants/healthcare';
2
+ import type { BundleEntry, BundleEntryResource, BundleJsonApi } from '../models/bundle';
3
+ import type { CommunicationAttachedBundleSessionMode, ConsentEditorClassifiedActorRole, ConsentEditorClassifiedActors, ConsentEditorClassifiedOrganization, ConsentEditorClassifiedPurpose, ConsentEditorClassifiedRole, ConsentEditorClassifiedRoles, ConsentEditorClassifiedTarget, ConsentViewModel, ConsentEditorScopeCode, ConsentEditorTargetKind } from '../models/communication-attached-bundle-session';
4
+ export declare const CSV_SEPARATOR = ",";
5
+ export declare function ensureEntryResource(entry: BundleEntry, mode: CommunicationAttachedBundleSessionMode): BundleEntryResource;
6
+ export declare function validateBundleLike(bundle: BundleJsonApi<BundleEntry>, mode: CommunicationAttachedBundleSessionMode): void;
7
+ export declare function createEmptyBundle(): BundleJsonApi<BundleEntry>;
8
+ export declare function encodeBundleToBase64(bundle: BundleJsonApi<BundleEntry>): string;
9
+ export declare function cloneBundle(bundle: BundleJsonApi<BundleEntry>): BundleJsonApi<BundleEntry>;
10
+ export declare function cloneEntry(entry: BundleEntry): BundleEntry;
11
+ export declare function cloneUnknownValue<T>(value: T): T;
12
+ export declare function asTrimmedString(value: unknown): string;
13
+ export declare function resolveContainedDocumentsClaimKey(resourceType: string): string;
14
+ export declare function resolveSubjectFromClaims(claims: Record<string, unknown>): string;
15
+ /**
16
+ * Resolves the canonical resource-scoped identifier used for upsert matching.
17
+ */
18
+ export declare function resolveBundleEntryIdentifier(claims: Record<string, unknown>): string;
19
+ /**
20
+ * Resolves the stable entry id value stored in `BundleEntry.id`.
21
+ */
22
+ export declare function resolveBundleEntryCanonicalIdValue(claims: Record<string, unknown>): string;
23
+ export declare function setIfMissing(target: Record<string, unknown>, key: string, value: unknown): void;
24
+ export declare function runtimeUuid(prefix: string): string;
25
+ export declare function splitCsv(value: unknown): string[];
26
+ export declare function normalizeCsvValues(values: readonly unknown[]): string[];
27
+ export declare function buildClassifiedConsentTarget(kind: ConsentEditorTargetKind, code: string, scopeCodes: readonly ConsentEditorScopeCode[], sectionFamily?: HealthcareCanonicalSectionFamily): ConsentEditorClassifiedTarget;
28
+ export declare function buildSectionCatalogOptions(sectionKind: HealthcareCanonicalSectionFamily): ConsentEditorClassifiedTarget[];
29
+ export declare function normalizeScopeCodes(scopeCodes: readonly ConsentEditorScopeCode[]): ConsentEditorScopeCode[];
30
+ export declare function normalizeClassifiedTargets(targets: readonly ConsentEditorClassifiedTarget[]): ConsentEditorClassifiedTarget[];
31
+ export declare function resolveConsentTargetDisplay(kind: ConsentEditorTargetKind, code: string): string | undefined;
32
+ export declare function resolveConsentScopeDisplay(scopeCode: ConsentEditorScopeCode): string;
33
+ export declare function resolveConsentPurposeDisplay(code: string): string;
34
+ export declare function looksLikeEmailToken(value: string): boolean;
35
+ export declare function looksLikePhoneToken(value: string): boolean;
36
+ export declare function looksLikeJurisdictionToken(value: string): boolean;
37
+ export declare function parseDidWebOrganizationToken(value: string): ConsentEditorClassifiedOrganization | undefined;
38
+ export declare function parseConsentActorRole(value: string): ConsentEditorClassifiedActorRole | undefined;
39
+ export declare function flattenClassifiedActors(classifiedActors: ConsentEditorClassifiedActors): string[];
40
+ export declare function serializeClassifiedOrganization(organization: ConsentEditorClassifiedOrganization): string;
41
+ export declare function flattenClassifiedRoles(classifiedRoles: ConsentEditorClassifiedRoles): string[];
42
+ export declare function serializeClassifiedRole(role: ConsentEditorClassifiedRole): string;
43
+ export declare function flattenClassifiedTargets(classifiedTargets: readonly ConsentEditorClassifiedTarget[]): Readonly<{
44
+ coreSections: readonly string[];
45
+ kindOfDocuments: readonly string[];
46
+ typeOfServices: readonly string[];
47
+ subjectMatterDomains: readonly string[];
48
+ resourceTypes: readonly string[];
49
+ }>;
50
+ export declare function classifyConsentTargetsFromClaims(claims: Record<string, unknown>): ConsentEditorClassifiedTarget[];
51
+ export declare function classifyConsentPurposes(purposeCodes: readonly string[]): ConsentEditorClassifiedPurpose[];
52
+ export declare function classifyConsentRoles(roleTokens: readonly string[]): ConsentEditorClassifiedRoles;
53
+ export declare function classifyConsentActors(actorTokens: readonly string[], actorRoleToken?: string): ConsentEditorClassifiedActors;
54
+ export declare function buildConsentViewModel(activeEntry: BundleEntry | null, decision: string, classifiedActors: ConsentEditorClassifiedActors, classifiedRoles: ConsentEditorClassifiedRoles, classifiedPurposes: readonly ConsentEditorClassifiedPurpose[], classifiedTargets: readonly ConsentEditorClassifiedTarget[]): ConsentViewModel;
@@ -0,0 +1,517 @@
1
+ // Copyright 2026 Conectate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
2
+ import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
3
+ import { getHealthcareRoleByClaim, HealthcareRoleFamilies, getHealthcareSectionsByFamily, getHealthcareSectionByCode, getHealthcareSectionFamilyByCode, HealthcareConsentPurposes, HealthcareCanonicalSectionFamilies, } from '../constants/healthcare.js';
4
+ import { ClaimConsent } from '../models/consent-rule.js';
5
+ import { CommunicationClaim } from '../models/interoperable-claims/communication-claims.js';
6
+ import { AppointmentClaim } from '../models/interoperable-claims/appointment-claims.js';
7
+ import { AppointmentResponseClaim } from '../models/interoperable-claims/appointment-response-claims.js';
8
+ import { CarePlanClaim } from '../models/interoperable-claims/care-plan-claims.js';
9
+ import { ClinicalImpressionClaim } from '../models/interoperable-claims/clinical-impression-claims.js';
10
+ import { ConditionClaim } from '../models/interoperable-claims/condition-claims.js';
11
+ import { CompositionClaim } from '../models/interoperable-claims/composition-claims.js';
12
+ import { CoverageClaim } from '../models/interoperable-claims/coverage-claims.js';
13
+ import { DeviceClaim } from '../models/interoperable-claims/device-claims.js';
14
+ import { DeviceUseStatementClaim } from '../models/interoperable-claims/device-use-statement-claims.js';
15
+ import { DiagnosticReportClaim } from '../models/interoperable-claims/diagnostic-report-claims.js';
16
+ import { DocumentReferenceClaim } from '../models/interoperable-claims/document-reference-claims.js';
17
+ import { EncounterClaim } from '../models/interoperable-claims/encounter-claims.js';
18
+ import { FlagClaim } from '../models/interoperable-claims/flag-claims.js';
19
+ import { ImmunizationClaim } from '../models/interoperable-claims/immunization-claims.js';
20
+ import { LocationClaim } from '../models/interoperable-claims/location-claims.js';
21
+ import { MedicationStatementClaim, } from '../models/interoperable-claims/medication-statement-claims.js';
22
+ import { ObservationClaim } from '../models/interoperable-claims/observation-claims.js';
23
+ import { OrganizationClaim } from '../models/interoperable-claims/organization-claims.js';
24
+ import { ProcedureClaim } from '../models/interoperable-claims/procedure-claims.js';
25
+ import { RelatedPersonClaim } from '../models/interoperable-claims/related-person-claims.js';
26
+ import { AllergyIntoleranceClaim } from '../models/interoperable-claims/allergy-intolerance-claims.js';
27
+ import { ConsentEditorScopeCodes, ConsentEditorTargetKinds, } from '../models/communication-attached-bundle-session.js';
28
+ export const CSV_SEPARATOR = ',';
29
+ const DID_WEB_PREFIX = 'did:web:';
30
+ const PHONE_PREFIX = 'tel:';
31
+ export function ensureEntryResource(entry, mode) {
32
+ const resource = entry.resource;
33
+ if (resource && typeof resource === 'object') {
34
+ return resource;
35
+ }
36
+ if (mode === 'normalize') {
37
+ return { meta: { claims: {} } };
38
+ }
39
+ throw new Error('Active entry does not contain a valid resource object.');
40
+ }
41
+ export function validateBundleLike(bundle, mode) {
42
+ const looksLikeBundle = bundle && bundle.resourceType === ResourceTypesFhirR4.Bundle && Array.isArray(bundle.data);
43
+ if (looksLikeBundle || mode === 'normalize') {
44
+ return;
45
+ }
46
+ throw new Error('Decoded attachment is not a valid BundleJsonApi payload.');
47
+ }
48
+ export function createEmptyBundle() {
49
+ return {
50
+ resourceType: ResourceTypesFhirR4.Bundle,
51
+ type: 'batch',
52
+ data: [],
53
+ };
54
+ }
55
+ export function encodeBundleToBase64(bundle) {
56
+ return Buffer.from(JSON.stringify(bundle), 'utf8').toString('base64');
57
+ }
58
+ export function cloneBundle(bundle) {
59
+ return JSON.parse(JSON.stringify(bundle));
60
+ }
61
+ export function cloneEntry(entry) {
62
+ return JSON.parse(JSON.stringify(entry));
63
+ }
64
+ export function cloneUnknownValue(value) {
65
+ if (value === undefined) {
66
+ return value;
67
+ }
68
+ return JSON.parse(JSON.stringify(value));
69
+ }
70
+ export function asTrimmedString(value) {
71
+ if (value === undefined || value === null) {
72
+ return '';
73
+ }
74
+ return String(value).trim();
75
+ }
76
+ export function resolveContainedDocumentsClaimKey(resourceType) {
77
+ if (resourceType === ResourceTypesFhirR4.Consent) {
78
+ return ClaimConsent.containedDocuments;
79
+ }
80
+ if (resourceType === ResourceTypesFhirR4.MedicationStatement) {
81
+ return MedicationStatementClaim.ContainedDocuments;
82
+ }
83
+ if (resourceType === ResourceTypesFhirR4.Condition) {
84
+ return ConditionClaim.ContainedDocuments;
85
+ }
86
+ if (resourceType === ResourceTypesFhirR4.AllergyIntolerance) {
87
+ return AllergyIntoleranceClaim.ContainedDocuments;
88
+ }
89
+ if (resourceType === ResourceTypesFhirR4.DiagnosticReport) {
90
+ return DiagnosticReportClaim.ContainedDocuments;
91
+ }
92
+ return '';
93
+ }
94
+ export function resolveSubjectFromClaims(claims) {
95
+ return asTrimmedString(claims[ClaimConsent.subject]
96
+ || claims[MedicationStatementClaim.Subject]
97
+ || claims[MedicationStatementClaim.Patient]
98
+ || claims[DiagnosticReportClaim.Subject]
99
+ || claims[DiagnosticReportClaim.Patient]
100
+ || claims[CarePlanClaim.Subject]
101
+ || claims[CarePlanClaim.Patient]
102
+ || claims[ConditionClaim.Subject]
103
+ || claims[ProcedureClaim.Subject]
104
+ || claims[ProcedureClaim.Patient]
105
+ || claims[AllergyIntoleranceClaim.Subject]
106
+ || claims[AllergyIntoleranceClaim.Patient]
107
+ || claims[ImmunizationClaim.Subject]
108
+ || claims[ImmunizationClaim.Patient]
109
+ || claims[EncounterClaim.Subject]
110
+ || claims[EncounterClaim.Patient]
111
+ || claims[DocumentReferenceClaim.Subject]
112
+ || claims[ObservationClaim.Subject]
113
+ || claims[ObservationClaim.Patient]
114
+ || claims[FlagClaim.Subject]
115
+ || claims[FlagClaim.Patient]
116
+ || claims[ClinicalImpressionClaim.Subject]
117
+ || claims[DeviceClaim.Patient]
118
+ || claims[DeviceUseStatementClaim.Subject]
119
+ || claims[CoverageClaim.Beneficiary]
120
+ || claims[AppointmentResponseClaim.Patient]
121
+ || claims[CompositionClaim.Subject]
122
+ || claims[RelatedPersonClaim.Patient]);
123
+ }
124
+ /**
125
+ * Resolves the canonical resource-scoped identifier used for upsert matching.
126
+ */
127
+ export function resolveBundleEntryIdentifier(claims) {
128
+ const identifierCandidates = [
129
+ [ResourceTypesFhirR4.Consent, ClaimConsent.identifier],
130
+ [ResourceTypesFhirR4.MedicationStatement, MedicationStatementClaim.Identifier],
131
+ [ResourceTypesFhirR4.DiagnosticReport, DiagnosticReportClaim.Identifier],
132
+ [ResourceTypesFhirR4.DocumentReference, DocumentReferenceClaim.Identifier],
133
+ [ResourceTypesFhirR4.Condition, ConditionClaim.Identifier],
134
+ [ResourceTypesFhirR4.AllergyIntolerance, AllergyIntoleranceClaim.Identifier],
135
+ [ResourceTypesFhirR4.Observation, ObservationClaim.Identifier],
136
+ [ResourceTypesFhirR4.Appointment, AppointmentClaim.Identifier],
137
+ [ResourceTypesFhirR4.AppointmentResponse, AppointmentResponseClaim.Identifier],
138
+ [ResourceTypesFhirR4.CarePlan, CarePlanClaim.Identifier],
139
+ [ResourceTypesFhirR4.Procedure, ProcedureClaim.Identifier],
140
+ [ResourceTypesFhirR4.Immunization, ImmunizationClaim.Identifier],
141
+ [ResourceTypesFhirR4.Encounter, EncounterClaim.Identifier],
142
+ [ResourceTypesFhirR4.Device, DeviceClaim.Identifier],
143
+ [ResourceTypesFhirR4.DeviceUseStatement, DeviceUseStatementClaim.Identifier],
144
+ [ResourceTypesFhirR4.Flag, FlagClaim.Identifier],
145
+ [ResourceTypesFhirR4.ClinicalImpression, ClinicalImpressionClaim.Identifier],
146
+ [ResourceTypesFhirR4.Coverage, CoverageClaim.Identifier],
147
+ [ResourceTypesFhirR4.Composition, CompositionClaim.Identifier],
148
+ [ResourceTypesFhirR4.Location, LocationClaim.Identifier],
149
+ [ResourceTypesFhirR4.Organization, OrganizationClaim.Identifier],
150
+ [ResourceTypesFhirR4.RelatedPerson, RelatedPersonClaim.IdentifierValue],
151
+ [ResourceTypesFhirR4.RelatedPerson, RelatedPersonClaim.Identifier],
152
+ [ResourceTypesFhirR4.Communication, CommunicationClaim.Identifier],
153
+ ];
154
+ for (const [resourceType, claimKey] of identifierCandidates) {
155
+ const identifier = asTrimmedString(claims[claimKey]);
156
+ if (identifier) {
157
+ return `${resourceType}:${identifier}`;
158
+ }
159
+ }
160
+ return '';
161
+ }
162
+ /**
163
+ * Resolves the stable entry id value stored in `BundleEntry.id`.
164
+ */
165
+ export function resolveBundleEntryCanonicalIdValue(claims) {
166
+ const identifierCandidates = [
167
+ ClaimConsent.identifier,
168
+ MedicationStatementClaim.Identifier,
169
+ DiagnosticReportClaim.Identifier,
170
+ DocumentReferenceClaim.Identifier,
171
+ ConditionClaim.Identifier,
172
+ AllergyIntoleranceClaim.Identifier,
173
+ ObservationClaim.Identifier,
174
+ AppointmentClaim.Identifier,
175
+ AppointmentResponseClaim.Identifier,
176
+ CarePlanClaim.Identifier,
177
+ ProcedureClaim.Identifier,
178
+ ImmunizationClaim.Identifier,
179
+ EncounterClaim.Identifier,
180
+ DeviceClaim.Identifier,
181
+ DeviceUseStatementClaim.Identifier,
182
+ FlagClaim.Identifier,
183
+ ClinicalImpressionClaim.Identifier,
184
+ CoverageClaim.Identifier,
185
+ CompositionClaim.Identifier,
186
+ LocationClaim.Identifier,
187
+ OrganizationClaim.Identifier,
188
+ RelatedPersonClaim.IdentifierValue,
189
+ RelatedPersonClaim.Identifier,
190
+ CommunicationClaim.Identifier,
191
+ ];
192
+ for (const claimKey of identifierCandidates) {
193
+ const identifier = asTrimmedString(claims[claimKey]);
194
+ if (identifier) {
195
+ return identifier;
196
+ }
197
+ }
198
+ return '';
199
+ }
200
+ export function setIfMissing(target, key, value) {
201
+ if (target[key] !== undefined) {
202
+ return;
203
+ }
204
+ if (value === undefined || value === null || String(value).trim() === '') {
205
+ return;
206
+ }
207
+ target[key] = value;
208
+ }
209
+ export function runtimeUuid(prefix) {
210
+ const cryptoLike = globalThis;
211
+ if (typeof cryptoLike.crypto?.randomUUID === 'function') {
212
+ return cryptoLike.crypto.randomUUID();
213
+ }
214
+ return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
215
+ }
216
+ export function splitCsv(value) {
217
+ return normalizeCsvValues(String(value || '').split(CSV_SEPARATOR));
218
+ }
219
+ export function normalizeCsvValues(values) {
220
+ return Array.from(new Set(values
221
+ .map((item) => String(item || '').trim())
222
+ .filter(Boolean)));
223
+ }
224
+ export function buildClassifiedConsentTarget(kind, code, scopeCodes, sectionFamily) {
225
+ const normalizedCode = String(code || '').trim();
226
+ return {
227
+ target: {
228
+ kind,
229
+ code: normalizedCode,
230
+ display: resolveConsentTargetDisplay(kind, normalizedCode),
231
+ ...(kind === ConsentEditorTargetKinds.Section
232
+ ? { sectionFamily: sectionFamily || getHealthcareSectionFamilyByCode(normalizedCode) }
233
+ : {}),
234
+ },
235
+ scopes: normalizeScopeCodes(scopeCodes).map((scopeCode) => ({
236
+ code: scopeCode,
237
+ display: resolveConsentScopeDisplay(scopeCode),
238
+ })),
239
+ };
240
+ }
241
+ export function buildSectionCatalogOptions(sectionKind) {
242
+ return Object.values(getHealthcareSectionsByFamily(sectionKind)).map((descriptor) => buildClassifiedConsentTarget(ConsentEditorTargetKinds.Section, descriptor.attributeValue, [ConsentEditorScopeCodes.Read]));
243
+ }
244
+ export function normalizeScopeCodes(scopeCodes) {
245
+ const values = Array.from(new Set(scopeCodes.map((scopeCode) => String(scopeCode).trim()).filter(Boolean)));
246
+ return values.filter((scopeCode) => Object.values(ConsentEditorScopeCodes).includes(scopeCode));
247
+ }
248
+ export function normalizeClassifiedTargets(targets) {
249
+ const result = new Map();
250
+ for (const target of targets) {
251
+ const normalized = buildClassifiedConsentTarget(target.target.kind, String(target.target.code || '').trim(), target.scopes.map((scope) => scope.code), target.target.sectionFamily);
252
+ const key = `${normalized.target.kind}:${normalized.target.code}`;
253
+ result.set(key, normalized);
254
+ }
255
+ return Array.from(result.values());
256
+ }
257
+ export function resolveConsentTargetDisplay(kind, code) {
258
+ if (kind === ConsentEditorTargetKinds.ResourceType) {
259
+ return code;
260
+ }
261
+ if (kind === ConsentEditorTargetKinds.Section) {
262
+ const loincCode = code.includes('|') ? code.split('|').slice(-1)[0] : code;
263
+ return getHealthcareSectionByCode(loincCode)?.titleEn;
264
+ }
265
+ return undefined;
266
+ }
267
+ export function resolveConsentScopeDisplay(scopeCode) {
268
+ if (scopeCode === ConsentEditorScopeCodes.Search)
269
+ return 'search';
270
+ if (scopeCode === ConsentEditorScopeCodes.Read)
271
+ return 'read';
272
+ if (scopeCode === ConsentEditorScopeCodes.Create)
273
+ return 'create';
274
+ if (scopeCode === ConsentEditorScopeCodes.Update)
275
+ return 'update';
276
+ return 'delete';
277
+ }
278
+ export function resolveConsentPurposeDisplay(code) {
279
+ const normalized = String(code || '').trim();
280
+ if (normalized === HealthcareConsentPurposes.Treatment)
281
+ return 'Treatment';
282
+ if (normalized === HealthcareConsentPurposes.EmergencyTreatment)
283
+ return 'Emergency treatment';
284
+ if (normalized === HealthcareConsentPurposes.CareManagement)
285
+ return 'Care management';
286
+ if (normalized === HealthcareConsentPurposes.Operations)
287
+ return 'Operations';
288
+ if (normalized === HealthcareConsentPurposes.PatientAdministration)
289
+ return 'Patient administration';
290
+ if (normalized === HealthcareConsentPurposes.RecordsManagement)
291
+ return 'Records management';
292
+ return normalized;
293
+ }
294
+ export function looksLikeEmailToken(value) {
295
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || '').trim().toLowerCase());
296
+ }
297
+ export function looksLikePhoneToken(value) {
298
+ return String(value || '').trim().startsWith(PHONE_PREFIX);
299
+ }
300
+ export function looksLikeJurisdictionToken(value) {
301
+ return /^[A-Z]{2}([\-:][A-Z0-9]+)?$/.test(String(value || '').trim());
302
+ }
303
+ export function parseDidWebOrganizationToken(value) {
304
+ const normalized = String(value || '').trim();
305
+ if (!normalized.startsWith(DID_WEB_PREFIX)) {
306
+ return undefined;
307
+ }
308
+ const segments = normalized.slice(DID_WEB_PREFIX.length).split(':').filter(Boolean);
309
+ if (segments.length === 0) {
310
+ return undefined;
311
+ }
312
+ const [domain, ...departmentSegments] = segments;
313
+ return {
314
+ domain,
315
+ display: domain,
316
+ departments: departmentSegments.map((segment) => ({
317
+ code: segment,
318
+ display: segment,
319
+ })),
320
+ locations: [],
321
+ };
322
+ }
323
+ export function parseConsentActorRole(value) {
324
+ const normalized = String(value || '').trim();
325
+ if (!normalized) {
326
+ return undefined;
327
+ }
328
+ const healthcareRole = getHealthcareRoleByClaim(normalized);
329
+ if (healthcareRole) {
330
+ return {
331
+ codingSystem: healthcareRole.codingSystem,
332
+ code: healthcareRole.code,
333
+ display: healthcareRole.titleEn,
334
+ };
335
+ }
336
+ const separatorIndex = normalized.indexOf('|');
337
+ if (separatorIndex < 0) {
338
+ return {
339
+ codingSystem: '',
340
+ code: normalized,
341
+ display: normalized,
342
+ };
343
+ }
344
+ return {
345
+ codingSystem: normalized.slice(0, separatorIndex),
346
+ code: normalized.slice(separatorIndex + 1),
347
+ display: normalized,
348
+ };
349
+ }
350
+ export function flattenClassifiedActors(classifiedActors) {
351
+ return normalizeCsvValues([
352
+ ...classifiedActors.jurisdictions.map((jurisdiction) => jurisdiction.code),
353
+ ...classifiedActors.organizations.map(serializeClassifiedOrganization),
354
+ ...classifiedActors.users.flatMap((user) => [user.email, user.phone]),
355
+ ]);
356
+ }
357
+ export function serializeClassifiedOrganization(organization) {
358
+ const domain = String(organization.domain || '').trim();
359
+ const departments = organization.departments
360
+ .map((department) => String(department.code || '').trim())
361
+ .filter(Boolean);
362
+ return [DID_WEB_PREFIX.replace(/:$/, ''), domain, ...departments].join(':');
363
+ }
364
+ export function flattenClassifiedRoles(classifiedRoles) {
365
+ return normalizeCsvValues([
366
+ ...classifiedRoles.professional.map(serializeClassifiedRole),
367
+ ...classifiedRoles.relationship.map(serializeClassifiedRole),
368
+ ...classifiedRoles.legalRepresentative.map(serializeClassifiedRole),
369
+ ...classifiedRoles.other.map(serializeClassifiedRole),
370
+ ]);
371
+ }
372
+ export function serializeClassifiedRole(role) {
373
+ const descriptor = getHealthcareRoleByClaim(`${String(role.codingSystem || '').trim()}|${String(role.code || '').trim()}`);
374
+ if (descriptor) {
375
+ return descriptor.claim;
376
+ }
377
+ return String(role.code || '').trim() || String(role.display || '').trim();
378
+ }
379
+ export function flattenClassifiedTargets(classifiedTargets) {
380
+ const coreSections = [];
381
+ const kindOfDocuments = [];
382
+ const typeOfServices = [];
383
+ const subjectMatterDomains = [];
384
+ const resourceTypes = [];
385
+ for (const entry of normalizeClassifiedTargets(classifiedTargets)) {
386
+ if (entry.target.kind === ConsentEditorTargetKinds.ResourceType) {
387
+ resourceTypes.push(entry.target.code);
388
+ continue;
389
+ }
390
+ const sectionFamily = entry.target.sectionFamily || getHealthcareSectionFamilyByCode(entry.target.code);
391
+ if (sectionFamily === HealthcareCanonicalSectionFamilies.KindOfDocument) {
392
+ kindOfDocuments.push(entry.target.code);
393
+ continue;
394
+ }
395
+ if (sectionFamily === HealthcareCanonicalSectionFamilies.TypeOfService) {
396
+ typeOfServices.push(entry.target.code);
397
+ continue;
398
+ }
399
+ if (sectionFamily === HealthcareCanonicalSectionFamilies.SubjectMatterDomain) {
400
+ subjectMatterDomains.push(entry.target.code);
401
+ continue;
402
+ }
403
+ coreSections.push(entry.target.code);
404
+ }
405
+ return {
406
+ coreSections: normalizeCsvValues(coreSections),
407
+ kindOfDocuments: normalizeCsvValues(kindOfDocuments),
408
+ typeOfServices: normalizeCsvValues(typeOfServices),
409
+ subjectMatterDomains: normalizeCsvValues(subjectMatterDomains),
410
+ resourceTypes: normalizeCsvValues(resourceTypes),
411
+ };
412
+ }
413
+ export function classifyConsentTargetsFromClaims(claims) {
414
+ const actionTargets = splitCsv(claims[ClaimConsent.action]).map((code) => buildClassifiedConsentTarget(ConsentEditorTargetKinds.Section, code, [ConsentEditorScopeCodes.Read]));
415
+ const categoryTargets = splitCsv(claims[ClaimConsent.category]).map((code) => buildClassifiedConsentTarget(ConsentEditorTargetKinds.Section, code, [ConsentEditorScopeCodes.Read], HealthcareCanonicalSectionFamilies.KindOfDocument));
416
+ const resourceTypes = splitCsv(claims[ClaimConsent.resourceType]).map((code) => buildClassifiedConsentTarget(ConsentEditorTargetKinds.ResourceType, code, [ConsentEditorScopeCodes.Read]));
417
+ return normalizeClassifiedTargets([
418
+ ...actionTargets,
419
+ ...categoryTargets,
420
+ ...resourceTypes,
421
+ ]);
422
+ }
423
+ export function classifyConsentPurposes(purposeCodes) {
424
+ return purposeCodes.map((code) => ({
425
+ code,
426
+ display: resolveConsentPurposeDisplay(code),
427
+ }));
428
+ }
429
+ export function classifyConsentRoles(roleTokens) {
430
+ const classified = {
431
+ professional: [],
432
+ relationship: [],
433
+ legalRepresentative: [],
434
+ other: [],
435
+ };
436
+ for (const roleToken of roleTokens) {
437
+ const role = getHealthcareRoleByClaim(roleToken);
438
+ if (!role) {
439
+ classified.other.push({
440
+ codingSystem: '',
441
+ code: roleToken,
442
+ display: roleToken,
443
+ });
444
+ continue;
445
+ }
446
+ const nextRole = {
447
+ kind: role.family,
448
+ codingSystem: role.codingSystem,
449
+ code: role.code,
450
+ display: role.titleEn,
451
+ definition: role.definition,
452
+ };
453
+ if (role.family === HealthcareRoleFamilies.ProfessionalOccupationIsco08) {
454
+ classified.professional.push(nextRole);
455
+ }
456
+ else if (role.family === HealthcareRoleFamilies.PersonalRelationshipHl7) {
457
+ classified.relationship.push(nextRole);
458
+ }
459
+ else if (role.family === HealthcareRoleFamilies.LegalRepresentativeHl7) {
460
+ classified.legalRepresentative.push(nextRole);
461
+ }
462
+ else {
463
+ classified.other.push(nextRole);
464
+ }
465
+ }
466
+ return classified;
467
+ }
468
+ export function classifyConsentActors(actorTokens, actorRoleToken) {
469
+ const actorRole = parseConsentActorRole(actorRoleToken || '');
470
+ const jurisdictions = new Map();
471
+ const organizations = new Map();
472
+ const users = new Map();
473
+ for (const token of actorTokens) {
474
+ if (looksLikeJurisdictionToken(token)) {
475
+ jurisdictions.set(token, { code: token, display: token });
476
+ continue;
477
+ }
478
+ if (looksLikeEmailToken(token)) {
479
+ users.set(`email:${token}`, {
480
+ email: token,
481
+ ...(actorRole ? { role: actorRole } : {}),
482
+ });
483
+ continue;
484
+ }
485
+ if (looksLikePhoneToken(token)) {
486
+ users.set(`phone:${token}`, {
487
+ phone: token,
488
+ ...(actorRole ? { role: actorRole } : {}),
489
+ });
490
+ continue;
491
+ }
492
+ const organization = parseDidWebOrganizationToken(token);
493
+ if (organization) {
494
+ organizations.set(organization.domain, organization);
495
+ }
496
+ }
497
+ return {
498
+ jurisdictions: Array.from(jurisdictions.values()),
499
+ organizations: Array.from(organizations.values()),
500
+ users: Array.from(users.values()),
501
+ };
502
+ }
503
+ export function buildConsentViewModel(activeEntry, decision, classifiedActors, classifiedRoles, classifiedPurposes, classifiedTargets) {
504
+ const claims = {
505
+ ...(activeEntry?.resource?.meta?.claims || {}),
506
+ };
507
+ return {
508
+ ...(activeEntry?.fullUrl ? { fullUrl: activeEntry.fullUrl } : {}),
509
+ identifier: asTrimmedString(claims[ClaimConsent.identifier]),
510
+ subject: asTrimmedString(claims[ClaimConsent.subject]),
511
+ decision,
512
+ classifiedActors,
513
+ classifiedRoles,
514
+ classifiedPurposes,
515
+ classifiedTargets,
516
+ };
517
+ }