gdc-common-utils-ts 2.8.8 → 2.9.1

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
@@ -427,7 +427,9 @@ The canonical API contract should live in JSDoc on exported code. The README act
427
427
  sections after six extensions, and the complete 82-section/classification
428
428
  catalog. The IPS document type lives separately in `HealthcareDocumentTypes`.
429
429
  - [`HealthcareConsentPurposes`, `HealthcareConsentActions`, `HealthcareActorRoles`, `HealthcareActorRoleCodes`](src/constants/healthcare.ts)
430
- - Shared healthcare authorization and role constants.
430
+ - Shared healthcare authorization and role constants, including the
431
+ `health-lab`, `animal-lab`, and `onehealth-lab` sector catalogs. See
432
+ [Healthcare Roles and i18n 101](docs/101-HEALTHCARE_ROLES_I18N.md).
431
433
  - [`EXAMPLE_PROFESSIONAL_ACCESS_SCENARIOS`](src/examples/professional.ts)
432
434
  - Reusable professional role/permission examples tying actor role, consent action, SMART scope, and expected FHIR resource types together.
433
435
  - [`DeviceUserClasses`, `DeviceAppTypes`](src/constants/device.ts)
@@ -454,7 +456,7 @@ The canonical API contract should live in JSDoc on exported code. The README act
454
456
  - do not teach its `entityId` as if it were the legal organization id
455
457
  - Derives the technical ML-DSA/ML-KEM communication identity for a device, portal, or app profile and returns JOSE header templates for `meta.jws.protected` and `meta.jwe.header`.
456
458
  - Uses explicit `seedMaterial` for deterministic derivation. Without `seedMaterial`, it defaults to random generation. `mode = deterministic` requires `seedMaterial`.
457
- - [`buildOrganizationDidWeb(...)`, `buildProfessionalDidWeb(...)`, `buildIndividualDidWeb(...)`](src/utils/did.ts)
459
+ - [`buildOrganizationDidWeb(...)`, `buildProfessionalDidWeb(...)`, `buildIndividualDidWeb(...)`, `buildIndividualMemberDidWebFromPrivateIdentifiers(...)`](src/utils/did.ts)
458
460
  - Build canonical data-space `did:web` identifiers for hosted organizations, professionals, and individuals/family actors.
459
461
  - [`buildSmartCompositionReadScope(...)`](src/utils/smart-scope.ts)
460
462
  - Builds the current CORE GW pinned SMART root scope for `organization/Composition...` token requests.
@@ -528,9 +530,9 @@ The canonical API contract should live in JSDoc on exported code. The README act
528
530
  - [`buildFhirIpsCreatorProvenance(...)`](src/utils/fhir-ips-creator-identity.ts)
529
531
  and [`docs/101-DIDCOMM-IDENTITY-BOUNDARY.md`](docs/101-DIDCOMM-IDENTITY-BOUNDARY.md)
530
532
  - Keep `Composition.author`, `Composition.attester`, DIDComm sender and
531
- signing key separate. A member-created fact may use the registered
532
- RelatedPerson as both author and personal attester; a dictated
533
- individual-created fact keeps the individual as author.
533
+ signing key separate. The organization, EHR/portal or individual remains
534
+ the document author; a registered PractitionerRole or RelatedPerson is the
535
+ attester, including for content entered by that authenticated person.
534
536
  - [`src/examples/frontend-session.ts`](src/examples/frontend-session.ts)
535
537
  - Frontend profile/session bootstrap examples.
536
538
  - [`src/examples/lifecycle.ts`](src/examples/lifecycle.ts)
@@ -1727,7 +1727,13 @@ export declare const HealthcareActorRoles: Readonly<{
1727
1727
  readonly MidwiferyProfessional: "ISCO-08|2222";
1728
1728
  readonly Paramedic: "ISCO-08|2240";
1729
1729
  readonly Veterinarian: "ISCO-08|2250";
1730
+ /** ISCO-08 laboratory occupation for non-medical life-science work. */
1731
+ readonly LifeScienceTechnician: "ISCO-08|3141";
1732
+ /** ISCO-08 laboratory occupation for human medical/pathology work. */
1733
+ readonly MedicalAndPathologyLaboratoryTechnician: "ISCO-08|3212";
1730
1734
  readonly VeterinaryTechnicianOrAssistant: "ISCO-08|3240";
1735
+ /** General receptionist occupation usable across human, animal and one-health organizations. */
1736
+ readonly Receptionist: "ISCO-08|4226";
1731
1737
  /** Administrative health occupation; it does not imply clinical authority. */
1732
1738
  readonly MedicalSecretary: "ISCO-08|3344";
1733
1739
  }>;
@@ -1750,7 +1756,10 @@ export declare const HealthcareActorRoleCodes: Readonly<{
1750
1756
  readonly MidwiferyProfessional: "2222";
1751
1757
  readonly Paramedic: "2240";
1752
1758
  readonly Veterinarian: "2250";
1759
+ readonly LifeScienceTechnician: "3141";
1760
+ readonly MedicalAndPathologyLaboratoryTechnician: "3212";
1753
1761
  readonly VeterinaryTechnicianOrAssistant: "3240";
1762
+ readonly Receptionist: "4226";
1754
1763
  readonly MedicalSecretary: "3344";
1755
1764
  }>;
1756
1765
  export declare const HealthcareRolesByFamily: Readonly<{
@@ -1784,16 +1793,19 @@ export declare const HealthcareRolesByFamily: Readonly<{
1784
1793
  }>;
1785
1794
  export declare const HealthcareAllRolesByClaim: Readonly<Record<string, HealthcareActorRoleDescriptor>>;
1786
1795
  export declare const HealthcareProfessionalRoleCodesBySector: Readonly<{
1787
- readonly "health-care": readonly ("RESPRSN" | "221" | "2211" | "2212" | "2221" | "2222" | "2240" | "3344")[];
1788
- readonly "health-research": readonly ("RESPRSN" | "221" | "2211" | "2212" | "3344")[];
1789
- readonly "health-tech": readonly "RESPRSN"[];
1790
- readonly "health-insurance": readonly "RESPRSN"[];
1791
- readonly "animal-care": readonly ("RESPRSN" | "2250" | "3240")[];
1792
- readonly "animal-research": readonly ("RESPRSN" | "2250" | "3240")[];
1793
- readonly "animal-insurance": readonly "RESPRSN"[];
1794
- readonly "animal-tech": readonly "RESPRSN"[];
1795
- readonly "onehealth-research": readonly ("RESPRSN" | "221" | "2211" | "2212" | "2250" | "3240" | "3344")[];
1796
- readonly "onehealth-tech": readonly "RESPRSN"[];
1796
+ readonly "health-care": readonly ("RESPRSN" | "221" | "2211" | "2212" | "2221" | "2222" | "2240" | "4226" | "3344")[];
1797
+ readonly "health-research": readonly ("RESPRSN" | "221" | "2211" | "2212" | "4226" | "3344")[];
1798
+ readonly "health-lab": readonly ("RESPRSN" | "221" | "2212" | "3212" | "4226" | "3344")[];
1799
+ readonly "health-tech": readonly ("RESPRSN" | "4226")[];
1800
+ readonly "health-insurance": readonly ("RESPRSN" | "4226")[];
1801
+ readonly "animal-care": readonly ("RESPRSN" | "2250" | "3240" | "4226")[];
1802
+ readonly "animal-research": readonly ("RESPRSN" | "2250" | "3240" | "4226")[];
1803
+ readonly "animal-lab": readonly ("RESPRSN" | "2250" | "3141" | "3240" | "4226")[];
1804
+ readonly "animal-insurance": readonly ("RESPRSN" | "4226")[];
1805
+ readonly "animal-tech": readonly ("RESPRSN" | "4226")[];
1806
+ readonly "onehealth-research": readonly ("RESPRSN" | "221" | "2211" | "2212" | "2250" | "3240" | "4226" | "3344")[];
1807
+ readonly "onehealth-lab": readonly ("RESPRSN" | "221" | "2212" | "2250" | "3141" | "3212" | "3240" | "4226" | "3344")[];
1808
+ readonly "onehealth-tech": readonly ("RESPRSN" | "4226")[];
1797
1809
  }>;
1798
1810
  export declare const HealthcareProfessionalRolesBySector: Readonly<{
1799
1811
  readonly "health-care": Readonly<Record<string, Readonly<{
@@ -1814,6 +1826,15 @@ export declare const HealthcareProfessionalRolesBySector: Readonly<{
1814
1826
  titleEn: string;
1815
1827
  definition?: string;
1816
1828
  }>>>;
1829
+ readonly "health-lab": Readonly<Record<string, Readonly<{
1830
+ family: HealthcareRoleFamily;
1831
+ codingSystem: string;
1832
+ code: string;
1833
+ claim: string;
1834
+ i18nKey: `org.ilo.isco-08.${string}` | `org.isco08.${string}` | `org.hl7.terminology.CodeSystem.v3-RoleCode.${string}`;
1835
+ titleEn: string;
1836
+ definition?: string;
1837
+ }>>>;
1817
1838
  readonly "health-tech": Readonly<Record<string, Readonly<{
1818
1839
  family: HealthcareRoleFamily;
1819
1840
  codingSystem: string;
@@ -1850,6 +1871,15 @@ export declare const HealthcareProfessionalRolesBySector: Readonly<{
1850
1871
  titleEn: string;
1851
1872
  definition?: string;
1852
1873
  }>>>;
1874
+ readonly "animal-lab": Readonly<Record<string, Readonly<{
1875
+ family: HealthcareRoleFamily;
1876
+ codingSystem: string;
1877
+ code: string;
1878
+ claim: string;
1879
+ i18nKey: `org.ilo.isco-08.${string}` | `org.isco08.${string}` | `org.hl7.terminology.CodeSystem.v3-RoleCode.${string}`;
1880
+ titleEn: string;
1881
+ definition?: string;
1882
+ }>>>;
1853
1883
  readonly "animal-insurance": Readonly<Record<string, Readonly<{
1854
1884
  family: HealthcareRoleFamily;
1855
1885
  codingSystem: string;
@@ -1877,6 +1907,15 @@ export declare const HealthcareProfessionalRolesBySector: Readonly<{
1877
1907
  titleEn: string;
1878
1908
  definition?: string;
1879
1909
  }>>>;
1910
+ readonly "onehealth-lab": Readonly<Record<string, Readonly<{
1911
+ family: HealthcareRoleFamily;
1912
+ codingSystem: string;
1913
+ code: string;
1914
+ claim: string;
1915
+ i18nKey: `org.ilo.isco-08.${string}` | `org.isco08.${string}` | `org.hl7.terminology.CodeSystem.v3-RoleCode.${string}`;
1916
+ titleEn: string;
1917
+ definition?: string;
1918
+ }>>>;
1880
1919
  readonly "onehealth-tech": Readonly<Record<string, Readonly<{
1881
1920
  family: HealthcareRoleFamily;
1882
1921
  codingSystem: string;
@@ -359,7 +359,13 @@ export const HealthcareActorRoles = Object.freeze({
359
359
  MidwiferyProfessional: 'ISCO-08|2222',
360
360
  Paramedic: 'ISCO-08|2240',
361
361
  Veterinarian: 'ISCO-08|2250',
362
+ /** ISCO-08 laboratory occupation for non-medical life-science work. */
363
+ LifeScienceTechnician: 'ISCO-08|3141',
364
+ /** ISCO-08 laboratory occupation for human medical/pathology work. */
365
+ MedicalAndPathologyLaboratoryTechnician: 'ISCO-08|3212',
362
366
  VeterinaryTechnicianOrAssistant: 'ISCO-08|3240',
367
+ /** General receptionist occupation usable across human, animal and one-health organizations. */
368
+ Receptionist: 'ISCO-08|4226',
363
369
  /** Administrative health occupation; it does not imply clinical authority. */
364
370
  MedicalSecretary: 'ISCO-08|3344',
365
371
  });
@@ -382,7 +388,10 @@ export const HealthcareActorRoleCodes = Object.freeze({
382
388
  MidwiferyProfessional: '2222',
383
389
  Paramedic: '2240',
384
390
  Veterinarian: '2250',
391
+ LifeScienceTechnician: '3141',
392
+ MedicalAndPathologyLaboratoryTechnician: '3212',
385
393
  VeterinaryTechnicianOrAssistant: '3240',
394
+ Receptionist: '4226',
386
395
  MedicalSecretary: '3344',
387
396
  });
388
397
  function buildProfessionalIscoRoles() {
@@ -464,6 +473,28 @@ function buildProfessionalIscoRoles() {
464
473
  titleEn: 'Veterinarian',
465
474
  }),
466
475
  ],
476
+ [
477
+ HealthcareActorRoleCodes.LifeScienceTechnician,
478
+ Object.freeze({
479
+ family: HealthcareRoleFamilies.ProfessionalOccupationIsco08,
480
+ codingSystem: ISCO08_CODING_SYSTEM,
481
+ code: HealthcareActorRoleCodes.LifeScienceTechnician,
482
+ claim: HealthcareActorRoles.LifeScienceTechnician,
483
+ i18nKey: `${ISCO08_I18N_NAMESPACE}.${HealthcareActorRoleCodes.LifeScienceTechnician}`,
484
+ titleEn: 'Life science technician (excluding medical)',
485
+ }),
486
+ ],
487
+ [
488
+ HealthcareActorRoleCodes.MedicalAndPathologyLaboratoryTechnician,
489
+ Object.freeze({
490
+ family: HealthcareRoleFamilies.ProfessionalOccupationIsco08,
491
+ codingSystem: ISCO08_CODING_SYSTEM,
492
+ code: HealthcareActorRoleCodes.MedicalAndPathologyLaboratoryTechnician,
493
+ claim: HealthcareActorRoles.MedicalAndPathologyLaboratoryTechnician,
494
+ i18nKey: `${ISCO08_I18N_NAMESPACE}.${HealthcareActorRoleCodes.MedicalAndPathologyLaboratoryTechnician}`,
495
+ titleEn: 'Medical and pathology laboratory technician',
496
+ }),
497
+ ],
467
498
  [
468
499
  HealthcareActorRoleCodes.VeterinaryTechnicianOrAssistant,
469
500
  Object.freeze({
@@ -475,6 +506,17 @@ function buildProfessionalIscoRoles() {
475
506
  titleEn: 'Veterinary technician or assistant',
476
507
  }),
477
508
  ],
509
+ [
510
+ HealthcareActorRoleCodes.Receptionist,
511
+ Object.freeze({
512
+ family: HealthcareRoleFamilies.ProfessionalOccupationIsco08,
513
+ codingSystem: ISCO08_CODING_SYSTEM,
514
+ code: HealthcareActorRoleCodes.Receptionist,
515
+ claim: HealthcareActorRoles.Receptionist,
516
+ i18nKey: `${ISCO08_I18N_NAMESPACE}.${HealthcareActorRoleCodes.Receptionist}`,
517
+ titleEn: 'Receptionists (general)',
518
+ }),
519
+ ],
478
520
  [
479
521
  HealthcareActorRoleCodes.MedicalSecretary,
480
522
  Object.freeze({
@@ -552,6 +594,7 @@ export const HealthcareProfessionalRoleCodesBySector = Object.freeze({
552
594
  HealthcareActorRoleCodes.NursingProfessional,
553
595
  HealthcareActorRoleCodes.MidwiferyProfessional,
554
596
  HealthcareActorRoleCodes.Paramedic,
597
+ HealthcareActorRoleCodes.Receptionist,
555
598
  HealthcareActorRoleCodes.MedicalSecretary,
556
599
  ]),
557
600
  [DataspaceSectors.HealthResearch]: Object.freeze([
@@ -559,22 +602,40 @@ export const HealthcareProfessionalRoleCodesBySector = Object.freeze({
559
602
  HealthcareActorRoleCodes.MedicalDoctors,
560
603
  HealthcareActorRoleCodes.GeneralistMedicalPractitioner,
561
604
  HealthcareActorRoleCodes.SpecialistMedicalPractitioner,
605
+ HealthcareActorRoleCodes.Receptionist,
562
606
  HealthcareActorRoleCodes.MedicalSecretary,
563
607
  ]),
564
- [DataspaceSectors.HealthTech]: Object.freeze([HealthcareActorRoleCodes.Controller]),
565
- [DataspaceSectors.HealthInsurance]: Object.freeze([HealthcareActorRoleCodes.Controller]),
608
+ [DataspaceSectors.HealthLab]: Object.freeze([
609
+ HealthcareActorRoleCodes.Controller,
610
+ HealthcareActorRoleCodes.MedicalDoctors,
611
+ HealthcareActorRoleCodes.SpecialistMedicalPractitioner,
612
+ HealthcareActorRoleCodes.MedicalAndPathologyLaboratoryTechnician,
613
+ HealthcareActorRoleCodes.Receptionist,
614
+ HealthcareActorRoleCodes.MedicalSecretary,
615
+ ]),
616
+ [DataspaceSectors.HealthTech]: Object.freeze([HealthcareActorRoleCodes.Controller, HealthcareActorRoleCodes.Receptionist]),
617
+ [DataspaceSectors.HealthInsurance]: Object.freeze([HealthcareActorRoleCodes.Controller, HealthcareActorRoleCodes.Receptionist]),
566
618
  [DataspaceSectors.AnimalCare]: Object.freeze([
567
619
  HealthcareActorRoleCodes.Controller,
568
620
  HealthcareActorRoleCodes.Veterinarian,
569
621
  HealthcareActorRoleCodes.VeterinaryTechnicianOrAssistant,
622
+ HealthcareActorRoleCodes.Receptionist,
570
623
  ]),
571
624
  [DataspaceSectors.AnimalResearch]: Object.freeze([
572
625
  HealthcareActorRoleCodes.Controller,
573
626
  HealthcareActorRoleCodes.Veterinarian,
574
627
  HealthcareActorRoleCodes.VeterinaryTechnicianOrAssistant,
628
+ HealthcareActorRoleCodes.Receptionist,
575
629
  ]),
576
- [DataspaceSectors.AnimalInsurance]: Object.freeze([HealthcareActorRoleCodes.Controller]),
577
- [DataspaceSectors.AnimalTech]: Object.freeze([HealthcareActorRoleCodes.Controller]),
630
+ [DataspaceSectors.AnimalLab]: Object.freeze([
631
+ HealthcareActorRoleCodes.Controller,
632
+ HealthcareActorRoleCodes.Veterinarian,
633
+ HealthcareActorRoleCodes.LifeScienceTechnician,
634
+ HealthcareActorRoleCodes.VeterinaryTechnicianOrAssistant,
635
+ HealthcareActorRoleCodes.Receptionist,
636
+ ]),
637
+ [DataspaceSectors.AnimalInsurance]: Object.freeze([HealthcareActorRoleCodes.Controller, HealthcareActorRoleCodes.Receptionist]),
638
+ [DataspaceSectors.AnimalTech]: Object.freeze([HealthcareActorRoleCodes.Controller, HealthcareActorRoleCodes.Receptionist]),
578
639
  [DataspaceSectors.OneHealthResearch]: Object.freeze([
579
640
  HealthcareActorRoleCodes.Controller,
580
641
  HealthcareActorRoleCodes.MedicalDoctors,
@@ -582,20 +643,35 @@ export const HealthcareProfessionalRoleCodesBySector = Object.freeze({
582
643
  HealthcareActorRoleCodes.SpecialistMedicalPractitioner,
583
644
  HealthcareActorRoleCodes.Veterinarian,
584
645
  HealthcareActorRoleCodes.VeterinaryTechnicianOrAssistant,
646
+ HealthcareActorRoleCodes.Receptionist,
647
+ HealthcareActorRoleCodes.MedicalSecretary,
648
+ ]),
649
+ [DataspaceSectors.OneHealthLab]: Object.freeze([
650
+ HealthcareActorRoleCodes.Controller,
651
+ HealthcareActorRoleCodes.MedicalDoctors,
652
+ HealthcareActorRoleCodes.SpecialistMedicalPractitioner,
653
+ HealthcareActorRoleCodes.Veterinarian,
654
+ HealthcareActorRoleCodes.LifeScienceTechnician,
655
+ HealthcareActorRoleCodes.MedicalAndPathologyLaboratoryTechnician,
656
+ HealthcareActorRoleCodes.VeterinaryTechnicianOrAssistant,
657
+ HealthcareActorRoleCodes.Receptionist,
585
658
  HealthcareActorRoleCodes.MedicalSecretary,
586
659
  ]),
587
- [DataspaceSectors.OneHealthTech]: Object.freeze([HealthcareActorRoleCodes.Controller]),
660
+ [DataspaceSectors.OneHealthTech]: Object.freeze([HealthcareActorRoleCodes.Controller, HealthcareActorRoleCodes.Receptionist]),
588
661
  });
589
662
  export const HealthcareProfessionalRolesBySector = Object.freeze({
590
663
  [DataspaceSectors.HealthCare]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.HealthCare]),
591
664
  [DataspaceSectors.HealthResearch]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.HealthResearch]),
665
+ [DataspaceSectors.HealthLab]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.HealthLab]),
592
666
  [DataspaceSectors.HealthTech]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.HealthTech]),
593
667
  [DataspaceSectors.HealthInsurance]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.HealthInsurance]),
594
668
  [DataspaceSectors.AnimalCare]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.AnimalCare]),
595
669
  [DataspaceSectors.AnimalResearch]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.AnimalResearch]),
670
+ [DataspaceSectors.AnimalLab]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.AnimalLab]),
596
671
  [DataspaceSectors.AnimalInsurance]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.AnimalInsurance]),
597
672
  [DataspaceSectors.AnimalTech]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.AnimalTech]),
598
673
  [DataspaceSectors.OneHealthResearch]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.OneHealthResearch]),
674
+ [DataspaceSectors.OneHealthLab]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.OneHealthLab]),
599
675
  [DataspaceSectors.OneHealthTech]: pickRoleCatalogByCodes(HealthcareProfessionalAndControllerRoles, HealthcareProfessionalRoleCodesBySector[DataspaceSectors.OneHealthTech]),
600
676
  });
601
677
  export const HealthcareProfessionalRolesBySectorAndClaim = Object.freeze(Object.fromEntries(Object.values(DataspaceSectors).map((sector) => [
@@ -614,7 +690,14 @@ export function getHealthcareRolesByFamily(family) {
614
690
  return HealthcareRolesByFamily[family];
615
691
  }
616
692
  export function getHealthcareRoleByClaim(claim) {
617
- return HealthcareAllRolesByClaim[String(claim || '').trim()];
693
+ const normalized = String(claim || '').trim();
694
+ const direct = HealthcareAllRolesByClaim[normalized];
695
+ if (direct)
696
+ return direct;
697
+ const fullHl7Prefix = `${HL7_CODING_SYSTEM_V3_ROLE_CODE}|`;
698
+ return normalized.startsWith(fullHl7Prefix)
699
+ ? HealthcareAllRolesByClaim[`v3-RoleCode|${normalized.slice(fullHl7Prefix.length)}`]
700
+ : undefined;
618
701
  }
619
702
  export function getHealthcareProfessionalRolesBySector(sector) {
620
703
  return HealthcareProfessionalRolesBySector[sector] || {};
@@ -32,6 +32,31 @@ export declare const HL7_V2_0203_IDENTIFIER_CODES: Readonly<{
32
32
  readonly PatientIdentifier: "PI";
33
33
  }>;
34
34
  export type Hl7V20203IdentifierCode = typeof HL7_V2_0203_IDENTIFIER_CODES[keyof typeof HL7_V2_0203_IDENTIFIER_CODES];
35
+ /**
36
+ * Identifier type tokens accepted by the hosted individual DID builders.
37
+ *
38
+ * `DL`, `PPN`, `NN`, and the other clinical identifier codes retain their
39
+ * HL7 v2-0203 meaning. `UUID`, `EMAIL`, and `PHONE` are GDC index identifier
40
+ * types and must not be described as HL7 codes.
41
+ */
42
+ export declare const SecureIdTypesIndividual: Readonly<{
43
+ readonly DriverLicense: "DL";
44
+ readonly CitizenshipCard: "CZ";
45
+ readonly NationalNumber: "NN";
46
+ readonly PassportNumber: "PPN";
47
+ readonly JurisdictionalHealthNumber: "JHN";
48
+ readonly WorkPermit: "WP";
49
+ readonly StudyPermit: "SP";
50
+ readonly HealthCard: "HC";
51
+ readonly MemberNumber: "MB";
52
+ readonly SubscriberNumber: "SN";
53
+ readonly DonorRecord: "DR";
54
+ readonly PatientIdentifier: "PI";
55
+ readonly Uuid: "UUID";
56
+ readonly Email: "EMAIL";
57
+ readonly Phone: "PHONE";
58
+ }>;
59
+ export type SecureIdTypeIndividual = typeof SecureIdTypesIndividual[keyof typeof SecureIdTypesIndividual];
35
60
  /** Product-neutral document kinds used by subject identifier forms. */
36
61
  export declare const INDIVIDUAL_IDENTIFIER_KINDS: Readonly<{
37
62
  readonly NationalPerson: "national-person";
@@ -31,6 +31,19 @@ export const HL7_V2_0203_IDENTIFIER_CODES = Object.freeze({
31
31
  DonorRecord: 'DR',
32
32
  PatientIdentifier: 'PI',
33
33
  });
34
+ /**
35
+ * Identifier type tokens accepted by the hosted individual DID builders.
36
+ *
37
+ * `DL`, `PPN`, `NN`, and the other clinical identifier codes retain their
38
+ * HL7 v2-0203 meaning. `UUID`, `EMAIL`, and `PHONE` are GDC index identifier
39
+ * types and must not be described as HL7 codes.
40
+ */
41
+ export const SecureIdTypesIndividual = Object.freeze({
42
+ Uuid: 'UUID',
43
+ Email: 'EMAIL',
44
+ Phone: 'PHONE',
45
+ ...HL7_V2_0203_IDENTIFIER_CODES,
46
+ });
34
47
  /** Product-neutral document kinds used by subject identifier forms. */
35
48
  export const INDIVIDUAL_IDENTIFIER_KINDS = Object.freeze({
36
49
  NationalPerson: 'national-person',
@@ -11,14 +11,17 @@
11
11
  */
12
12
  export declare const DataspaceSectors: Readonly<{
13
13
  readonly HealthCare: "health-care";
14
+ readonly HealthLab: "health-lab";
14
15
  readonly HealthResearch: "health-research";
15
16
  readonly HealthTech: "health-tech";
16
17
  readonly HealthInsurance: "health-insurance";
17
18
  readonly AnimalCare: "animal-care";
19
+ readonly AnimalLab: "animal-lab";
18
20
  readonly AnimalResearch: "animal-research";
19
21
  readonly AnimalInsurance: "animal-insurance";
20
22
  readonly AnimalTech: "animal-tech";
21
23
  readonly OneHealthResearch: "onehealth-research";
24
+ readonly OneHealthLab: "onehealth-lab";
22
25
  readonly OneHealthTech: "onehealth-tech";
23
26
  }>;
24
27
  export type DataspaceSector = typeof DataspaceSectors[keyof typeof DataspaceSectors];
@@ -12,13 +12,16 @@
12
12
  */
13
13
  export const DataspaceSectors = Object.freeze({
14
14
  HealthCare: 'health-care',
15
+ HealthLab: 'health-lab',
15
16
  HealthResearch: 'health-research',
16
17
  HealthTech: 'health-tech',
17
18
  HealthInsurance: 'health-insurance',
18
19
  AnimalCare: 'animal-care',
20
+ AnimalLab: 'animal-lab',
19
21
  AnimalResearch: 'animal-research',
20
22
  AnimalInsurance: 'animal-insurance',
21
23
  AnimalTech: 'animal-tech',
22
24
  OneHealthResearch: 'onehealth-research',
25
+ OneHealthLab: 'onehealth-lab',
23
26
  OneHealthTech: 'onehealth-tech',
24
27
  });
@@ -149,6 +149,8 @@ export declare const EXAMPLE_PROVIDER_DOMAIN: "health-care.provider.example.org"
149
149
  /** Synthetic public portal domain used to derive organization path DIDs. */
150
150
  export declare const EXAMPLE_PUBLIC_PORTAL_DOMAIN: "portal.example.org";
151
151
  export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID: string;
152
+ /** Synthetic private UUID used to prove one-way hosted individual DID construction. */
153
+ export declare const EXAMPLE_PRIVATE_INDIVIDUAL_UUID: "a87e5b15-aea4-4475-9c7c-40aa88354b6f";
152
154
  export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_SECONDARY: string;
153
155
  export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_TERTIARY: string;
154
156
  export declare const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_CONDITION: string;
@@ -212,7 +214,13 @@ export declare const EXAMPLE_HEALTHCARE_ACTOR_ROLE_PHYSICIAN: "ISCO-08|2211";
212
214
  export declare const EXAMPLE_HEALTHCARE_ACTOR_ROLE_GENERALIST_MEDICAL_PRACTITIONER: "ISCO-08|2211";
213
215
  export declare const EXAMPLE_HEALTHCARE_ACTOR_ROLE_RECEPTIONIST: "ISCO-08|4226";
214
216
  export declare const EXAMPLE_HEALTHCARE_ROLE_PHYSICIAN_TEXT: "physician";
217
+ export declare const EXAMPLE_INDIVIDUAL_CONTROLLER_ROLE_TYPE = "http://terminology.hl7.org/CodeSystem/v3-RoleCode";
218
+ export declare const EXAMPLE_INDIVIDUAL_CONTROLLER_ROLE_VALUE: "RESPRSN";
215
219
  export declare const EXAMPLE_RELATED_PERSON_ROLE: "v3-RoleCode|RESPRSN";
220
+ export declare const EXAMPLE_SECURE_ID_VALUE_INDIVIDUAL: string;
221
+ export declare const EXAMPLE_SECURE_ID_VALUE_INDIVIDUAL_CONTROLLER: string;
222
+ export declare const EXAMPLE_HOSTED_INDIVIDUAL_DID: string;
223
+ export declare const EXAMPLE_HOSTED_INDIVIDUAL_CONTROLLER_DID: string;
216
224
  export declare const EXAMPLE_RELATED_PERSON_MEMBER_DID: string;
217
225
  export declare const EXAMPLE_CLINICAL_SECTION_RESULTS: "LOINC|30954-2";
218
226
  export declare const EXAMPLE_CLINICAL_SECTION_PATIENT_SUMMARY: "LOINC|60591-5";
@@ -6,12 +6,13 @@ import { FhirCodeSystems } from '../constants/fhir-code-systems.js';
6
6
  import { Format } from '../constants/Schemas.js';
7
7
  import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
8
8
  import { BirthSex, GenderIdentity } from '../constants/identity-gender.js';
9
- import { IdKind } from '../constants/identity-identifiers.js';
9
+ import { IdKind, SecureIdTypesIndividual } from '../constants/identity-identifiers.js';
10
+ import { HL7_CODING_SYSTEM_V3_ROLE_CODE } from '../constants/hl7-roles.js';
10
11
  import { HealthcareActorRoles, HealthcareBasicSections, HealthcareConsentPurposes, HealthcareDocumentTypes, } from '../constants/healthcare.js';
11
12
  import { CommunicationCategoryCodes } from '../constants/communication.js';
12
13
  import { LOINC_SYSTEM_URL } from '../models/clinical-sections.js';
13
14
  import { CommunicationClaim } from '../models/interoperable-claims/communication-claims.js';
14
- import { buildHostedProviderDidWeb, buildIndividualDidWeb, buildIndividualMemberDidWeb, buildProfessionalDidWeb, buildProviderSectorDidWeb, } from '../utils/did.js';
15
+ import { buildHostedProviderDidWeb, buildIndividualDidWeb, buildIndividualMemberDidWeb, buildSecureIdValueIndividual, buildSecureIdValueMember, buildProfessionalDidWeb, buildProviderSectorDidWeb, } from '../utils/did.js';
15
16
  import { encodeHexToMultibase58btc } from '../utils/multibase58.js';
16
17
  import { MedicationStatementClaim, MedicationStatementClaimsFhirApiExtended, } from '../models/interoperable-claims/medication-statement-claims.js';
17
18
  import { medicationStatementFlatToFhirR4 } from '../utils/clinical-resource-converters.js';
@@ -167,6 +168,8 @@ export const EXAMPLE_PROVIDER_DOMAIN = 'health-care.provider.example.org';
167
168
  /** Synthetic public portal domain used to derive organization path DIDs. */
168
169
  export const EXAMPLE_PUBLIC_PORTAL_DOMAIN = 'portal.example.org';
169
170
  export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID = encodeHexToMultibase58btc('a87e5b15aea444759c7c40aa88354b6f');
171
+ /** Synthetic private UUID used to prove one-way hosted individual DID construction. */
172
+ export const EXAMPLE_PRIVATE_INDIVIDUAL_UUID = 'a87e5b15-aea4-4475-9c7c-40aa88354b6f';
170
173
  export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_SECONDARY = encodeHexToMultibase58btc('b98f6c24bfb545849d8d51bb99465c7e');
171
174
  export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_TERTIARY = encodeHexToMultibase58btc('c39f7d35c0c65695ae9e62cca0576d8f');
172
175
  export const EXAMPLE_INDIVIDUAL_MULTIBASE_ID_CONDITION = encodeHexToMultibase58btc('d4a08e46d1d767a6bfaf73ddb1687ea0');
@@ -251,7 +254,28 @@ export const EXAMPLE_HEALTHCARE_ACTOR_ROLE_PHYSICIAN = HealthcareActorRoles.Phys
251
254
  export const EXAMPLE_HEALTHCARE_ACTOR_ROLE_GENERALIST_MEDICAL_PRACTITIONER = HealthcareActorRoles.GeneralistMedicalPractitioner;
252
255
  export const EXAMPLE_HEALTHCARE_ACTOR_ROLE_RECEPTIONIST = 'ISCO-08|4226';
253
256
  export const EXAMPLE_HEALTHCARE_ROLE_PHYSICIAN_TEXT = 'physician';
257
+ export const EXAMPLE_INDIVIDUAL_CONTROLLER_ROLE_TYPE = HL7_CODING_SYSTEM_V3_ROLE_CODE;
258
+ export const EXAMPLE_INDIVIDUAL_CONTROLLER_ROLE_VALUE = EXAMPLE_ORGANIZATION_CONTROLLER_ROLE;
254
259
  export const EXAMPLE_RELATED_PERSON_ROLE = 'v3-RoleCode|RESPRSN';
260
+ export const EXAMPLE_SECURE_ID_VALUE_INDIVIDUAL = buildSecureIdValueIndividual({
261
+ secureIdTypeIndividual: SecureIdTypesIndividual.Uuid,
262
+ privateIdValueIndividual: EXAMPLE_PRIVATE_INDIVIDUAL_UUID,
263
+ });
264
+ export const EXAMPLE_SECURE_ID_VALUE_INDIVIDUAL_CONTROLLER = buildSecureIdValueMember({
265
+ secureIdTypeMember: SecureIdTypesIndividual.Email,
266
+ privateIdValueMember: EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL,
267
+ });
268
+ export const EXAMPLE_HOSTED_INDIVIDUAL_DID = buildIndividualDidWeb({
269
+ providerDidWeb: EXAMPLE_HOSTED_PROVIDER_DID,
270
+ secureIdTypeIndividual: SecureIdTypesIndividual.Uuid,
271
+ secureIdValueIndividual: EXAMPLE_SECURE_ID_VALUE_INDIVIDUAL,
272
+ });
273
+ export const EXAMPLE_HOSTED_INDIVIDUAL_CONTROLLER_DID = buildIndividualMemberDidWeb({
274
+ individualDidWeb: EXAMPLE_HOSTED_INDIVIDUAL_DID,
275
+ memberId: EXAMPLE_SECURE_ID_VALUE_INDIVIDUAL_CONTROLLER,
276
+ roleType: EXAMPLE_INDIVIDUAL_CONTROLLER_ROLE_TYPE,
277
+ roleValue: EXAMPLE_INDIVIDUAL_CONTROLLER_ROLE_VALUE,
278
+ });
255
279
  export const EXAMPLE_RELATED_PERSON_MEMBER_DID = buildIndividualMemberDidWeb({
256
280
  individualDidWeb: EXAMPLE_SUBJECT_DID,
257
281
  role: EXAMPLE_RELATED_PERSON_ROLE,
@@ -1,4 +1,5 @@
1
1
  import { ServiceEndpointSelector } from "../models/did";
2
+ import { type SecureIdTypeIndividual } from '../constants/identity-identifiers';
2
3
  /**
3
4
  * Canonical DID path markers for hosted/provider individual identities.
4
5
  *
@@ -11,6 +12,19 @@ export declare const IndividualDidMarkers: {
11
12
  readonly Member: "member";
12
13
  readonly Role: "role";
13
14
  };
15
+ export declare function buildSecureIdValueIndividual(input: {
16
+ secureIdTypeIndividual: SecureIdTypeIndividual | string;
17
+ privateIdValueIndividual: string;
18
+ }): string;
19
+ /**
20
+ * Builds the same SHA3-384 multihash path value for the private identifier of
21
+ * an individual member/controller. The member and represented individual
22
+ * remain distinct inputs even when both refer to the same natural person.
23
+ */
24
+ export declare function buildSecureIdValueMember(input: {
25
+ secureIdTypeMember: SecureIdTypeIndividual | string;
26
+ privateIdValueMember: string;
27
+ }): string;
14
28
  /**
15
29
  * Removes any coding-system prefix from a role claim when building a member DID.
16
30
  *
@@ -189,38 +203,71 @@ export declare function buildProfessionalDidWeb(input: {
189
203
  * Builds the canonical individual DID under either a hosted-provider DID root
190
204
  * or an external provider-sector DID root.
191
205
  *
192
- * Canonical supported forms:
193
- * - hosted:
194
- * `did:web:<host.domain>:<sector>:organization:taxid:<provider-tax-id>:individual:multibase:<individualId>`
195
- * - external/provider-domain:
196
- * `did:web:<sector.provider.domain>:individual:multibase:<individualId>`
206
+ * Canonical hosted form:
207
+ * `did:web:<provider-path>:individual:<secureIdTypeIndividual>:<secureIdValueIndividual>`
197
208
  *
198
209
  * Important semantics:
199
- * - `individualId` must be the canonical stable identity of the individual
200
- * - `individualId` should already be serialized in multibase form, typically
201
- * `zBase58(UUID-bytes)`
202
- * - role is not part of the canonical individual DID; roles are only appended
203
- * in member DIDs via `:member:role:<role-code-no-coding-system>`
210
+ * - the type remains an explicit upper-case path token such as `UUID`, `DL`,
211
+ * `PPN`, `EMAIL`, or `PHONE`;
212
+ * - the value is always the one-way SHA3-384 multihash built by
213
+ * `buildSecureIdValueIndividual`, never the private identifier in plain text;
214
+ * - role is not part of the individual DID.
204
215
  *
205
216
  * Backward-compatibility:
206
217
  * - `subjectId` is accepted as a legacy alias for `individualId`
207
218
  */
208
219
  export declare function buildIndividualDidWeb(input: {
209
220
  providerDidWeb: string;
221
+ secureIdTypeIndividual?: SecureIdTypeIndividual | string;
222
+ secureIdValueIndividual?: string;
223
+ /** @deprecated Use `secureIdTypeIndividual` plus `secureIdValueIndividual`. */
210
224
  individualId?: string;
225
+ /** @deprecated Use `secureIdTypeIndividual` plus `secureIdValueIndividual`. */
211
226
  subjectId?: string;
212
227
  }): string;
213
228
  /**
214
229
  * Builds the canonical member DID under an individual DID.
215
230
  *
216
231
  * Canonical form:
217
- * `did:web:...:individual:multibase:<individualId>:member:role:<role-code-no-coding-system>`
232
+ * `did:web:...:individual:<type>:<secure-value>:member:<secure-member-value>:<role-value>`
218
233
  *
219
- * The DID suffix intentionally strips coding-system prefixes because the DID is
220
- * a compact routing/identity string, while the full coded role still belongs in
221
- * the claims layer.
234
+ * `roleType` is mandatory input so callers cannot accidentally lose the coded
235
+ * role semantics. Only `roleValue` is serialized in the DID because the full
236
+ * FHIR token (`system|value`) remains in the protected claims/licence layer.
222
237
  */
223
238
  export declare function buildIndividualMemberDidWeb(input: {
224
239
  individualDidWeb: string;
225
- role: string;
240
+ memberId?: string;
241
+ roleType?: string;
242
+ roleValue?: string;
243
+ /** @deprecated Use `roleType` plus `roleValue`. */
244
+ role?: string;
245
+ }): string;
246
+ /**
247
+ * Builds a complete hosted individual-member DID directly from the private
248
+ * identifiers already held by an authorized BFF.
249
+ *
250
+ * This is the integration helper for portals: the provider DID returned by GW,
251
+ * the individual's private UUID or other governed identifier, the logged-in
252
+ * member's email/phone/private identifier, and the protected role type/value
253
+ * are sufficient. Neither private identifier is serialized in the result.
254
+ */
255
+ export declare function buildIndividualMemberDidWebFromPrivateIdentifiers(input: {
256
+ providerDidWeb: string;
257
+ secureIdTypeIndividual: SecureIdTypeIndividual | string;
258
+ privateIdValueIndividual: string;
259
+ secureIdTypeMember: SecureIdTypeIndividual | string;
260
+ privateIdValueMember: string;
261
+ roleType: string;
262
+ roleValue: string;
226
263
  }): string;
264
+ /**
265
+ * Parses one hosted individual-member DID without interpreting its role value
266
+ * as authorization. The complete role system and value must still be checked
267
+ * against the protected licence or credential by the consuming gateway.
268
+ */
269
+ export declare function parseIndividualMemberDidWeb(didWeb: string): {
270
+ individualDidWeb: string;
271
+ memberId: string;
272
+ roleValue: string;
273
+ };
package/dist/utils/did.js CHANGED
@@ -1,6 +1,9 @@
1
1
  // crypto-ts/utils/did.ts
2
2
  // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
3
3
  import { multibase58MultihashSha3_256 } from './same-as.js';
4
+ import { SecureIdTypesIndividual, } from '../constants/identity-identifiers.js';
5
+ import { normalizePhone } from './consent.js';
6
+ import { encodeMultibaseSha3 } from './multibasehash.js';
4
7
  /**
5
8
  * Canonical DID path markers for hosted/provider individual identities.
6
9
  *
@@ -13,6 +16,63 @@ export const IndividualDidMarkers = {
13
16
  Member: 'member',
14
17
  Role: 'role',
15
18
  };
19
+ const UUID_VALUE_PATTERN = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
20
+ const MULTIBASE_SHA3_384_PATTERN = /^z[1-9A-HJ-NP-Za-km-z]+$/;
21
+ /**
22
+ * Builds the privacy-preserving value used by a hosted individual DID.
23
+ *
24
+ * Wire profile: `multibase(base58btc(multihash(SHA3-384, canonical-bytes)))`.
25
+ * The returned value is the bare `z...` path token, not an
26
+ * `urn:multibase:...` value and not a CID.
27
+ *
28
+ * Canonical inputs:
29
+ * - `UUID`: remove hyphens, interpret the 32 hexadecimal digits as the UUID's
30
+ * canonical 16 bytes, then hash those bytes;
31
+ * - `EMAIL`: remove `mailto:`, whitespace and letter-case differences;
32
+ * - `PHONE`: remove `tel:` and normalize the number through the shared phone
33
+ * normalizer (including its E.164 leading `+` when supplied);
34
+ * - HL7 v2-0203 and other governed private identifiers: NFKC-normalized,
35
+ * trimmed, upper-case UTF-8 text. Their type and jurisdiction remain
36
+ * separate governed fields and are never inferred from the value.
37
+ */
38
+ function buildSecureIdValue(typeInput, valueInput, context) {
39
+ const type = String(typeInput || '').trim().toUpperCase();
40
+ const rawValue = String(valueInput || '').trim();
41
+ if (!type)
42
+ throw new Error(`${context} requires a secure identifier type.`);
43
+ if (!rawValue)
44
+ throw new Error(`${context} requires a private identifier value.`);
45
+ if (type === SecureIdTypesIndividual.Uuid) {
46
+ if (!UUID_VALUE_PATTERN.test(rawValue)) {
47
+ throw new Error(`${context} requires a hexadecimal UUID value.`);
48
+ }
49
+ const hex = rawValue.replace(/-/g, '').toLowerCase();
50
+ const bytes = new Uint8Array(16);
51
+ for (let index = 0; index < bytes.length; index += 1) {
52
+ bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
53
+ }
54
+ return encodeMultibaseSha3(bytes, 384);
55
+ }
56
+ const canonicalText = type === SecureIdTypesIndividual.Email
57
+ ? rawValue.replace(/^mailto:/i, '').replace(/\s+/g, '').toLowerCase()
58
+ : type === SecureIdTypesIndividual.Phone
59
+ ? normalizePhone(rawValue.replace(/^tel:/i, ''))
60
+ : rawValue.normalize('NFKC').toUpperCase();
61
+ if (!canonicalText)
62
+ throw new Error(`${context} requires a valid ${type} value.`);
63
+ return encodeMultibaseSha3(canonicalText, 384);
64
+ }
65
+ export function buildSecureIdValueIndividual(input) {
66
+ return buildSecureIdValue(input.secureIdTypeIndividual, input.privateIdValueIndividual, 'buildSecureIdValueIndividual');
67
+ }
68
+ /**
69
+ * Builds the same SHA3-384 multihash path value for the private identifier of
70
+ * an individual member/controller. The member and represented individual
71
+ * remain distinct inputs even when both refer to the same natural person.
72
+ */
73
+ export function buildSecureIdValueMember(input) {
74
+ return buildSecureIdValue(input.secureIdTypeMember, input.privateIdValueMember, 'buildSecureIdValueMember');
75
+ }
16
76
  /**
17
77
  * Removes any coding-system prefix from a role claim when building a member DID.
18
78
  *
@@ -334,27 +394,40 @@ export function buildProfessionalDidWeb(input) {
334
394
  * Builds the canonical individual DID under either a hosted-provider DID root
335
395
  * or an external provider-sector DID root.
336
396
  *
337
- * Canonical supported forms:
338
- * - hosted:
339
- * `did:web:<host.domain>:<sector>:organization:taxid:<provider-tax-id>:individual:multibase:<individualId>`
340
- * - external/provider-domain:
341
- * `did:web:<sector.provider.domain>:individual:multibase:<individualId>`
397
+ * Canonical hosted form:
398
+ * `did:web:<provider-path>:individual:<secureIdTypeIndividual>:<secureIdValueIndividual>`
342
399
  *
343
400
  * Important semantics:
344
- * - `individualId` must be the canonical stable identity of the individual
345
- * - `individualId` should already be serialized in multibase form, typically
346
- * `zBase58(UUID-bytes)`
347
- * - role is not part of the canonical individual DID; roles are only appended
348
- * in member DIDs via `:member:role:<role-code-no-coding-system>`
401
+ * - the type remains an explicit upper-case path token such as `UUID`, `DL`,
402
+ * `PPN`, `EMAIL`, or `PHONE`;
403
+ * - the value is always the one-way SHA3-384 multihash built by
404
+ * `buildSecureIdValueIndividual`, never the private identifier in plain text;
405
+ * - role is not part of the individual DID.
349
406
  *
350
407
  * Backward-compatibility:
351
408
  * - `subjectId` is accepted as a legacy alias for `individualId`
352
409
  */
353
410
  export function buildIndividualDidWeb(input) {
354
411
  const providerDidWeb = String(input.providerDidWeb || '').trim();
412
+ const secureIdTypeIndividual = String(input.secureIdTypeIndividual || '').trim().toUpperCase();
413
+ const secureIdValueIndividual = String(input.secureIdValueIndividual || '').trim();
355
414
  const individualId = String(input.individualId || input.subjectId || '').trim();
356
415
  if (!providerDidWeb)
357
416
  throw new Error('buildIndividualDidWeb requires providerDidWeb.');
417
+ if (secureIdTypeIndividual || secureIdValueIndividual) {
418
+ if (!secureIdTypeIndividual || !secureIdValueIndividual) {
419
+ throw new Error('buildIndividualDidWeb requires both secure individual id type and value.');
420
+ }
421
+ if (!MULTIBASE_SHA3_384_PATTERN.test(secureIdValueIndividual)) {
422
+ throw new Error('buildIndividualDidWeb requires secureIdValueIndividual as bare multibase base58btc.');
423
+ }
424
+ return [
425
+ providerDidWeb,
426
+ IndividualDidMarkers.Individual,
427
+ secureIdTypeIndividual,
428
+ secureIdValueIndividual,
429
+ ].join(':');
430
+ }
358
431
  if (!individualId)
359
432
  throw new Error('buildIndividualDidWeb requires individualId.');
360
433
  return [
@@ -368,20 +441,83 @@ export function buildIndividualDidWeb(input) {
368
441
  * Builds the canonical member DID under an individual DID.
369
442
  *
370
443
  * Canonical form:
371
- * `did:web:...:individual:multibase:<individualId>:member:role:<role-code-no-coding-system>`
444
+ * `did:web:...:individual:<type>:<secure-value>:member:<secure-member-value>:<role-value>`
372
445
  *
373
- * The DID suffix intentionally strips coding-system prefixes because the DID is
374
- * a compact routing/identity string, while the full coded role still belongs in
375
- * the claims layer.
446
+ * `roleType` is mandatory input so callers cannot accidentally lose the coded
447
+ * role semantics. Only `roleValue` is serialized in the DID because the full
448
+ * FHIR token (`system|value`) remains in the protected claims/licence layer.
376
449
  */
377
450
  export function buildIndividualMemberDidWeb(input) {
378
451
  const individualDidWeb = String(input.individualDidWeb || '').trim();
379
452
  if (!individualDidWeb)
380
453
  throw new Error('buildIndividualMemberDidWeb requires individualDidWeb.');
454
+ const memberId = String(input.memberId || '').trim();
455
+ const roleType = String(input.roleType || '').trim();
456
+ const roleValue = String(input.roleValue || '').trim();
457
+ if (memberId || roleType || roleValue) {
458
+ if (!MULTIBASE_SHA3_384_PATTERN.test(memberId)) {
459
+ throw new Error('buildIndividualMemberDidWeb requires memberId as bare multibase base58btc.');
460
+ }
461
+ if (!roleType || !roleValue) {
462
+ throw new Error('buildIndividualMemberDidWeb requires both roleType and roleValue.');
463
+ }
464
+ return [individualDidWeb, IndividualDidMarkers.Member, memberId, roleValue].join(':');
465
+ }
381
466
  return [
382
467
  individualDidWeb,
383
468
  IndividualDidMarkers.Member,
384
469
  IndividualDidMarkers.Role,
385
- toDidMemberRoleCode(input.role),
470
+ toDidMemberRoleCode(String(input.role || '')),
386
471
  ].join(':');
387
472
  }
473
+ /**
474
+ * Builds a complete hosted individual-member DID directly from the private
475
+ * identifiers already held by an authorized BFF.
476
+ *
477
+ * This is the integration helper for portals: the provider DID returned by GW,
478
+ * the individual's private UUID or other governed identifier, the logged-in
479
+ * member's email/phone/private identifier, and the protected role type/value
480
+ * are sufficient. Neither private identifier is serialized in the result.
481
+ */
482
+ export function buildIndividualMemberDidWebFromPrivateIdentifiers(input) {
483
+ const individualDidWeb = buildIndividualDidWeb({
484
+ providerDidWeb: input.providerDidWeb,
485
+ secureIdTypeIndividual: input.secureIdTypeIndividual,
486
+ secureIdValueIndividual: buildSecureIdValueIndividual({
487
+ secureIdTypeIndividual: input.secureIdTypeIndividual,
488
+ privateIdValueIndividual: input.privateIdValueIndividual,
489
+ }),
490
+ });
491
+ return buildIndividualMemberDidWeb({
492
+ individualDidWeb,
493
+ memberId: buildSecureIdValueMember({
494
+ secureIdTypeMember: input.secureIdTypeMember,
495
+ privateIdValueMember: input.privateIdValueMember,
496
+ }),
497
+ roleType: input.roleType,
498
+ roleValue: input.roleValue,
499
+ });
500
+ }
501
+ /**
502
+ * Parses one hosted individual-member DID without interpreting its role value
503
+ * as authorization. The complete role system and value must still be checked
504
+ * against the protected licence or credential by the consuming gateway.
505
+ */
506
+ export function parseIndividualMemberDidWeb(didWeb) {
507
+ const candidate = String(didWeb || '').trim();
508
+ const marker = `:${IndividualDidMarkers.Member}:`;
509
+ const markerOffset = candidate.lastIndexOf(marker);
510
+ if (!candidate.startsWith('did:web:') || markerOffset < 0) {
511
+ throw new Error('Invalid hosted individual member DID.');
512
+ }
513
+ const individualDidWeb = candidate.slice(0, markerOffset);
514
+ const suffix = candidate.slice(markerOffset + marker.length).split(':');
515
+ const [memberId, roleValue, ...unexpected] = suffix;
516
+ if (!individualDidWeb.includes(`:${IndividualDidMarkers.Individual}:`)
517
+ || unexpected.length > 0
518
+ || !MULTIBASE_SHA3_384_PATTERN.test(String(memberId || ''))
519
+ || !String(roleValue || '').trim()) {
520
+ throw new Error('Invalid hosted individual member DID.');
521
+ }
522
+ return { individualDidWeb, memberId, roleValue };
523
+ }
@@ -33,10 +33,11 @@ export type FhirIpsCreatorAuthor = Readonly<{
33
33
  }>;
34
34
  export type FhirIpsCreatorProvenanceInput = FhirIpsCreatorAuthorInput & Readonly<{
35
35
  /**
36
- * Explicit source originator. Defaults to the owning organization or
37
- * individual. When the authenticated member/professional actually authored
38
- * the content, pass its registered RelatedPerson/PractitionerRole assignment
39
- * reference; that same assignment may also remain the attester.
36
+ * Explicit document/source author. Defaults to the owning organization or
37
+ * individual. A registered RelatedPerson/PractitionerRole is an attester,
38
+ * never a substitute for the organization, EHR/portal or individual that
39
+ * authors the document. Passing the assignment reference is normalized to
40
+ * the owner for compatibility with the former creator-as-author contract.
40
41
  */
41
42
  compositionAuthorReference?: string;
42
43
  /** Defaults to professional for employees and personal for individual members. */
@@ -58,13 +59,11 @@ export type FhirIpsCreatorProvenance = Readonly<{
58
59
  /**
59
60
  * Builds source authorship separately from the person or role that attests it.
60
61
  *
61
- * Defaults model delegated flows: an organization's professional attests an
62
- * organization-authored record, while a member/caregiver attests a
63
- * subject-authored record. For content actually created by that authenticated
64
- * member or professional, set `compositionAuthorReference` to its registered
65
- * RelatedPerson or PractitionerRole assignment. Author and attester may then
66
- * intentionally reference the same assignment; neither is the transport
67
- * sender or signing key.
62
+ * An organization's employee attests an organization/EHR-authored record,
63
+ * while a controller/member/caregiver attests an individual-authored record.
64
+ * Neither the attester nor the document author is the transport sender or
65
+ * signing key merely because the same authenticated person performed the
66
+ * submission.
68
67
  */
69
68
  export declare function buildFhirIpsCreatorProvenance(input: FhirIpsCreatorProvenanceInput): FhirIpsCreatorProvenance;
70
69
  /**
@@ -11,13 +11,11 @@ export const FhirIpsCreatorKinds = Object.freeze({
11
11
  /**
12
12
  * Builds source authorship separately from the person or role that attests it.
13
13
  *
14
- * Defaults model delegated flows: an organization's professional attests an
15
- * organization-authored record, while a member/caregiver attests a
16
- * subject-authored record. For content actually created by that authenticated
17
- * member or professional, set `compositionAuthorReference` to its registered
18
- * RelatedPerson or PractitionerRole assignment. Author and attester may then
19
- * intentionally reference the same assignment; neither is the transport
20
- * sender or signing key.
14
+ * An organization's employee attests an organization/EHR-authored record,
15
+ * while a controller/member/caregiver attests an individual-authored record.
16
+ * Neither the attester nor the document author is the transport sender or
17
+ * signing key merely because the same authenticated person performed the
18
+ * submission.
21
19
  */
22
20
  export function buildFhirIpsCreatorProvenance(input) {
23
21
  if (input.kind === FhirIpsCreatorKinds.IndividualSubject) {
@@ -31,11 +29,14 @@ export function buildFhirIpsCreatorProvenance(input) {
31
29
  const defaultAuthor = input.kind === FhirIpsCreatorKinds.Professional
32
30
  ? input.organizationReference
33
31
  : input.subjectReference;
32
+ const requestedAuthor = input.compositionAuthorReference === creator.authorReference
33
+ ? defaultAuthor
34
+ : input.compositionAuthorReference;
34
35
  const mode = input.attesterMode || (input.kind === FhirIpsCreatorKinds.Professional
35
36
  ? CompositionAttesterModes.Professional
36
37
  : CompositionAttesterModes.Personal);
37
38
  return {
38
- authorReference: requireReference(input.compositionAuthorReference || defaultAuthor, 'compositionAuthorReference'),
39
+ authorReference: requireReference(requestedAuthor || defaultAuthor, 'compositionAuthorReference'),
39
40
  attesters: [{
40
41
  mode,
41
42
  party: { reference: creator.authorReference },
@@ -89,6 +90,13 @@ export function buildFhirIpsCreatorAuthor(input) {
89
90
  return {
90
91
  authorReference: authorIdentifier,
91
92
  entries: [
93
+ {
94
+ fullUrl: organizationReference,
95
+ resource: {
96
+ resourceType: 'Organization',
97
+ identifier: [{ value: organizationReference }],
98
+ },
99
+ },
92
100
  {
93
101
  fullUrl: actorIdentifier,
94
102
  resource: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.8.8",
3
+ "version": "2.9.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },