gdc-common-utils-ts 2.8.5 → 2.8.7

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
@@ -522,6 +522,12 @@ The canonical API contract should live in JSDoc on exported code. The README act
522
522
  - The base token examples are read-only; richer scenario fixtures intentionally add `organization/Consent.cruds`.
523
523
  - [`src/examples/related-person.ts`](src/examples/related-person.ts)
524
524
  - RelatedPerson/family-member examples.
525
+ - [`buildFhirIpsCreatorProvenance(...)`](src/utils/fhir-ips-creator-identity.ts)
526
+ and [`docs/101-DIDCOMM-IDENTITY-BOUNDARY.md`](docs/101-DIDCOMM-IDENTITY-BOUNDARY.md)
527
+ - Keep `Composition.author`, `Composition.attester`, DIDComm sender and
528
+ signing key separate. A member-created fact may use the registered
529
+ RelatedPerson as both author and personal attester; a dictated
530
+ individual-created fact keeps the individual as author.
525
531
  - [`src/examples/frontend-session.ts`](src/examples/frontend-session.ts)
526
532
  - Frontend profile/session bootstrap examples.
527
533
  - [`src/examples/lifecycle.ts`](src/examples/lifecycle.ts)
@@ -50,6 +50,18 @@ export interface AuditInfo {
50
50
  txId?: string;
51
51
  /** Transaction timestamp (ISO 8601). */
52
52
  txTime?: string;
53
+ /** Stable semantic creator resolved from authenticated clinical identity. */
54
+ creatorDid?: string;
55
+ /** Authenticated transport actor when different from the source author. */
56
+ submitterDid?: string;
57
+ /** Verified DCR/JWS signing key used by the submitting channel. */
58
+ signingKeyId?: string;
59
+ /** Organization or subject that owns the source author's role/relationship. */
60
+ authorOwnerIdentifier?: string;
61
+ /** Source Composition.date used for strictly-later successor decisions. */
62
+ documentDate?: string;
63
+ /** Source record linked to a derived confidential projection. */
64
+ sourceRecordId?: string;
53
65
  }
54
66
  /**
55
67
  * Public runtime projection kept outside encrypted `content`.
@@ -16,4 +16,12 @@ export declare const CompositionClaim: {
16
16
  readonly Identifier: "Composition.identifier";
17
17
  readonly Title: "Composition.title";
18
18
  };
19
+ /** Canonical FHIR R4 Composition.attester.mode codes. */
20
+ export declare const CompositionAttesterModes: Readonly<{
21
+ readonly Personal: "personal";
22
+ readonly Professional: "professional";
23
+ readonly Legal: "legal";
24
+ readonly Official: "official";
25
+ }>;
26
+ export type CompositionAttesterMode = typeof CompositionAttesterModes[keyof typeof CompositionAttesterModes];
19
27
  export type CompositionClaimKey = typeof CompositionClaim[keyof typeof CompositionClaim];
@@ -18,3 +18,10 @@ export const CompositionClaim = {
18
18
  Identifier: 'Composition.identifier',
19
19
  Title: 'Composition.title',
20
20
  };
21
+ /** Canonical FHIR R4 Composition.attester.mode codes. */
22
+ export const CompositionAttesterModes = Object.freeze({
23
+ Personal: 'personal',
24
+ Professional: 'professional',
25
+ Legal: 'legal',
26
+ Official: 'official',
27
+ });
@@ -0,0 +1,26 @@
1
+ import type { AuditInfo, IndexedAttribute } from '../models/confidential-storage.js';
2
+ export declare const ConfidentialDocumentIndex: Readonly<{
3
+ readonly Sector: "sector";
4
+ readonly CreatorDid: "audit.creatorDid";
5
+ readonly SubmitterDid: "audit.submitterDid";
6
+ readonly SigningKeyId: "audit.signingKeyId";
7
+ }>;
8
+ /** Flat Composition claims whose string value is a positional or set-like CSV projection. */
9
+ export declare const CompositionCsvClaimKeys: readonly ["Composition.author", "Composition.attester", "Composition.attester-mode", "Composition.attester-time", "Composition.section", "Composition.entry"];
10
+ export type ConfidentialDocumentProvenanceAudit = Pick<AuditInfo, 'creatorDid' | 'submitterDid' | 'signingKeyId'>;
11
+ export type ConfidentialDocumentIndexedAttributesInput = Readonly<{
12
+ claims: Readonly<Record<string, unknown>>;
13
+ sector?: string;
14
+ audit?: ConfidentialDocumentProvenanceAudit;
15
+ /**
16
+ * Temporarily retain the former whole-CSV attribute so exact legacy queries
17
+ * continue to match while consumers move to one attribute per value.
18
+ */
19
+ includeDeprecatedCsvAggregate?: boolean;
20
+ }>;
21
+ /**
22
+ * Projects searchable metadata without changing the protected canonical
23
+ * claims. CSV multiplicity is governed by `CompositionCsvClaimKeys`; arbitrary
24
+ * text containing commas is never split.
25
+ */
26
+ export declare function buildConfidentialDocumentIndexedAttributes(input: ConfidentialDocumentIndexedAttributesInput): IndexedAttribute[];
@@ -0,0 +1,68 @@
1
+ // Copyright 2026 Conéctate Soluciones y Aplicaciones SL under the Apache License, Version 2.0.
2
+ import { CompositionClaim } from '../models/interoperable-claims/composition-claims.js';
3
+ export const ConfidentialDocumentIndex = Object.freeze({
4
+ Sector: 'sector',
5
+ CreatorDid: 'audit.creatorDid',
6
+ SubmitterDid: 'audit.submitterDid',
7
+ SigningKeyId: 'audit.signingKeyId',
8
+ });
9
+ /** Flat Composition claims whose string value is a positional or set-like CSV projection. */
10
+ export const CompositionCsvClaimKeys = Object.freeze([
11
+ CompositionClaim.Author,
12
+ CompositionClaim.Attester,
13
+ CompositionClaim.AttesterMode,
14
+ CompositionClaim.AttesterTime,
15
+ CompositionClaim.Section,
16
+ CompositionClaim.Entry,
17
+ ]);
18
+ const CSV_CLAIMS = new Set(CompositionCsvClaimKeys);
19
+ const COMPOSITION_CLAIMS = new Set(Object.values(CompositionClaim));
20
+ function canonicalCompositionClaimName(name) {
21
+ return [...COMPOSITION_CLAIMS].find((claimName) => name === claimName || name.endsWith(`.${claimName}`));
22
+ }
23
+ /**
24
+ * Projects searchable metadata without changing the protected canonical
25
+ * claims. CSV multiplicity is governed by `CompositionCsvClaimKeys`; arbitrary
26
+ * text containing commas is never split.
27
+ */
28
+ export function buildConfidentialDocumentIndexedAttributes(input) {
29
+ const attributes = [];
30
+ const includeDeprecatedAggregate = input.includeDeprecatedCsvAggregate !== false;
31
+ const append = (name, value, unique) => {
32
+ const normalized = String(value ?? '').trim();
33
+ if (!normalized)
34
+ return;
35
+ if (attributes.some((attribute) => attribute.name === name && attribute.value === normalized))
36
+ return;
37
+ attributes.push({ name, value: normalized, ...(unique ? { unique: true } : {}) });
38
+ };
39
+ append(ConfidentialDocumentIndex.Sector, input.sector);
40
+ append(ConfidentialDocumentIndex.CreatorDid, input.audit?.creatorDid);
41
+ append(ConfidentialDocumentIndex.SubmitterDid, input.audit?.submitterDid);
42
+ append(ConfidentialDocumentIndex.SigningKeyId, input.audit?.signingKeyId);
43
+ for (const [name, rawValue] of Object.entries(input.claims)) {
44
+ if (name === '@context' || name === '@type' || rawValue === undefined || rawValue === null || Array.isArray(rawValue)) {
45
+ continue;
46
+ }
47
+ const value = String(rawValue).trim();
48
+ if (!value)
49
+ continue;
50
+ const canonicalCompositionName = canonicalCompositionClaimName(name);
51
+ const indexedName = canonicalCompositionName || name;
52
+ if (CSV_CLAIMS.has(indexedName)) {
53
+ const values = value.split(',').map((item) => item.trim()).filter(Boolean);
54
+ values.forEach((item) => append(indexedName, item));
55
+ if (includeDeprecatedAggregate && values.length > 1)
56
+ append(indexedName, value);
57
+ if (name !== indexedName)
58
+ append(name, value);
59
+ continue;
60
+ }
61
+ const isUniqueResourceIdentifier = !canonicalCompositionName
62
+ && (name.endsWith('.identifier') || name.endsWith('.identifier.value'));
63
+ append(indexedName, value, isUniqueResourceIdentifier);
64
+ if (name !== indexedName)
65
+ append(name, value);
66
+ }
67
+ return attributes;
68
+ }
@@ -70,6 +70,21 @@ export declare function createHostedDidWeb(hostDidWeb: string, tenantId: string,
70
70
  * used by the hosted organization form.
71
71
  */
72
72
  export declare function extractTenantIdFromHostedDidWeb(did: string): string | undefined;
73
+ /**
74
+ * Builds the canonical hosted organization DID from the official identifier
75
+ * vocabulary governed by the selected data space.
76
+ *
77
+ * The identifier type is a path token such as `taxID`, `VAT`, or `BN`; this
78
+ * helper normalizes only that token and never guesses a type by parsing the
79
+ * identifier value. The corresponding FHIR Organization must retain the same
80
+ * type and value in `Organization.identifier`.
81
+ */
82
+ export declare function buildHostedOrganizationDidWeb(input: {
83
+ hostDomain: string;
84
+ sector: string;
85
+ officialIdentifierType: string;
86
+ officialIdentifierValue: string;
87
+ }): string;
73
88
  /**
74
89
  * Builds the canonical hosted provider DID root used by hosted tenant services.
75
90
  *
package/dist/utils/did.js CHANGED
@@ -150,6 +150,32 @@ export function extractTenantIdFromHostedDidWeb(did) {
150
150
  }
151
151
  return decodeURIComponent(tenantId);
152
152
  }
153
+ /**
154
+ * Builds the canonical hosted organization DID from the official identifier
155
+ * vocabulary governed by the selected data space.
156
+ *
157
+ * The identifier type is a path token such as `taxID`, `VAT`, or `BN`; this
158
+ * helper normalizes only that token and never guesses a type by parsing the
159
+ * identifier value. The corresponding FHIR Organization must retain the same
160
+ * type and value in `Organization.identifier`.
161
+ */
162
+ export function buildHostedOrganizationDidWeb(input) {
163
+ const hostDomain = String(input.hostDomain || '').trim().toLowerCase();
164
+ const sector = String(input.sector || '').trim().toLowerCase();
165
+ const identifierType = String(input.officialIdentifierType || '').trim().toLowerCase();
166
+ const identifierValue = String(input.officialIdentifierValue || '').trim();
167
+ if (!hostDomain)
168
+ throw new Error('buildHostedOrganizationDidWeb requires hostDomain.');
169
+ if (!sector)
170
+ throw new Error('buildHostedOrganizationDidWeb requires sector.');
171
+ if (!/^[a-z][a-z0-9._-]*$/.test(identifierType)) {
172
+ throw new Error('buildHostedOrganizationDidWeb requires a canonical officialIdentifierType path token.');
173
+ }
174
+ if (!identifierValue || /[:/\s]/.test(identifierValue)) {
175
+ throw new Error('buildHostedOrganizationDidWeb requires one DID-path-safe officialIdentifierValue.');
176
+ }
177
+ return `did:web:${hostDomain}:${sector}:organization:${identifierType}:${identifierValue}`;
178
+ }
153
179
  /**
154
180
  * Builds the canonical hosted provider DID root used by hosted tenant services.
155
181
  *
@@ -170,7 +196,12 @@ export function buildHostedProviderDidWeb(input) {
170
196
  throw new Error('buildHostedProviderDidWeb requires sector.');
171
197
  if (!providerTaxId)
172
198
  throw new Error('buildHostedProviderDidWeb requires providerTaxId.');
173
- return `did:web:${hostDomain}:${sector}:organization:taxid:${providerTaxId}`;
199
+ return buildHostedOrganizationDidWeb({
200
+ hostDomain,
201
+ sector,
202
+ officialIdentifierType: 'taxid',
203
+ officialIdentifierValue: providerTaxId,
204
+ });
174
205
  }
175
206
  /**
176
207
  * Builds the canonical public provider DID root used by an external provider domain.
@@ -1,3 +1,4 @@
1
+ import { type CompositionAttesterMode } from '../models/interoperable-claims/composition-claims.js';
1
2
  export declare const FhirIpsCreatorKinds: Readonly<{
2
3
  readonly IndividualSubject: "individual-subject";
3
4
  readonly IndividualMember: "individual-member";
@@ -30,6 +31,42 @@ export type FhirIpsCreatorAuthor = Readonly<{
30
31
  authorReference: string;
31
32
  entries: readonly FhirIpsCreatorBundleEntry[];
32
33
  }>;
34
+ export type FhirIpsCreatorProvenanceInput = FhirIpsCreatorAuthorInput & Readonly<{
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.
40
+ */
41
+ compositionAuthorReference?: string;
42
+ /** Defaults to professional for employees and personal for individual members. */
43
+ attesterMode?: CompositionAttesterMode;
44
+ /** Optional FHIR instant copied to Composition.attester.time. */
45
+ attestedAt?: string;
46
+ }>;
47
+ export type FhirIpsCreatorProvenance = Readonly<{
48
+ authorReference: string;
49
+ attesters: readonly Readonly<{
50
+ mode: CompositionAttesterMode;
51
+ party: Readonly<{
52
+ reference: string;
53
+ }>;
54
+ time?: string;
55
+ }>[];
56
+ entries: readonly FhirIpsCreatorBundleEntry[];
57
+ }>;
58
+ /**
59
+ * Builds source authorship separately from the person or role that attests it.
60
+ *
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.
68
+ */
69
+ export declare function buildFhirIpsCreatorProvenance(input: FhirIpsCreatorProvenanceInput): FhirIpsCreatorProvenance;
33
70
  /**
34
71
  * Builds the ordinary FHIR R4 resources referenced by an IPS
35
72
  * `Composition.author`.
@@ -39,6 +76,10 @@ export type FhirIpsCreatorAuthor = Readonly<{
39
76
  * ONESELF references the existing Patient/subject entry. A professional
40
77
  * references PractitionerRole while retaining the underlying Practitioner.
41
78
  * An individual member references RelatedPerson.
79
+ *
80
+ * @deprecated Use `buildFhirIpsCreatorProvenance` so Composition.author and
81
+ * Composition.attester remain separate. This compatibility helper preserves
82
+ * its historical role-as-author result.
42
83
  */
43
84
  export declare function buildFhirIpsCreatorAuthor(input: FhirIpsCreatorAuthorInput): FhirIpsCreatorAuthor;
44
85
  type ClinicalCreatorChannelAliases = Readonly<{
@@ -2,11 +2,48 @@
2
2
  import { getHealthcareRoleByClaim } from '../constants/healthcare.js';
3
3
  import { isStableActorIdentifier } from './actor-identifier.js';
4
4
  import { normalizeUuid } from './normalize-uuid.js';
5
+ import { CompositionAttesterModes, } from '../models/interoperable-claims/composition-claims.js';
5
6
  export const FhirIpsCreatorKinds = Object.freeze({
6
7
  IndividualSubject: 'individual-subject',
7
8
  IndividualMember: 'individual-member',
8
9
  Professional: 'professional',
9
10
  });
11
+ /**
12
+ * Builds source authorship separately from the person or role that attests it.
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.
21
+ */
22
+ export function buildFhirIpsCreatorProvenance(input) {
23
+ if (input.kind === FhirIpsCreatorKinds.IndividualSubject) {
24
+ return {
25
+ authorReference: requireReference(input.compositionAuthorReference || input.subjectReference, 'compositionAuthorReference'),
26
+ attesters: [],
27
+ entries: [],
28
+ };
29
+ }
30
+ const creator = buildFhirIpsCreatorAuthor(input);
31
+ const defaultAuthor = input.kind === FhirIpsCreatorKinds.Professional
32
+ ? input.organizationReference
33
+ : input.subjectReference;
34
+ const mode = input.attesterMode || (input.kind === FhirIpsCreatorKinds.Professional
35
+ ? CompositionAttesterModes.Professional
36
+ : CompositionAttesterModes.Personal);
37
+ return {
38
+ authorReference: requireReference(input.compositionAuthorReference || defaultAuthor, 'compositionAuthorReference'),
39
+ attesters: [{
40
+ mode,
41
+ party: { reference: creator.authorReference },
42
+ ...(input.attestedAt ? { time: input.attestedAt } : {}),
43
+ }],
44
+ entries: creator.entries,
45
+ };
46
+ }
10
47
  /**
11
48
  * Builds the ordinary FHIR R4 resources referenced by an IPS
12
49
  * `Composition.author`.
@@ -16,6 +53,10 @@ export const FhirIpsCreatorKinds = Object.freeze({
16
53
  * ONESELF references the existing Patient/subject entry. A professional
17
54
  * references PractitionerRole while retaining the underlying Practitioner.
18
55
  * An individual member references RelatedPerson.
56
+ *
57
+ * @deprecated Use `buildFhirIpsCreatorProvenance` so Composition.author and
58
+ * Composition.attester remain separate. This compatibility helper preserves
59
+ * its historical role-as-author result.
19
60
  */
20
61
  export function buildFhirIpsCreatorAuthor(input) {
21
62
  const actorIdentifier = requireUuidUrn(input.actorIdentifier, 'actorIdentifier');
@@ -29,7 +70,7 @@ export function buildFhirIpsCreatorAuthor(input) {
29
70
  }
30
71
  const role = requireRoleCoding(input.role);
31
72
  if (input.kind === FhirIpsCreatorKinds.IndividualMember) {
32
- const subjectReference = requireUuidUrn(input.subjectReference, 'subjectReference');
73
+ const subjectReference = requireReference(input.subjectReference, 'subjectReference');
33
74
  return {
34
75
  authorReference: authorIdentifier,
35
76
  entries: [{
@@ -52,6 +52,7 @@ export * from './bundle-didcomm-payload';
52
52
  export * from './communication-didcomm-payload';
53
53
  export * from './confidential-storage-persistence';
54
54
  export * from './confidential-storage-test-data';
55
+ export * from './confidential-document-provenance';
55
56
  export * from './permission-templates';
56
57
  export * from './client-assertion';
57
58
  export * from './professional-smart';
@@ -52,6 +52,7 @@ export * from './bundle-didcomm-payload.js';
52
52
  export * from './communication-didcomm-payload.js';
53
53
  export * from './confidential-storage-persistence.js';
54
54
  export * from './confidential-storage-test-data.js';
55
+ export * from './confidential-document-provenance.js';
55
56
  export * from './permission-templates.js';
56
57
  export * from './client-assertion.js';
57
58
  export * from './professional-smart.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.8.5",
3
+ "version": "2.8.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },