gdc-common-utils-ts 2.3.6 → 2.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,10 @@ boundaries used in `gdc-common-utils-ts`.
59
59
  - Only define custom names when no canonical FHIR SearchParameter exists.
60
60
  - `resource.meta.claims` is the canonical project-specific claims container and must be preserved across conversions/transports.
61
61
  - `resource.meta.claims` is not part of base FHIR; it is a claims-first extension carried by FHIR-like resources in GDC contracts.
62
+ - Native FHIR resources received from EHR systems do not need that extension:
63
+ normalize them with `normalizeClaimsFromFhirResource(...)` at the processing
64
+ boundary before indexed storage. Existing `resource.meta.claims` take
65
+ precedence so SDK-authored semantics survive transport conversion.
62
66
 
63
67
  ## Identity Continuity
64
68
 
@@ -1724,6 +1724,8 @@ export declare const HealthcareActorRoles: Readonly<{
1724
1724
  readonly MidwiferyProfessional: "ISCO-08|2222";
1725
1725
  readonly Paramedic: "ISCO-08|2240";
1726
1726
  readonly Veterinarian: "ISCO-08|2250";
1727
+ /** Administrative health occupation; it does not imply clinical authority. */
1728
+ readonly MedicalSecretary: "ISCO-08|3344";
1727
1729
  }>;
1728
1730
  export declare const HealthcareActorRoleCodes: Readonly<{
1729
1731
  readonly Controller: "1120";
@@ -1744,6 +1746,7 @@ export declare const HealthcareActorRoleCodes: Readonly<{
1744
1746
  readonly MidwiferyProfessional: "2222";
1745
1747
  readonly Paramedic: "2240";
1746
1748
  readonly Veterinarian: "2250";
1749
+ readonly MedicalSecretary: "3344";
1747
1750
  }>;
1748
1751
  export declare const HealthcareRolesByFamily: Readonly<{
1749
1752
  readonly professionalOccupationIsco08: Readonly<Record<string, Readonly<{
@@ -1776,15 +1779,15 @@ export declare const HealthcareRolesByFamily: Readonly<{
1776
1779
  }>;
1777
1780
  export declare const HealthcareAllRolesByClaim: Readonly<Record<string, HealthcareActorRoleDescriptor>>;
1778
1781
  export declare const HealthcareProfessionalRoleCodesBySector: Readonly<{
1779
- readonly "health-care": readonly ("1120" | "221" | "2211" | "2212" | "2221" | "2222" | "2240")[];
1780
- readonly "health-research": readonly ("1120" | "221" | "2211" | "2212")[];
1782
+ readonly "health-care": readonly ("1120" | "221" | "2211" | "2212" | "2221" | "2222" | "2240" | "3344")[];
1783
+ readonly "health-research": readonly ("1120" | "221" | "2211" | "2212" | "3344")[];
1781
1784
  readonly "health-tech": readonly "1120"[];
1782
1785
  readonly "health-insurance": readonly "1120"[];
1783
1786
  readonly "animal-care": readonly ("1120" | "2250")[];
1784
1787
  readonly "animal-research": readonly ("1120" | "2250")[];
1785
1788
  readonly "animal-insurance": readonly "1120"[];
1786
1789
  readonly "animal-tech": readonly "1120"[];
1787
- readonly "onehealth-research": readonly ("1120" | "221" | "2211" | "2212" | "2250")[];
1790
+ readonly "onehealth-research": readonly ("1120" | "221" | "2211" | "2212" | "2250" | "3344")[];
1788
1791
  readonly "onehealth-tech": readonly "1120"[];
1789
1792
  }>;
1790
1793
  export declare const HealthcareProfessionalRolesBySector: Readonly<{
@@ -356,6 +356,8 @@ export const HealthcareActorRoles = Object.freeze({
356
356
  MidwiferyProfessional: 'ISCO-08|2222',
357
357
  Paramedic: 'ISCO-08|2240',
358
358
  Veterinarian: 'ISCO-08|2250',
359
+ /** Administrative health occupation; it does not imply clinical authority. */
360
+ MedicalSecretary: 'ISCO-08|3344',
359
361
  });
360
362
  export const HealthcareActorRoleCodes = Object.freeze({
361
363
  Controller: '1120',
@@ -376,6 +378,7 @@ export const HealthcareActorRoleCodes = Object.freeze({
376
378
  MidwiferyProfessional: '2222',
377
379
  Paramedic: '2240',
378
380
  Veterinarian: '2250',
381
+ MedicalSecretary: '3344',
379
382
  });
380
383
  function buildProfessionalIscoRoles() {
381
384
  const entries = [
@@ -467,6 +470,17 @@ function buildProfessionalIscoRoles() {
467
470
  titleEn: 'Veterinarian',
468
471
  }),
469
472
  ],
473
+ [
474
+ HealthcareActorRoleCodes.MedicalSecretary,
475
+ Object.freeze({
476
+ family: HealthcareRoleFamilies.ProfessionalOccupationIsco08,
477
+ codingSystem: ISCO08_CODING_SYSTEM,
478
+ code: HealthcareActorRoleCodes.MedicalSecretary,
479
+ claim: HealthcareActorRoles.MedicalSecretary,
480
+ i18nKey: `${ISCO08_I18N_NAMESPACE}.${HealthcareActorRoleCodes.MedicalSecretary}`,
481
+ titleEn: 'Medical secretary',
482
+ }),
483
+ ],
470
484
  ];
471
485
  return Object.freeze(Object.fromEntries(entries));
472
486
  }
@@ -523,12 +537,14 @@ export const HealthcareProfessionalRoleCodesBySector = Object.freeze({
523
537
  HealthcareActorRoleCodes.NursingProfessional,
524
538
  HealthcareActorRoleCodes.MidwiferyProfessional,
525
539
  HealthcareActorRoleCodes.Paramedic,
540
+ HealthcareActorRoleCodes.MedicalSecretary,
526
541
  ]),
527
542
  [DataspaceSectors.HealthResearch]: Object.freeze([
528
543
  HealthcareActorRoleCodes.Controller,
529
544
  HealthcareActorRoleCodes.MedicalDoctors,
530
545
  HealthcareActorRoleCodes.GeneralistMedicalPractitioner,
531
546
  HealthcareActorRoleCodes.SpecialistMedicalPractitioner,
547
+ HealthcareActorRoleCodes.MedicalSecretary,
532
548
  ]),
533
549
  [DataspaceSectors.HealthTech]: Object.freeze([HealthcareActorRoleCodes.Controller]),
534
550
  [DataspaceSectors.HealthInsurance]: Object.freeze([HealthcareActorRoleCodes.Controller]),
@@ -548,6 +564,7 @@ export const HealthcareProfessionalRoleCodesBySector = Object.freeze({
548
564
  HealthcareActorRoleCodes.GeneralistMedicalPractitioner,
549
565
  HealthcareActorRoleCodes.SpecialistMedicalPractitioner,
550
566
  HealthcareActorRoleCodes.Veterinarian,
567
+ HealthcareActorRoleCodes.MedicalSecretary,
551
568
  ]),
552
569
  [DataspaceSectors.OneHealthTech]: Object.freeze([HealthcareActorRoleCodes.Controller]),
553
570
  });
@@ -17,6 +17,9 @@ export function allergyIntoleranceFlatToFhirR4(claims) {
17
17
  code: claims[AllergyIntoleranceClaim.Code] ? { coding: codingFromValue(claims[AllergyIntoleranceClaim.Code]) } : undefined,
18
18
  clinicalStatus: claims[AllergyIntoleranceClaim.ClinicalStatus] ? { coding: [{ code: claims[AllergyIntoleranceClaim.ClinicalStatus] }] } : undefined,
19
19
  verificationStatus: claims[AllergyIntoleranceClaim.VerificationStatus] ? { coding: [{ code: claims[AllergyIntoleranceClaim.VerificationStatus] }] } : undefined,
20
+ category: claims[AllergyIntoleranceClaim.Category] ? [claims[AllergyIntoleranceClaim.Category]] : undefined,
21
+ criticality: claims[AllergyIntoleranceClaim.Criticality],
22
+ onsetDateTime: claims[AllergyIntoleranceClaim.OnsetDateTime],
20
23
  recorder: recorder ? { reference: recorder } : undefined,
21
24
  };
22
25
  }
@@ -30,6 +33,9 @@ export function allergyIntoleranceFhirR4ToFlat(resource) {
30
33
  [AllergyIntoleranceClaim.Code]: codingToValue(code?.coding?.[0]) || code?.text,
31
34
  [AllergyIntoleranceClaim.ClinicalStatus]: resource.clinicalStatus?.coding?.[0]?.code,
32
35
  [AllergyIntoleranceClaim.VerificationStatus]: resource.verificationStatus?.coding?.[0]?.code,
36
+ [AllergyIntoleranceClaim.Category]: resource.category?.[0],
37
+ [AllergyIntoleranceClaim.Criticality]: resource.criticality,
38
+ [AllergyIntoleranceClaim.OnsetDateTime]: resource.onsetDateTime,
33
39
  [AllergyIntoleranceClaim.Recorder]: resource.recorder?.reference,
34
40
  };
35
41
  }
@@ -1,5 +1,5 @@
1
1
  import type { ClaimsRecord } from '../models/resource-document';
2
- import { type GaiaXCredentialDraft, type GaiaXLegalPersonCredentialSubject, type GaiaXServiceOfferingCredentialSubject, type GaiaXVcJwtAttachment, type IcaMemberDiscoveryData } from '../models/gaia-x';
2
+ import { type GaiaXCredentialAttachmentRoleValue, type GaiaXCredentialDraft, type GaiaXLegalPersonCredentialSubject, type GaiaXServiceOfferingCredentialSubject, type GaiaXVcJwtAttachment, type IcaMemberDiscoveryData } from '../models/gaia-x';
3
3
  import type { DidDocument } from '../models/did';
4
4
  export interface SchemaOrgOrganizationRegistrationIdentifier {
5
5
  additionalType: string;
@@ -94,6 +94,27 @@ export declare function buildGaiaXVcJwtAttachment(input: Readonly<{
94
94
  jwt: string;
95
95
  role: GaiaXVcJwtAttachment['role'];
96
96
  }>): GaiaXVcJwtAttachment;
97
+ /**
98
+ * Enforces the semantic contract of a signed Gaia-X discovery VC-JWT.
99
+ *
100
+ * This assertion deliberately does not verify the cryptographic signature.
101
+ * Signature, issuer, status and trust-chain verification remain the verifier's
102
+ * responsibility. It prevents a validly shaped schema.org
103
+ * OrganizationCredential from being merely serialized as JWT and mislabeled
104
+ * as the distinct Gaia-X participant credential.
105
+ *
106
+ * Participant attachments require `gx:LegalPerson` with the ICAM 25.11 legal
107
+ * properties. Service-offering attachments require `gx:ServiceOffering`,
108
+ * `gx:providedBy` and `gx:serviceOfferingTermsAndConditions`.
109
+ *
110
+ * This validates member-level `data[].attachments[]`; it does not describe
111
+ * `_retrieve?format=vc+jwt` or credential-internal
112
+ * `credential.evidence[].attachments`.
113
+ *
114
+ * @see https://docs.gaia-x.eu/technical-committee/identity-credential-access-management/25.11/gaia-x_credentials/
115
+ * @see https://docs.gaia-x.eu/technical-committee/identity-credential-access-management/25.11/semantic_model/
116
+ */
117
+ export declare function assertGaiaXDiscoveryAttachmentSemantics(jwt: string, role: GaiaXCredentialAttachmentRoleValue): void;
97
118
  /**
98
119
  * Assembles one ICA member discovery entry and enforces the interoperable
99
120
  * ordering contract: schema.org OrganizationCredential first in `vc[]`, and
@@ -174,14 +174,90 @@ export function buildGaiaXParticipantAttachment(input) {
174
174
  }
175
175
  /** Wraps an exact signed Gaia-X VC-JWT without decoding or re-signing it. */
176
176
  export function buildGaiaXVcJwtAttachment(input) {
177
+ const jwt = requiredInput(input.jwt, 'VC-JWT');
178
+ assertGaiaXDiscoveryAttachmentSemantics(jwt, input.role);
177
179
  return {
178
180
  id: requiredInput(input.id, 'attachment id'),
179
181
  format: GaiaXCredentialAttachmentFormat,
180
182
  role: input.role,
181
183
  media_type: GaiaXCredentialMediaType.VcJwt,
182
- data: { json: { jwt: requiredInput(input.jwt, 'VC-JWT') } },
184
+ data: { json: { jwt } },
183
185
  };
184
186
  }
187
+ function decodeVcJwtCredential(jwt) {
188
+ const parts = jwt.split('.');
189
+ if (parts.length !== 3 || parts.some((part) => !part)) {
190
+ throw new Error('Gaia-X attachment must contain one compact three-part VC-JWT.');
191
+ }
192
+ let payload;
193
+ try {
194
+ payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
195
+ }
196
+ catch {
197
+ throw new Error('Gaia-X attachment VC-JWT payload is not valid base64url JSON.');
198
+ }
199
+ return asObject(payload.vc) || payload;
200
+ }
201
+ function gaiaXCredentialSubject(document) {
202
+ const raw = document.credentialSubject;
203
+ if (Array.isArray(raw)) {
204
+ return raw.map(asObject).find((value) => Boolean(value));
205
+ }
206
+ return asObject(raw);
207
+ }
208
+ function requireGaiaXProperties(subject, names, role) {
209
+ const missing = names.filter((name) => !(name in subject));
210
+ if (missing.length) {
211
+ throw new Error(`Gaia-X ${role} VC-JWT is missing required semantic properties: ${missing.join(', ')}.`);
212
+ }
213
+ }
214
+ /**
215
+ * Enforces the semantic contract of a signed Gaia-X discovery VC-JWT.
216
+ *
217
+ * This assertion deliberately does not verify the cryptographic signature.
218
+ * Signature, issuer, status and trust-chain verification remain the verifier's
219
+ * responsibility. It prevents a validly shaped schema.org
220
+ * OrganizationCredential from being merely serialized as JWT and mislabeled
221
+ * as the distinct Gaia-X participant credential.
222
+ *
223
+ * Participant attachments require `gx:LegalPerson` with the ICAM 25.11 legal
224
+ * properties. Service-offering attachments require `gx:ServiceOffering`,
225
+ * `gx:providedBy` and `gx:serviceOfferingTermsAndConditions`.
226
+ *
227
+ * This validates member-level `data[].attachments[]`; it does not describe
228
+ * `_retrieve?format=vc+jwt` or credential-internal
229
+ * `credential.evidence[].attachments`.
230
+ *
231
+ * @see https://docs.gaia-x.eu/technical-committee/identity-credential-access-management/25.11/gaia-x_credentials/
232
+ * @see https://docs.gaia-x.eu/technical-committee/identity-credential-access-management/25.11/semantic_model/
233
+ */
234
+ export function assertGaiaXDiscoveryAttachmentSemantics(jwt, role) {
235
+ const document = decodeVcJwtCredential(jwt);
236
+ const subject = gaiaXCredentialSubject(document);
237
+ if (!subject)
238
+ throw new Error(`Gaia-X ${role} VC-JWT requires one credentialSubject object.`);
239
+ const subjectType = asString(subject.type);
240
+ if (role === GaiaXCredentialAttachmentRole.Participant) {
241
+ if (subjectType !== 'gx:LegalPerson') {
242
+ throw new Error('Gaia-X participant VC-JWT credentialSubject.type must be gx:LegalPerson.');
243
+ }
244
+ requireGaiaXProperties(subject, [
245
+ 'gx:legalName',
246
+ 'gx:legalRegistrationNumber',
247
+ 'gx:headquarterAddress',
248
+ 'gx:legalAddress',
249
+ ], 'participant');
250
+ }
251
+ if (role === GaiaXCredentialAttachmentRole.ServiceOffering) {
252
+ if (subjectType !== 'gx:ServiceOffering') {
253
+ throw new Error('Gaia-X service-offering VC-JWT credentialSubject.type must be gx:ServiceOffering.');
254
+ }
255
+ requireGaiaXProperties(subject, [
256
+ 'gx:providedBy',
257
+ 'gx:serviceOfferingTermsAndConditions',
258
+ ], 'service-offering');
259
+ }
260
+ }
185
261
  /**
186
262
  * Assembles one ICA member discovery entry and enforces the interoperable
187
263
  * ordering contract: schema.org OrganizationCredential first in `vc[]`, and
@@ -88,9 +88,7 @@ export declare const EXAMPLE_CONSENT_GRANT_INPUT: {
88
88
  };
89
89
  export declare const EXAMPLE_LIVE_CONSENT_GRANT_INPUT: {
90
90
  readonly subjectDid: string;
91
- readonly actor: {
92
- readonly identifier: "did:web:api.acme.org";
93
- };
91
+ readonly actorId: string;
94
92
  readonly actorRole: "ISCO-08|2211";
95
93
  readonly purpose: "TREAT";
96
94
  readonly actions: readonly [string];
@@ -1,5 +1,5 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
- import { EXAMPLE_CONTROLLER_DID, EXAMPLE_API_ORGANIZATION_DID, EXAMPLE_CLINICAL_CODE_PROBLEM, EXAMPLE_CLINICAL_DATE_RANGE, EXAMPLE_CLINICAL_SECTION_ALLERGIES, EXAMPLE_CLINICAL_SECTION_PATIENT_SUMMARY, EXAMPLE_CONSENT_PURPOSE_TREATMENT, EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL, EXAMPLE_GENERIC_SUBJECT_DID, EXAMPLE_HEALTHCARE_ACTOR_ROLE_PHYSICIAN, EXAMPLE_HEALTHCARE_JURISDICTION, EXAMPLE_HEALTHCARE_ROLE_PHYSICIAN_TEXT, EXAMPLE_PRACTITIONER_DID, EXAMPLE_PROFESSIONAL_DID, EXAMPLE_SUBJECT_DID, EXAMPLE_FORM_CONTROLLER_PHONE, } from './shared.js';
2
+ import { EXAMPLE_CONTROLLER_DID, EXAMPLE_CLINICAL_CODE_PROBLEM, EXAMPLE_CLINICAL_DATE_RANGE, EXAMPLE_CLINICAL_SECTION_ALLERGIES, EXAMPLE_CLINICAL_SECTION_PATIENT_SUMMARY, EXAMPLE_CONSENT_PURPOSE_TREATMENT, EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL, EXAMPLE_GENERIC_SUBJECT_DID, EXAMPLE_HEALTHCARE_ACTOR_ROLE_GENERALIST_MEDICAL_PRACTITIONER, EXAMPLE_HEALTHCARE_JURISDICTION, EXAMPLE_HEALTHCARE_ROLE_PHYSICIAN_TEXT, EXAMPLE_PRACTITIONER_DID, EXAMPLE_PROFESSIONAL_DID, EXAMPLE_SUBJECT_DID, EXAMPLE_FORM_CONTROLLER_PHONE, } from './shared.js';
3
3
  export const EXAMPLE_INDIVIDUAL_ORGANIZATION_START_INPUT = {
4
4
  alternateName: 'ana',
5
5
  controllerEmail: EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL,
@@ -57,8 +57,8 @@ export const EXAMPLE_CONSENT_GRANT_INPUT = {
57
57
  };
58
58
  export const EXAMPLE_LIVE_CONSENT_GRANT_INPUT = {
59
59
  subjectDid: EXAMPLE_SUBJECT_DID,
60
- actor: { identifier: EXAMPLE_API_ORGANIZATION_DID },
61
- actorRole: EXAMPLE_HEALTHCARE_ACTOR_ROLE_PHYSICIAN,
60
+ actorId: EXAMPLE_PROFESSIONAL_DID,
61
+ actorRole: EXAMPLE_HEALTHCARE_ACTOR_ROLE_GENERALIST_MEDICAL_PRACTITIONER,
62
62
  purpose: EXAMPLE_CONSENT_PURPOSE_TREATMENT,
63
63
  actions: [EXAMPLE_CLINICAL_SECTION_ALLERGIES],
64
64
  };
@@ -75,36 +75,36 @@ export declare const EXAMPLE_SEARCH_CLINICAL_BUNDLE_INPUT: {
75
75
  *
76
76
  * - use `EXAMPLE_TOKEN_EXCHANGE_SMART_INPUT` or `EXAMPLE_OPENID_SMART_TOKEN_INPUT`
77
77
  * for the first read-only examples
78
- * - use the scenarios below when you explicitly want the composition read scope
79
- * plus `organization/Consent.cruds`
78
+ * - do not append `organization/Consent.cruds` to a clinical read unless a
79
+ * separate rule explicitly grants that resource capability
80
80
  */
81
81
  export declare const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS: Readonly<{
82
82
  readonly physicianAllergiesRead: {
83
83
  readonly actorRole: "ISCO-08|2211";
84
84
  readonly purpose: "TREAT";
85
85
  readonly consentActions: readonly [string];
86
- readonly smartScopes: readonly [string, "organization/Consent.cruds"];
86
+ readonly smartScopes: readonly [string];
87
87
  readonly includedTypes: readonly ["Composition", "AllergyIntolerance", "DocumentReference"];
88
88
  };
89
89
  readonly nursingMedicationRead: {
90
90
  readonly actorRole: "ISCO-08|2221";
91
91
  readonly purpose: "TREAT";
92
92
  readonly consentActions: readonly [string];
93
- readonly smartScopes: readonly [string, "organization/Consent.cruds"];
93
+ readonly smartScopes: readonly [string];
94
94
  readonly includedTypes: readonly ["Composition", "MedicationStatement", "DocumentReference"];
95
95
  };
96
96
  readonly paramedicEmergencySummaryRead: {
97
97
  readonly actorRole: "ISCO-08|2240";
98
98
  readonly purpose: "ETREAT";
99
99
  readonly consentActions: readonly [string];
100
- readonly smartScopes: readonly [string, "organization/Consent.cruds"];
100
+ readonly smartScopes: readonly [string];
101
101
  readonly includedTypes: readonly ["Composition", "DocumentReference", "Observation"];
102
102
  };
103
103
  readonly physicianResultsAndProblemsRead: {
104
104
  readonly actorRole: "ISCO-08|2211";
105
105
  readonly purpose: "TREAT";
106
106
  readonly consentActions: readonly [string, string];
107
- readonly smartScopes: readonly [string, "organization/Consent.cruds"];
107
+ readonly smartScopes: readonly [string];
108
108
  readonly includedTypes: readonly ["Composition", "Condition", "DiagnosticReport", "DocumentReference"];
109
109
  };
110
110
  }>;
@@ -1,7 +1,6 @@
1
1
  // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
- import { HealthcareActorRoles, HealthcareBasicSections, HealthcareConsentPurposes, } from '../constants/healthcare.js';
2
+ import { HealthcareActorRoles, HealthcareBasicSections, HealthcareConsentActions, HealthcareConsentPurposes, } from '../constants/healthcare.js';
3
3
  import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
4
- import { SmartGatewayScopesFhirR4 } from '../constants/smart.js';
5
4
  import { buildSmartCompositionReadScope } from '../utils/smart-scope.js';
6
5
  import { EXAMPLE_CLINICAL_SECTION_ALLERGIES, EXAMPLE_EMAIL_PROFESSIONAL, EXAMPLE_HEALTHCARE_JURISDICTION, EXAMPLE_PATIENT_DID, EXAMPLE_PROVIDER_ORGANIZATION_URL, EXAMPLE_SUBJECT_DID, } from './shared.js';
7
6
  /**
@@ -12,7 +11,7 @@ import { EXAMPLE_CLINICAL_SECTION_ALLERGIES, EXAMPLE_EMAIL_PROFESSIONAL, EXAMPLE
12
11
  * - the actor role of the professional (`Physician`, `NursingProfessional`, ...)
13
12
  * - the consented action/section over an individual subject
14
13
  * - the SMART scope ultimately requested against GW CORE
15
- * - the difference between a minimal read scope and an elevated consent-management scope
14
+ * - a read scope containing only capabilities covered by the consent
16
15
  *
17
16
  * This differs from organization-controller and individual-owner bootstrap
18
17
  * examples, where the main concern is identity/bootstrap rather than
@@ -28,10 +27,6 @@ const EXAMPLE_CANONICAL_SMART_READ_SCOPES = [
28
27
  sections: EXAMPLE_CLINICAL_SECTION_ALLERGIES,
29
28
  }),
30
29
  ];
31
- const EXAMPLE_CANONICAL_SMART_SCOPES = [
32
- ...EXAMPLE_CANONICAL_SMART_READ_SCOPES,
33
- SmartGatewayScopesFhirR4.ConsentCruds,
34
- ];
35
30
  export const EXAMPLE_TOKEN_EXCHANGE_SMART_INPUT = {
36
31
  idToken: 'employee-id-token-001',
37
32
  scopes: [...EXAMPLE_CANONICAL_SMART_READ_SCOPES],
@@ -99,20 +94,19 @@ export const EXAMPLE_SEARCH_CLINICAL_BUNDLE_INPUT = {
99
94
  *
100
95
  * - use `EXAMPLE_TOKEN_EXCHANGE_SMART_INPUT` or `EXAMPLE_OPENID_SMART_TOKEN_INPUT`
101
96
  * for the first read-only examples
102
- * - use the scenarios below when you explicitly want the composition read scope
103
- * plus `organization/Consent.cruds`
97
+ * - do not append `organization/Consent.cruds` to a clinical read unless a
98
+ * separate rule explicitly grants that resource capability
104
99
  */
105
100
  export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
106
101
  physicianAllergiesRead: {
107
- actorRole: HealthcareActorRoles.Physician,
102
+ actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
108
103
  purpose: HealthcareConsentPurposes.Treatment,
109
- consentActions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
104
+ consentActions: [HealthcareConsentActions.AllergiesAndIntolerances],
110
105
  smartScopes: [
111
106
  buildSmartCompositionReadScope({
112
107
  subjectDid: EXAMPLE_SMART_SUBJECT_DID,
113
- sections: HealthcareBasicSections.AllergiesAndIntolerances.claim,
108
+ sections: HealthcareConsentActions.AllergiesAndIntolerances,
114
109
  }),
115
- SmartGatewayScopesFhirR4.ConsentCruds,
116
110
  ],
117
111
  includedTypes: [
118
112
  ResourceTypesFhirR4.Composition,
@@ -129,7 +123,6 @@ export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
129
123
  subjectDid: EXAMPLE_SMART_SUBJECT_DID,
130
124
  sections: HealthcareBasicSections.HistoryOfMedicationUse.claim,
131
125
  }),
132
- SmartGatewayScopesFhirR4.ConsentCruds,
133
126
  ],
134
127
  includedTypes: [
135
128
  ResourceTypesFhirR4.Composition,
@@ -146,7 +139,6 @@ export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
146
139
  subjectDid: EXAMPLE_SMART_SUBJECT_DID,
147
140
  sections: HealthcareBasicSections.PatientSummaryDocument.claim,
148
141
  }),
149
- SmartGatewayScopesFhirR4.ConsentCruds,
150
142
  ],
151
143
  includedTypes: [
152
144
  ResourceTypesFhirR4.Composition,
@@ -155,7 +147,7 @@ export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
155
147
  ],
156
148
  },
157
149
  physicianResultsAndProblemsRead: {
158
- actorRole: HealthcareActorRoles.Physician,
150
+ actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
159
151
  purpose: HealthcareConsentPurposes.Treatment,
160
152
  consentActions: [
161
153
  HealthcareBasicSections.Results.claim,
@@ -169,7 +161,6 @@ export const EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS = Object.freeze({
169
161
  HealthcareBasicSections.ProblemList.claim,
170
162
  ],
171
163
  }),
172
- SmartGatewayScopesFhirR4.ConsentCruds,
173
164
  ],
174
165
  includedTypes: [
175
166
  ResourceTypesFhirR4.Composition,
@@ -190,7 +181,6 @@ function buildConsentDecisionScenario(params) {
190
181
  subjectDid: EXAMPLE_SMART_SUBJECT_DID,
191
182
  sections: params.requestedSections,
192
183
  }),
193
- SmartGatewayScopesFhirR4.ConsentCruds,
194
184
  ],
195
185
  includedTypes: [...params.includedTypes],
196
186
  expectedSmartTokenDecision: params.expectedSmartTokenDecision,
@@ -212,7 +202,7 @@ function buildConsentDecisionScenario(params) {
212
202
  export const EXAMPLE_PROFESSIONAL_CONSENT_SCENARIOS = Object.freeze({
213
203
  physicianByEmailContinuousCareAllergiesAllowed: buildConsentDecisionScenario({
214
204
  actorId: EXAMPLE_PHYSICIAN_EMAIL,
215
- actorRole: HealthcareActorRoles.Physician,
205
+ actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
216
206
  purpose: HealthcareConsentPurposes.Treatment,
217
207
  consentActions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
218
208
  requestedSections: HealthcareBasicSections.AllergiesAndIntolerances.claim,
@@ -226,7 +216,7 @@ export const EXAMPLE_PROFESSIONAL_CONSENT_SCENARIOS = Object.freeze({
226
216
  }),
227
217
  physicianByEmailEmergencySummaryAllowed: buildConsentDecisionScenario({
228
218
  actorId: EXAMPLE_PHYSICIAN_EMAIL,
229
- actorRole: HealthcareActorRoles.Physician,
219
+ actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
230
220
  purpose: HealthcareConsentPurposes.EmergencyTreatment,
231
221
  consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
232
222
  requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
@@ -240,7 +230,7 @@ export const EXAMPLE_PROFESSIONAL_CONSENT_SCENARIOS = Object.freeze({
240
230
  }),
241
231
  physicianByOrganizationResultsAllowed: buildConsentDecisionScenario({
242
232
  actorId: { organizationUrl: EXAMPLE_PROVIDER_ORG_URL },
243
- actorRole: HealthcareActorRoles.Physician,
233
+ actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
244
234
  purpose: HealthcareConsentPurposes.Treatment,
245
235
  consentActions: [HealthcareBasicSections.Results.claim],
246
236
  requestedSections: HealthcareBasicSections.Results.claim,
@@ -254,7 +244,7 @@ export const EXAMPLE_PROFESSIONAL_CONSENT_SCENARIOS = Object.freeze({
254
244
  }),
255
245
  physicianByJurisdictionEmergencySummaryAllowed: buildConsentDecisionScenario({
256
246
  actorId: EXAMPLE_JURISDICTION,
257
- actorRole: HealthcareActorRoles.Physician,
247
+ actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
258
248
  purpose: HealthcareConsentPurposes.EmergencyTreatment,
259
249
  consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
260
250
  requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
@@ -296,7 +286,7 @@ export const EXAMPLE_PROFESSIONAL_CONSENT_SCENARIOS = Object.freeze({
296
286
  }),
297
287
  physicianObstetricianDeniedWhenOnlyAllergiesConsent: buildConsentDecisionScenario({
298
288
  actorId: EXAMPLE_PHYSICIAN_EMAIL,
299
- actorRole: `${HealthcareActorRoles.Physician}:obstetrician`,
289
+ actorRole: `${HealthcareActorRoles.GeneralistMedicalPractitioner}:obstetrician`,
300
290
  purpose: HealthcareConsentPurposes.Treatment,
301
291
  consentActions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
302
292
  requestedSections: HealthcareBasicSections.Results.claim,
@@ -310,7 +300,7 @@ export const EXAMPLE_PROFESSIONAL_CONSENT_SCENARIOS = Object.freeze({
310
300
  }),
311
301
  physicianByEmailDeniedWhenConsentRevokedAndNoOrgNorJurisdictionConsentIsActive: buildConsentDecisionScenario({
312
302
  actorId: EXAMPLE_PHYSICIAN_EMAIL,
313
- actorRole: HealthcareActorRoles.Physician,
303
+ actorRole: HealthcareActorRoles.GeneralistMedicalPractitioner,
314
304
  purpose: HealthcareConsentPurposes.EmergencyTreatment,
315
305
  consentActions: [HealthcareBasicSections.PatientSummaryDocument.claim],
316
306
  requestedSections: HealthcareBasicSections.PatientSummaryDocument.claim,
@@ -128,7 +128,7 @@ export const EXAMPLE_SERVICE_PUBLIC_DID = 'did:web:public.acme.org';
128
128
  export const EXAMPLE_PROFESSIONAL_DID = buildProfessionalDidWeb({
129
129
  organizationDidWeb: 'did:web:api.acme.org',
130
130
  email: 'doctor.oncall@example.org',
131
- role: HealthcareActorRoles.Physician,
131
+ role: HealthcareActorRoles.GeneralistMedicalPractitioner,
132
132
  });
133
133
  export const EXAMPLE_PROVIDER_ORGANIZATION_DID = 'did:web:hospital.acme.org';
134
134
  export const EXAMPLE_PROVIDER_ORGANIZATION_URL = 'https://hospital.acme.org';
@@ -148,7 +148,13 @@ export declare function buildOrganizationDidWeb(input: {
148
148
  /**
149
149
  * Builds a professional/member DID under a hosted organization DID.
150
150
  *
151
- * The stable actor identifier is derived from the email using multibase(base58btc(multihash(sha384))).
151
+ * The stable actor path identifier is derived from the lower-cased email using
152
+ * multibase(base58btc(multihash(SHA3-256))). The raw email is never embedded
153
+ * in the DID.
154
+ *
155
+ * This is the same multibase payload used by the ICA-compatible credential
156
+ * `sameAs`; only the representation differs: the DID path uses `z...` and the
157
+ * credential alias uses `urn:multibase:z...`.
152
158
  *
153
159
  * @param input.organizationDidWeb Canonical hosted organization DID.
154
160
  * @param input.email Professional email used to derive a stable member identifier.
package/dist/utils/did.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // crypto-ts/utils/did.ts
2
2
  // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
3
- import { encodeMultibaseSha384 } from './multibasehash.js';
3
+ import { multibase58MultihashSha3_256 } from './same-as.js';
4
4
  /**
5
5
  * Canonical DID path markers for hosted/provider individual identities.
6
6
  *
@@ -240,7 +240,13 @@ export function buildOrganizationDidWeb(input) {
240
240
  /**
241
241
  * Builds a professional/member DID under a hosted organization DID.
242
242
  *
243
- * The stable actor identifier is derived from the email using multibase(base58btc(multihash(sha384))).
243
+ * The stable actor path identifier is derived from the lower-cased email using
244
+ * multibase(base58btc(multihash(SHA3-256))). The raw email is never embedded
245
+ * in the DID.
246
+ *
247
+ * This is the same multibase payload used by the ICA-compatible credential
248
+ * `sameAs`; only the representation differs: the DID path uses `z...` and the
249
+ * credential alias uses `urn:multibase:z...`.
244
250
  *
245
251
  * @param input.organizationDidWeb Canonical hosted organization DID.
246
252
  * @param input.email Professional email used to derive a stable member identifier.
@@ -254,7 +260,7 @@ export function buildProfessionalDidWeb(input) {
254
260
  throw new Error('buildProfessionalDidWeb requires email.');
255
261
  if (!role)
256
262
  throw new Error('buildProfessionalDidWeb requires role.');
257
- const memberId = encodeMultibaseSha384(normalizedEmail);
263
+ const memberId = multibase58MultihashSha3_256(normalizedEmail);
258
264
  return [
259
265
  String(input.organizationDidWeb).trim(),
260
266
  'employee',
@@ -29,6 +29,10 @@ export type ProfessionalSmartVpPayloadInput = Readonly<{
29
29
  * - otherwise the public employee email
30
30
  *
31
31
  * When both are present they are merged and deduplicated after normalization.
32
+ * Plain emails become ICA-compatible
33
+ * `urn:multibase:<base58btc(multihash(SHA3-256))>` values. The multibase
34
+ * payload is the same value used in the professional DID actor path; `sameAs`
35
+ * adds only the `urn:multibase:` prefix.
32
36
  *
33
37
  * @param input Professional identity source values.
34
38
  */
@@ -11,6 +11,10 @@ import { buildUnsignedVpJwt } from './jwt.js';
11
11
  * - otherwise the public employee email
12
12
  *
13
13
  * When both are present they are merged and deduplicated after normalization.
14
+ * Plain emails become ICA-compatible
15
+ * `urn:multibase:<base58btc(multihash(SHA3-256))>` values. The multibase
16
+ * payload is the same value used in the professional DID actor path; `sameAs`
17
+ * adds only the `urn:multibase:` prefix.
14
18
  *
15
19
  * @param input Professional identity source values.
16
20
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.3.6",
3
+ "version": "2.3.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },