gdc-common-utils-ts 2.0.17 → 2.0.18

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.
@@ -36,5 +36,12 @@ export declare const ActivationCredentialTypes: Readonly<{
36
36
  export declare const ProfessionalCredentialTypes: Readonly<{
37
37
  EmployeeCredential: "EmployeeCredential";
38
38
  }>;
39
+ /**
40
+ * Canonical credential subtype names used by inter-tenant authorization
41
+ * contracts for cross-organization access.
42
+ */
43
+ export declare const ContractCredentialTypes: Readonly<{
44
+ InterTenantAccessContractCredential: "InterTenantAccessContractCredential";
45
+ }>;
39
46
  export declare const ORGANIZATION_ACTIVATION_VC_TYPES: readonly ("OrganizationCredential" | "LegalOrganizationCredential")[];
40
47
  export declare const REPRESENTATIVE_ACTIVATION_VC_TYPES: readonly ("LegalRepresentativeCredential" | "PersonCredential")[];
@@ -38,6 +38,13 @@ export const ActivationCredentialTypes = Object.freeze({
38
38
  export const ProfessionalCredentialTypes = Object.freeze({
39
39
  EmployeeCredential: 'EmployeeCredential',
40
40
  });
41
+ /**
42
+ * Canonical credential subtype names used by inter-tenant authorization
43
+ * contracts for cross-organization access.
44
+ */
45
+ export const ContractCredentialTypes = Object.freeze({
46
+ InterTenantAccessContractCredential: 'InterTenantAccessContractCredential',
47
+ });
41
48
  export const ORGANIZATION_ACTIVATION_VC_TYPES = Object.freeze([
42
49
  ActivationCredentialTypes.OrganizationCredential,
43
50
  ActivationCredentialTypes.LegalOrganizationCredential,
@@ -8,6 +8,7 @@
8
8
  export * from './shared';
9
9
  export * from './ica-activation-proof';
10
10
  export * from './organization-controller';
11
+ export * from './inter-tenant-access-contract';
11
12
  export * from './individual-controller';
12
13
  export * from './professional';
13
14
  export * from './related-person';
@@ -9,6 +9,7 @@
9
9
  export * from './shared.js';
10
10
  export * from './ica-activation-proof.js';
11
11
  export * from './organization-controller.js';
12
+ export * from './inter-tenant-access-contract.js';
12
13
  export * from './individual-controller.js';
13
14
  export * from './professional.js';
14
15
  export * from './related-person.js';
@@ -10,6 +10,7 @@ export * from './professional';
10
10
  export * from './employee';
11
11
  export * from './license';
12
12
  export * from './invoice';
13
+ export * from './inter-tenant-access-contract';
13
14
  export * from './related-person';
14
15
  export * from './consent-access';
15
16
  export * from './relationship-access';
@@ -10,6 +10,7 @@ export * from './professional.js';
10
10
  export * from './employee.js';
11
11
  export * from './license.js';
12
12
  export * from './invoice.js';
13
+ export * from './inter-tenant-access-contract.js';
13
14
  export * from './related-person.js';
14
15
  export * from './consent-access.js';
15
16
  export * from './relationship-access.js';
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Shared synthetic inter-tenant contract fixtures reused by GW and SDK tests.
3
+ *
4
+ * Scenario:
5
+ * - `acme-id` is the clinical provider tenant in `health-care`
6
+ * - `lab-id` is the research tenant in `health-research`
7
+ * - both are assumed to be hosted by the same operator for the current scope
8
+ *
9
+ * Programmer hints:
10
+ * - provider = the tenant that owns/exposes the data
11
+ * - consumer = the foreign tenant asking for access
12
+ * - capability = the allowed technical scope, for example
13
+ * `organization/Composition.rs`
14
+ * - purpose = the allowed business/legal reason, for example `RESEARCH`
15
+ * - instantiates-uri = primary agreement artifact URL/CID, typically the
16
+ * signed PDF of the contract itself
17
+ * - invoice/payment evidence is intentionally not modeled as a mandatory core
18
+ * field in this first inter-tenant fixture
19
+ */
20
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_ID: "urn:uuid:inter-tenant-access-contract-001";
21
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM: "2026-06-29T00:00:00.000Z";
22
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_UNTIL: "2027-06-29T00:00:00.000Z";
23
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PURPOSE: "RESEARCH";
24
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SCOPE: "organization/Composition.rs";
25
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SUBJECT_DID: "did:web:api.acme.org:individual:123";
26
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SECTION: "LOINC|48765-2";
27
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_AGREEMENT_PDF_URL: "https://portal.example.org/files/contracts/inter-tenant-contract-acme-lab-001.pdf";
28
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_PROFESSIONAL_DID: "did:web:api.lab.org:employee:researcher1@lab.org:ISCO-08|2211";
29
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SMART_SCOPE: "organization/Composition.rs?subject=did:web:api.acme.org:individual:123&section=LOINC|48765-2";
30
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CLAIMS: Readonly<{
31
+ readonly "Contract.identifier": "urn:uuid:inter-tenant-access-contract-001";
32
+ readonly "Contract.status": "executed";
33
+ readonly "Contract.issued": "2026-06-29T00:00:00.000Z";
34
+ readonly "Contract.applies-start": "2026-06-29T00:00:00.000Z";
35
+ readonly "Contract.applies-end": "2027-06-29T00:00:00.000Z";
36
+ readonly "Contract.provider-organization": "did:web:api.acme.org";
37
+ readonly "Contract.consumer-organization": "did:web:api.lab.org";
38
+ readonly "Contract.provider-controller": "did:web:people.acme.org:controllers:primary";
39
+ readonly "Contract.consumer-controller": "did:web:people.lab.org:controllers:primary";
40
+ readonly "Contract.security-label": "organization/Composition.rs";
41
+ readonly "Contract.term-type": "RESEARCH";
42
+ readonly "Contract.instantiates-uri": "https://portal.example.org/files/contracts/inter-tenant-contract-acme-lab-001.pdf";
43
+ }>;
44
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_RESOURCE: Readonly<{
45
+ [x: string]: any;
46
+ }>;
47
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CREDENTIAL: Readonly<import("..").VerifiableCredentialV2>;
48
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT: Readonly<{
49
+ readonly providerTenantId: "acme-id";
50
+ readonly providerSector: "health-care";
51
+ readonly providerOrganizationDid: "did:web:api.acme.org";
52
+ readonly consumerTenantId: "lab-id";
53
+ readonly consumerSector: "health-research";
54
+ readonly consumerOrganizationDid: "did:web:api.lab.org";
55
+ readonly jurisdiction: "ES";
56
+ readonly actorRole: "ISCO-08|2211";
57
+ readonly subjectDid: "did:web:api.acme.org:individual:123";
58
+ readonly consumerProfessionalDid: "did:web:api.lab.org:employee:researcher1@lab.org:ISCO-08|2211";
59
+ readonly requestedScope: "organization/Composition.rs";
60
+ readonly requestedSection: "LOINC|48765-2";
61
+ readonly smartScope: "organization/Composition.rs?subject=did:web:api.acme.org:individual:123&section=LOINC|48765-2";
62
+ readonly purpose: "RESEARCH";
63
+ }>;
64
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PROVIDER_ORGANIZATION_URN: string;
65
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_ORGANIZATION_URN: string;
66
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_MEMBER_URN: string;
67
+ export declare const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT_WITH_URNS: Readonly<{
68
+ readonly providerOrganizationUrn: string;
69
+ readonly consumerOrganizationUrn: string;
70
+ readonly consumerMemberUrn: string;
71
+ readonly providerTenantId: "acme-id";
72
+ readonly providerSector: "health-care";
73
+ readonly providerOrganizationDid: "did:web:api.acme.org";
74
+ readonly consumerTenantId: "lab-id";
75
+ readonly consumerSector: "health-research";
76
+ readonly consumerOrganizationDid: "did:web:api.lab.org";
77
+ readonly jurisdiction: "ES";
78
+ readonly actorRole: "ISCO-08|2211";
79
+ readonly subjectDid: "did:web:api.acme.org:individual:123";
80
+ readonly consumerProfessionalDid: "did:web:api.lab.org:employee:researcher1@lab.org:ISCO-08|2211";
81
+ readonly requestedScope: "organization/Composition.rs";
82
+ readonly requestedSection: "LOINC|48765-2";
83
+ readonly smartScope: "organization/Composition.rs?subject=did:web:api.acme.org:individual:123&section=LOINC|48765-2";
84
+ readonly purpose: "RESEARCH";
85
+ }>;
86
+ /**
87
+ * Consent-style organization-to-employee delegation rule reused for the
88
+ * consumer tenant side.
89
+ *
90
+ * This intentionally reuses the existing `Consent.*` rule pattern:
91
+ * - `Consent.subject` is the consumer organization URN
92
+ * - `Consent.actor-identifier` is the delegated member URN
93
+ * - `Consent.actor-role` carries the role separately
94
+ * - `Consent.source-reference` points to the blockchain-safe contract VC
95
+ * reference that was or will be anchored on-chain
96
+ */
97
+ export declare const EXAMPLE_INTER_TENANT_EMPLOYEE_CONTRACT_AUTHORIZATION_CONSENT: Readonly<{
98
+ readonly '@context': "org.hl7.fhir.api";
99
+ readonly "Consent.identifier": "urn:uuid:employee-contract-authorization-001";
100
+ readonly "Consent.subject": string;
101
+ readonly "Consent.actor-identifier": string;
102
+ readonly "Consent.actor-role": "ISCO-08|2211";
103
+ readonly "Consent.decision": "permit";
104
+ readonly "Consent.purpose": "RESEARCH";
105
+ readonly "Consent.action": "organization/Composition.rs";
106
+ readonly "Consent.source-reference": string | undefined;
107
+ readonly "Consent.date": "2026-06-29T00:00:00.000Z";
108
+ readonly "Consent.period-start": "2026-06-29T00:00:00.000Z";
109
+ readonly "Consent.period-end": "2027-06-29T00:00:00.000Z";
110
+ }>;
111
+ export declare const EXAMPLE_INTER_TENANT_PROVIDER_CONTRACT_AUTHORIZATION_CONSENT: Readonly<{
112
+ readonly '@context': "org.hl7.fhir.api";
113
+ readonly "Consent.identifier": "urn:uuid:provider-contract-authorization-001";
114
+ readonly "Consent.subject": string;
115
+ readonly "Consent.actor-identifier": string;
116
+ readonly "Consent.actor-role": "organization";
117
+ readonly "Consent.decision": "permit";
118
+ readonly "Consent.purpose": "RESEARCH";
119
+ readonly "Consent.action": "organization/Composition.rs";
120
+ readonly "Consent.source-reference": string | undefined;
121
+ readonly "Consent.date": "2026-06-29T00:00:00.000Z";
122
+ readonly "Consent.period-start": "2026-06-29T00:00:00.000Z";
123
+ readonly "Consent.period-end": "2027-06-29T00:00:00.000Z";
124
+ }>;
@@ -0,0 +1,134 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { ServiceCapability } from '../constants/service-capabilities.js';
3
+ import { DataspaceSectors } from '../constants/sectors.js';
4
+ import { ClaimConsent } from '../models/consent-rule.js';
5
+ import { ClaimInterTenantAccessContract } from '../models/inter-tenant-access-contract.js';
6
+ import { buildInterTenantAccessContractCredential, buildInterTenantAccessContractResource, getInterTenantAccessContractBlockchainReference, } from '../utils/inter-tenant-access-contract.js';
7
+ import { buildMemberAuthorizationUrn, buildOrganizationAuthorizationUrn, } from '../utils/organization-authorization-urn.js';
8
+ import { EXAMPLE_API_ORGANIZATION_DID, EXAMPLE_CONTROLLER_DID, EXAMPLE_HEALTHCARE_ACTOR_ROLE_PHYSICIAN, EXAMPLE_JURISDICTION, EXAMPLE_RESEARCH_API_ORGANIZATION_DID, EXAMPLE_RESEARCH_CONTROLLER_DID, EXAMPLE_RESEARCH_TENANT_IDENTIFIER, EXAMPLE_SECTOR, EXAMPLE_TENANT_IDENTIFIER, } from './shared.js';
9
+ /**
10
+ * Shared synthetic inter-tenant contract fixtures reused by GW and SDK tests.
11
+ *
12
+ * Scenario:
13
+ * - `acme-id` is the clinical provider tenant in `health-care`
14
+ * - `lab-id` is the research tenant in `health-research`
15
+ * - both are assumed to be hosted by the same operator for the current scope
16
+ *
17
+ * Programmer hints:
18
+ * - provider = the tenant that owns/exposes the data
19
+ * - consumer = the foreign tenant asking for access
20
+ * - capability = the allowed technical scope, for example
21
+ * `organization/Composition.rs`
22
+ * - purpose = the allowed business/legal reason, for example `RESEARCH`
23
+ * - instantiates-uri = primary agreement artifact URL/CID, typically the
24
+ * signed PDF of the contract itself
25
+ * - invoice/payment evidence is intentionally not modeled as a mandatory core
26
+ * field in this first inter-tenant fixture
27
+ */
28
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_ID = 'urn:uuid:inter-tenant-access-contract-001';
29
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM = '2026-06-29T00:00:00.000Z';
30
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_UNTIL = '2027-06-29T00:00:00.000Z';
31
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PURPOSE = 'RESEARCH';
32
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SCOPE = ServiceCapability.IndexReader;
33
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SUBJECT_DID = 'did:web:api.acme.org:individual:123';
34
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SECTION = 'LOINC|48765-2';
35
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_AGREEMENT_PDF_URL = 'https://portal.example.org/files/contracts/inter-tenant-contract-acme-lab-001.pdf';
36
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_PROFESSIONAL_DID = `${EXAMPLE_RESEARCH_API_ORGANIZATION_DID}:employee:researcher1@lab.org:${EXAMPLE_HEALTHCARE_ACTOR_ROLE_PHYSICIAN}`;
37
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SMART_SCOPE = `${EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SCOPE}?subject=${EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SUBJECT_DID}&section=${EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SECTION}`;
38
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CLAIMS = Object.freeze({
39
+ [ClaimInterTenantAccessContract.identifier]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_ID,
40
+ [ClaimInterTenantAccessContract.status]: 'executed',
41
+ [ClaimInterTenantAccessContract.issued]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM,
42
+ [ClaimInterTenantAccessContract.appliesStart]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM,
43
+ [ClaimInterTenantAccessContract.appliesEnd]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_UNTIL,
44
+ [ClaimInterTenantAccessContract.providerOrganization]: EXAMPLE_API_ORGANIZATION_DID,
45
+ [ClaimInterTenantAccessContract.consumerOrganization]: EXAMPLE_RESEARCH_API_ORGANIZATION_DID,
46
+ [ClaimInterTenantAccessContract.providerController]: EXAMPLE_CONTROLLER_DID,
47
+ [ClaimInterTenantAccessContract.consumerController]: EXAMPLE_RESEARCH_CONTROLLER_DID,
48
+ [ClaimInterTenantAccessContract.capability]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SCOPE,
49
+ [ClaimInterTenantAccessContract.purpose]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PURPOSE,
50
+ [ClaimInterTenantAccessContract.instantiatesUri]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_AGREEMENT_PDF_URL,
51
+ });
52
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_RESOURCE = Object.freeze(buildInterTenantAccessContractResource(EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CLAIMS));
53
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CREDENTIAL = Object.freeze(buildInterTenantAccessContractCredential({
54
+ claims: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CLAIMS,
55
+ issuer: EXAMPLE_CONTROLLER_DID,
56
+ validFrom: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM,
57
+ validUntil: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_UNTIL,
58
+ additionalCredential: {
59
+ id: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_ID,
60
+ },
61
+ }));
62
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT = Object.freeze({
63
+ providerTenantId: EXAMPLE_TENANT_IDENTIFIER,
64
+ providerSector: EXAMPLE_SECTOR,
65
+ providerOrganizationDid: EXAMPLE_API_ORGANIZATION_DID,
66
+ consumerTenantId: EXAMPLE_RESEARCH_TENANT_IDENTIFIER,
67
+ consumerSector: DataspaceSectors.HealthResearch,
68
+ consumerOrganizationDid: EXAMPLE_RESEARCH_API_ORGANIZATION_DID,
69
+ jurisdiction: EXAMPLE_JURISDICTION,
70
+ actorRole: EXAMPLE_HEALTHCARE_ACTOR_ROLE_PHYSICIAN,
71
+ subjectDid: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SUBJECT_DID,
72
+ consumerProfessionalDid: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_PROFESSIONAL_DID,
73
+ requestedScope: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SCOPE,
74
+ requestedSection: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SECTION,
75
+ smartScope: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_SMART_SCOPE,
76
+ purpose: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PURPOSE,
77
+ });
78
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PROVIDER_ORGANIZATION_URN = buildOrganizationAuthorizationUrn({
79
+ identifierType: 'TAX',
80
+ identifierValue: EXAMPLE_TENANT_IDENTIFIER,
81
+ });
82
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_ORGANIZATION_URN = buildOrganizationAuthorizationUrn({
83
+ identifierType: 'TAX',
84
+ identifierValue: EXAMPLE_RESEARCH_TENANT_IDENTIFIER,
85
+ });
86
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_MEMBER_URN = buildMemberAuthorizationUrn({
87
+ organizationUrn: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_ORGANIZATION_URN,
88
+ memberId: 'researcher-001',
89
+ });
90
+ export const EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT_WITH_URNS = Object.freeze({
91
+ ...EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT,
92
+ providerOrganizationUrn: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PROVIDER_ORGANIZATION_URN,
93
+ consumerOrganizationUrn: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_ORGANIZATION_URN,
94
+ consumerMemberUrn: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_MEMBER_URN,
95
+ });
96
+ /**
97
+ * Consent-style organization-to-employee delegation rule reused for the
98
+ * consumer tenant side.
99
+ *
100
+ * This intentionally reuses the existing `Consent.*` rule pattern:
101
+ * - `Consent.subject` is the consumer organization URN
102
+ * - `Consent.actor-identifier` is the delegated member URN
103
+ * - `Consent.actor-role` carries the role separately
104
+ * - `Consent.source-reference` points to the blockchain-safe contract VC
105
+ * reference that was or will be anchored on-chain
106
+ */
107
+ export const EXAMPLE_INTER_TENANT_EMPLOYEE_CONTRACT_AUTHORIZATION_CONSENT = Object.freeze({
108
+ '@context': 'org.hl7.fhir.api',
109
+ [ClaimConsent.identifier]: 'urn:uuid:employee-contract-authorization-001',
110
+ [ClaimConsent.subject]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_ORGANIZATION_URN,
111
+ [ClaimConsent.actorIdentifier]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_MEMBER_URN,
112
+ [ClaimConsent.actorRole]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT.actorRole,
113
+ [ClaimConsent.decision]: 'permit',
114
+ [ClaimConsent.purpose]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT.purpose,
115
+ [ClaimConsent.action]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT.requestedScope,
116
+ [ClaimConsent.sourceReference]: getInterTenantAccessContractBlockchainReference(EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CREDENTIAL),
117
+ [ClaimConsent.date]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM,
118
+ [ClaimConsent.periodStart]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM,
119
+ [ClaimConsent.periodEnd]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_UNTIL,
120
+ });
121
+ export const EXAMPLE_INTER_TENANT_PROVIDER_CONTRACT_AUTHORIZATION_CONSENT = Object.freeze({
122
+ '@context': 'org.hl7.fhir.api',
123
+ [ClaimConsent.identifier]: 'urn:uuid:provider-contract-authorization-001',
124
+ [ClaimConsent.subject]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_PROVIDER_ORGANIZATION_URN,
125
+ [ClaimConsent.actorIdentifier]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONSUMER_ORGANIZATION_URN,
126
+ [ClaimConsent.actorRole]: 'organization',
127
+ [ClaimConsent.decision]: 'permit',
128
+ [ClaimConsent.purpose]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT.purpose,
129
+ [ClaimConsent.action]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CONTEXT.requestedScope,
130
+ [ClaimConsent.sourceReference]: getInterTenantAccessContractBlockchainReference(EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_CREDENTIAL),
131
+ [ClaimConsent.date]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM,
132
+ [ClaimConsent.periodStart]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_FROM,
133
+ [ClaimConsent.periodEnd]: EXAMPLE_INTER_TENANT_ACCESS_CONTRACT_VALID_UNTIL,
134
+ });
@@ -11,12 +11,15 @@ import { IdKind } from '../constants/identity-identifiers';
11
11
  * - all values below are synthetic documentation/test fixtures, never real data
12
12
  */
13
13
  export declare const EXAMPLE_TENANT_IDENTIFIER: "acme-id";
14
+ export declare const EXAMPLE_RESEARCH_TENANT_IDENTIFIER: "lab-id";
14
15
  export declare const EXAMPLE_JURISDICTION: "ES";
15
16
  export declare const EXAMPLE_HOST_COVERAGE_SCOPE: "EU";
16
17
  export declare const EXAMPLE_NETWORK_TYPE: "test";
17
18
  export declare const EXAMPLE_ROUTE_VERSION: "v1";
18
19
  export declare const EXAMPLE_SECTOR: "health-care";
20
+ export declare const EXAMPLE_RESEARCH_SECTOR: "health-research";
19
21
  export declare const EXAMPLE_EMAIL_CONTROLLER_ORG: "controller@acme.org";
22
+ export declare const EXAMPLE_EMAIL_CONTROLLER_RESEARCH_ORG: "controller@lab.org";
20
23
  export declare const EXAMPLE_ORGANIZATION_LEGAL_NAME: "ACME HEALTH SL";
21
24
  export declare const EXAMPLE_LEGAL_ORGANIZATION_TAX_ID: "VATES-B00112233";
22
25
  export declare const EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL: "ana.parent@example.org";
@@ -101,9 +104,11 @@ export declare const EXAMPLE_HOST_ROUTE_CONTEXT: {
101
104
  readonly sector: "test";
102
105
  };
103
106
  export declare const EXAMPLE_CONTROLLER_DID: "did:web:people.acme.org:controllers:primary";
107
+ export declare const EXAMPLE_RESEARCH_CONTROLLER_DID: "did:web:people.lab.org:controllers:primary";
104
108
  export declare const EXAMPLE_CONTROLLER_EMAIL: "controller@acme.org";
105
109
  export declare const EXAMPLE_CONTROLLER_SAME_AS: "mailto:controller@acme.org";
106
110
  export declare const EXAMPLE_API_ORGANIZATION_DID: "did:web:api.acme.org";
111
+ export declare const EXAMPLE_RESEARCH_API_ORGANIZATION_DID: "did:web:api.lab.org";
107
112
  export declare const EXAMPLE_SERVICE_PUBLIC_DID: "did:web:public.acme.org";
108
113
  export declare const EXAMPLE_PROFESSIONAL_DID: string;
109
114
  export declare const EXAMPLE_PROVIDER_ORGANIZATION_DID: "did:web:hospital.acme.org";
@@ -26,12 +26,15 @@ import { medicationStatementFlatToFhirR4 } from '../utils/clinical-resource-conv
26
26
  * - all values below are synthetic documentation/test fixtures, never real data
27
27
  */
28
28
  export const EXAMPLE_TENANT_IDENTIFIER = 'acme-id';
29
+ export const EXAMPLE_RESEARCH_TENANT_IDENTIFIER = 'lab-id';
29
30
  export const EXAMPLE_JURISDICTION = 'ES';
30
31
  export const EXAMPLE_HOST_COVERAGE_SCOPE = 'EU';
31
32
  export const EXAMPLE_NETWORK_TYPE = HostNetworkTypes.Test;
32
33
  export const EXAMPLE_ROUTE_VERSION = 'v1';
33
34
  export const EXAMPLE_SECTOR = DataspaceSectors.HealthCare;
35
+ export const EXAMPLE_RESEARCH_SECTOR = DataspaceSectors.HealthResearch;
34
36
  export const EXAMPLE_EMAIL_CONTROLLER_ORG = 'controller@acme.org';
37
+ export const EXAMPLE_EMAIL_CONTROLLER_RESEARCH_ORG = 'controller@lab.org';
35
38
  export const EXAMPLE_ORGANIZATION_LEGAL_NAME = 'ACME HEALTH SL';
36
39
  export const EXAMPLE_LEGAL_ORGANIZATION_TAX_ID = 'VATES-B00112233';
37
40
  export const EXAMPLE_EMAIL_CONTROLLER_INDIVIDUAL = 'ana.parent@example.org';
@@ -116,9 +119,11 @@ export const EXAMPLE_HOST_ROUTE_CONTEXT = {
116
119
  sector: EXAMPLE_NETWORK_TYPE,
117
120
  };
118
121
  export const EXAMPLE_CONTROLLER_DID = 'did:web:people.acme.org:controllers:primary';
122
+ export const EXAMPLE_RESEARCH_CONTROLLER_DID = 'did:web:people.lab.org:controllers:primary';
119
123
  export const EXAMPLE_CONTROLLER_EMAIL = EXAMPLE_EMAIL_CONTROLLER_ORG;
120
124
  export const EXAMPLE_CONTROLLER_SAME_AS = `mailto:${EXAMPLE_CONTROLLER_EMAIL}`;
121
125
  export const EXAMPLE_API_ORGANIZATION_DID = 'did:web:api.acme.org';
126
+ export const EXAMPLE_RESEARCH_API_ORGANIZATION_DID = 'did:web:api.lab.org';
122
127
  export const EXAMPLE_SERVICE_PUBLIC_DID = 'did:web:public.acme.org';
123
128
  export const EXAMPLE_PROFESSIONAL_DID = buildProfessionalDidWeb({
124
129
  organizationDidWeb: 'did:web:api.acme.org',
@@ -22,6 +22,7 @@ export * from './interoperable-claims';
22
22
  export * from './indexing';
23
23
  export * from './identity-bootstrap';
24
24
  export * from './individual-onboarding';
25
+ export * from './inter-tenant-access-contract';
25
26
  export * from './issue';
26
27
  export * from './jsonapi';
27
28
  export * from './jwe';
@@ -22,6 +22,7 @@ export * from './interoperable-claims.js';
22
22
  export * from './indexing.js';
23
23
  export * from './identity-bootstrap.js';
24
24
  export * from './individual-onboarding.js';
25
+ export * from './inter-tenant-access-contract.js';
25
26
  export * from './issue.js';
26
27
  export * from './jsonapi.js';
27
28
  export * from './jwe.js';
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Canonical flat claim keys used when a frontend captures an inter-tenant
3
+ * access contract form and sends it to a backend before the final FHIR
4
+ * `Contract` resource is assembled.
5
+ *
6
+ * Notes:
7
+ * - these keys are claims-first transport helpers, not the legal source of
8
+ * truth
9
+ * - the canonical legal payload is the FHIR `Contract` placed in
10
+ * `credentialSubject` of the emitted VC
11
+ * - custom flat keys are used only where base FHIR does not define a simple
12
+ * one-claim scalar path suitable for UI transport
13
+ */
14
+ export declare enum ClaimInterTenantAccessContract {
15
+ /** Stable business identifier of the agreement/contract. */
16
+ identifier = "Contract.identifier",
17
+ /** FHIR Contract lifecycle status, for example `executed` or `amended`. */
18
+ status = "Contract.status",
19
+ /** When the agreement was formally issued. */
20
+ issued = "Contract.issued",
21
+ /** Agreement validity start date/time. */
22
+ appliesStart = "Contract.applies-start",
23
+ /** Agreement validity end date/time. */
24
+ appliesEnd = "Contract.applies-end",
25
+ /**
26
+ * Provider organization DID.
27
+ *
28
+ * Meaning for programmers:
29
+ * - this is the tenant that owns/exposes the protected data
30
+ * - in the current example this is `did:web:api.acme.org`
31
+ */
32
+ providerOrganization = "Contract.provider-organization",
33
+ /**
34
+ * Consumer organization DID.
35
+ *
36
+ * Meaning for programmers:
37
+ * - this is the foreign tenant requesting access under the agreement
38
+ * - in the current example this is `did:web:api.lab.org`
39
+ */
40
+ consumerOrganization = "Contract.consumer-organization",
41
+ /**
42
+ * DID of the controller who signed on behalf of the provider organization.
43
+ */
44
+ providerController = "Contract.provider-controller",
45
+ /**
46
+ * DID of the controller who signed on behalf of the consumer organization.
47
+ */
48
+ consumerController = "Contract.consumer-controller",
49
+ /**
50
+ * Allowed operational capability, expressed with the same vocabulary used by
51
+ * GW service capabilities and SMART-like root scopes.
52
+ *
53
+ * Example:
54
+ * - `organization/Composition.rs`
55
+ */
56
+ capability = "Contract.security-label",
57
+ /**
58
+ * Allowed business/legal purpose for the access.
59
+ *
60
+ * Example:
61
+ * - `RESEARCH`
62
+ */
63
+ purpose = "Contract.term-type",
64
+ /**
65
+ * External instantiated artifact for the agreement, typically the signed PDF
66
+ * or CID/URL of the contract itself plus direct contractual annexes.
67
+ *
68
+ * Canonical FHIR field:
69
+ * - `Contract.instantiatesUri`
70
+ *
71
+ * Flat claim rule in this stack:
72
+ * - use FHIR-style `instantiates-uri` in claims, then map it to
73
+ * `instantiatesUri` in the actual FHIR resource object
74
+ */
75
+ instantiatesUri = "Contract.instantiates-uri"
76
+ }
77
+ export type InterTenantAccessContractClaims = Partial<Record<ClaimInterTenantAccessContract, string>>;
78
+ export type InterTenantAccessContractSummary = Readonly<{
79
+ identifier?: string;
80
+ status?: string;
81
+ issued?: string;
82
+ appliesStart?: string;
83
+ appliesEnd?: string;
84
+ providerOrganizationDid?: string;
85
+ consumerOrganizationDid?: string;
86
+ providerControllerDid?: string;
87
+ consumerControllerDid?: string;
88
+ capabilities: readonly string[];
89
+ purposes: readonly string[];
90
+ }>;
91
+ export type InterTenantAccessContractMatchCriteria = Readonly<{
92
+ providerOrganizationDid: string;
93
+ consumerOrganizationDid: string;
94
+ requiredCapabilities?: readonly string[];
95
+ purpose?: string;
96
+ now?: string | Date;
97
+ }>;
98
+ export type InterTenantContractAuthorizationConsentCriteria = Readonly<{
99
+ consumerOrganizationDid: string;
100
+ actorIdentifier: string;
101
+ actorRole?: string;
102
+ requiredCapabilities?: readonly string[];
103
+ purpose?: string;
104
+ now?: string | Date;
105
+ }>;
@@ -0,0 +1,78 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ /**
3
+ * Canonical flat claim keys used when a frontend captures an inter-tenant
4
+ * access contract form and sends it to a backend before the final FHIR
5
+ * `Contract` resource is assembled.
6
+ *
7
+ * Notes:
8
+ * - these keys are claims-first transport helpers, not the legal source of
9
+ * truth
10
+ * - the canonical legal payload is the FHIR `Contract` placed in
11
+ * `credentialSubject` of the emitted VC
12
+ * - custom flat keys are used only where base FHIR does not define a simple
13
+ * one-claim scalar path suitable for UI transport
14
+ */
15
+ export var ClaimInterTenantAccessContract;
16
+ (function (ClaimInterTenantAccessContract) {
17
+ /** Stable business identifier of the agreement/contract. */
18
+ ClaimInterTenantAccessContract["identifier"] = "Contract.identifier";
19
+ /** FHIR Contract lifecycle status, for example `executed` or `amended`. */
20
+ ClaimInterTenantAccessContract["status"] = "Contract.status";
21
+ /** When the agreement was formally issued. */
22
+ ClaimInterTenantAccessContract["issued"] = "Contract.issued";
23
+ /** Agreement validity start date/time. */
24
+ ClaimInterTenantAccessContract["appliesStart"] = "Contract.applies-start";
25
+ /** Agreement validity end date/time. */
26
+ ClaimInterTenantAccessContract["appliesEnd"] = "Contract.applies-end";
27
+ /**
28
+ * Provider organization DID.
29
+ *
30
+ * Meaning for programmers:
31
+ * - this is the tenant that owns/exposes the protected data
32
+ * - in the current example this is `did:web:api.acme.org`
33
+ */
34
+ ClaimInterTenantAccessContract["providerOrganization"] = "Contract.provider-organization";
35
+ /**
36
+ * Consumer organization DID.
37
+ *
38
+ * Meaning for programmers:
39
+ * - this is the foreign tenant requesting access under the agreement
40
+ * - in the current example this is `did:web:api.lab.org`
41
+ */
42
+ ClaimInterTenantAccessContract["consumerOrganization"] = "Contract.consumer-organization";
43
+ /**
44
+ * DID of the controller who signed on behalf of the provider organization.
45
+ */
46
+ ClaimInterTenantAccessContract["providerController"] = "Contract.provider-controller";
47
+ /**
48
+ * DID of the controller who signed on behalf of the consumer organization.
49
+ */
50
+ ClaimInterTenantAccessContract["consumerController"] = "Contract.consumer-controller";
51
+ /**
52
+ * Allowed operational capability, expressed with the same vocabulary used by
53
+ * GW service capabilities and SMART-like root scopes.
54
+ *
55
+ * Example:
56
+ * - `organization/Composition.rs`
57
+ */
58
+ ClaimInterTenantAccessContract["capability"] = "Contract.security-label";
59
+ /**
60
+ * Allowed business/legal purpose for the access.
61
+ *
62
+ * Example:
63
+ * - `RESEARCH`
64
+ */
65
+ ClaimInterTenantAccessContract["purpose"] = "Contract.term-type";
66
+ /**
67
+ * External instantiated artifact for the agreement, typically the signed PDF
68
+ * or CID/URL of the contract itself plus direct contractual annexes.
69
+ *
70
+ * Canonical FHIR field:
71
+ * - `Contract.instantiatesUri`
72
+ *
73
+ * Flat claim rule in this stack:
74
+ * - use FHIR-style `instantiates-uri` in claims, then map it to
75
+ * `instantiatesUri` in the actual FHIR resource object
76
+ */
77
+ ClaimInterTenantAccessContract["instantiatesUri"] = "Contract.instantiates-uri";
78
+ })(ClaimInterTenantAccessContract || (ClaimInterTenantAccessContract = {}));
@@ -4,6 +4,8 @@ import type { EvidenceObjectDLT } from '../models/oidc4ida.evidence.model.js';
4
4
  *
5
5
  * Current shared rule:
6
6
  * - `z...` multibase base58 references are already public and stable
7
+ * - `sha3-384:...` references are already blockchain-safe and must pass
8
+ * through unchanged
7
9
  * - any other identifier-like value is SHA3-384 hashed before persistence
8
10
  */
9
11
  export declare function sanitizeBlockchainReference(value: unknown): string | undefined;
@@ -5,6 +5,8 @@ const SHA3_384_PREFIX = 'sha3-384:';
5
5
  *
6
6
  * Current shared rule:
7
7
  * - `z...` multibase base58 references are already public and stable
8
+ * - `sha3-384:...` references are already blockchain-safe and must pass
9
+ * through unchanged
8
10
  * - any other identifier-like value is SHA3-384 hashed before persistence
9
11
  */
10
12
  export function sanitizeBlockchainReference(value) {
@@ -13,6 +15,8 @@ export function sanitizeBlockchainReference(value) {
13
15
  return undefined;
14
16
  if (/^z[1-9A-HJ-NP-Za-km-z]+$/.test(normalized))
15
17
  return normalized;
18
+ if (/^sha3-384:[a-f0-9]{96}$/i.test(normalized))
19
+ return normalized;
16
20
  return `${SHA3_384_PREFIX}${createHash('sha3-384').update(normalized, 'utf8').digest('hex')}`;
17
21
  }
18
22
  /**
@@ -54,9 +54,11 @@ export * from './fhir-validator';
54
54
  export * from './family-registration-test-data';
55
55
  export * from './individual-form-pdf';
56
56
  export * from './individual-organization-claims';
57
+ export * from './inter-tenant-access-contract';
57
58
  export * from './legal-organization-onboarding-editor';
58
59
  export * from './organization-lifecycle';
59
60
  export * from './organization-did-binding';
61
+ export * from './organization-authorization-urn';
60
62
  export * from './individual-organization-lifecycle';
61
63
  export * from './individual-onboarding-editor';
62
64
  export * from './individual-onboarding-document-reference';
@@ -54,9 +54,11 @@ export * from './fhir-validator.js';
54
54
  export * from './family-registration-test-data.js';
55
55
  export * from './individual-form-pdf.js';
56
56
  export * from './individual-organization-claims.js';
57
+ export * from './inter-tenant-access-contract.js';
57
58
  export * from './legal-organization-onboarding-editor.js';
58
59
  export * from './organization-lifecycle.js';
59
60
  export * from './organization-did-binding.js';
61
+ export * from './organization-authorization-urn.js';
60
62
  export * from './individual-organization-lifecycle.js';
61
63
  export * from './individual-onboarding-editor.js';
62
64
  export * from './individual-onboarding-document-reference.js';
@@ -0,0 +1,44 @@
1
+ import type { InterTenantAccessContractClaims, InterTenantContractAuthorizationConsentCriteria, InterTenantAccessContractMatchCriteria, InterTenantAccessContractSummary } from '../models/inter-tenant-access-contract';
2
+ import type { VerifiableCredentialV2 } from '../models/verifiable-credential';
3
+ type ContractLike = Record<string, any>;
4
+ export declare function buildInterTenantAccessContractResource(claims: InterTenantAccessContractClaims): ContractLike;
5
+ export declare function buildInterTenantAccessContractCredential(input: Readonly<{
6
+ claims: InterTenantAccessContractClaims;
7
+ issuer: string;
8
+ validFrom: string;
9
+ validUntil?: string;
10
+ additionalCredential?: Record<string, unknown>;
11
+ }>): VerifiableCredentialV2;
12
+ export declare function summarizeInterTenantAccessContract(credential: unknown): InterTenantAccessContractSummary | undefined;
13
+ export declare function isInterTenantAccessContractActive(summary: InterTenantAccessContractSummary | undefined, options?: Readonly<{
14
+ now?: string | Date;
15
+ }>): boolean;
16
+ export declare function matchesInterTenantAccessContract(summary: InterTenantAccessContractSummary | undefined, criteria: InterTenantAccessContractMatchCriteria): boolean;
17
+ export declare function getMatchingInterTenantAccessContractFromVpToken(vpToken: string, criteria: InterTenantAccessContractMatchCriteria): Record<string, unknown> | undefined;
18
+ /**
19
+ * Returns the public blockchain-safe reference that a reused consent-style
20
+ * authorization rule should store under `Consent.source-reference` when it
21
+ * delegates use of one inter-tenant contract VC to an employee/researcher.
22
+ *
23
+ * Source-of-truth order:
24
+ * - VC `id`
25
+ * - FHIR `Contract.identifier`
26
+ *
27
+ * In both cases the reference is normalized through the same blockchain-safe
28
+ * sanitization rule already used by consent-access assets.
29
+ */
30
+ export declare function getInterTenantAccessContractBlockchainReference(credential: unknown): string | undefined;
31
+ /**
32
+ * Evaluates one consent-style rule reused as an organization-to-employee
33
+ * delegation for inter-tenant contract usage.
34
+ *
35
+ * Shared semantic contract:
36
+ * - `Consent.subject` = consumer organization DID
37
+ * - `Consent.actor-identifier` = delegated employee/researcher identifier
38
+ * - `Consent.action` = allowed capabilities/scopes
39
+ * - `Consent.purpose` = allowed purpose
40
+ * - `Consent.source-reference` = blockchain-safe hash/reference of the
41
+ * underlying contract VC
42
+ */
43
+ export declare function matchesInterTenantContractAuthorizationConsentRule(rule: Record<string, unknown>, contractCredential: unknown, criteria: InterTenantContractAuthorizationConsentCriteria): boolean;
44
+ export {};
@@ -0,0 +1,358 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { ContractCredentialTypes, W3cCredentialContexts, W3cCredentialTypes } from '../constants/verifiable-credentials.js';
3
+ import { ClaimConsent } from '../models/consent-rule.js';
4
+ import { ClaimInterTenantAccessContract } from '../models/inter-tenant-access-contract.js';
5
+ import { sanitizeBlockchainReference } from './evidence-blockchain-references.js';
6
+ import { getVpCredentials } from './vp-token.js';
7
+ const ACTIVE_CONTRACT_STATUSES = new Set(['executed', 'amended', 'appended']);
8
+ const PROVIDER_ROLE = 'provider';
9
+ const CONSUMER_ROLE = 'consumer';
10
+ const PROVIDER_CONTROLLER_ROLE = 'provider-controller';
11
+ const CONSUMER_CONTROLLER_ROLE = 'consumer-controller';
12
+ function splitCsv(value) {
13
+ return Array.from(new Set(String(value || '')
14
+ .split(',')
15
+ .map((item) => item.trim())
16
+ .filter(Boolean)));
17
+ }
18
+ function readClaimWithAliases(claims, canonicalKey, aliases = []) {
19
+ const direct = String(claims[canonicalKey] || '').trim();
20
+ if (direct)
21
+ return direct;
22
+ for (const alias of aliases) {
23
+ const value = String(claims[alias] || '').trim();
24
+ if (value)
25
+ return value;
26
+ }
27
+ return undefined;
28
+ }
29
+ function firstIdentifierValue(input) {
30
+ if (Array.isArray(input)) {
31
+ for (const candidate of input) {
32
+ const found = firstIdentifierValue(candidate);
33
+ if (found)
34
+ return found;
35
+ }
36
+ return undefined;
37
+ }
38
+ if (!input || typeof input !== 'object')
39
+ return undefined;
40
+ const direct = String(input.value || input.id || '').trim();
41
+ return direct || undefined;
42
+ }
43
+ function extractRoleTexts(input) {
44
+ const items = Array.isArray(input) ? input : input ? [input] : [];
45
+ return items
46
+ .flatMap((item) => {
47
+ if (!item || typeof item !== 'object')
48
+ return [];
49
+ const text = String(item.text || '').trim();
50
+ const codingItems = Array.isArray(item.coding) ? item.coding : item.coding ? [item.coding] : [];
51
+ const codingValues = codingItems
52
+ .map((coding) => String(coding?.code || coding?.display || '').trim())
53
+ .filter(Boolean);
54
+ return [text, ...codingValues].filter(Boolean);
55
+ });
56
+ }
57
+ function extractCapabilityValues(input) {
58
+ const items = Array.isArray(input) ? input : input ? [input] : [];
59
+ return Array.from(new Set(items.flatMap((item) => {
60
+ if (!item || typeof item !== 'object')
61
+ return [];
62
+ const text = String(item.text || '').trim();
63
+ const codingItems = Array.isArray(item.coding) ? item.coding : item.coding ? [item.coding] : [];
64
+ const codingValues = codingItems
65
+ .map((coding) => String(coding?.code || coding?.display || '').trim())
66
+ .filter(Boolean);
67
+ return [text, ...codingValues].flatMap((value) => splitCsv(value));
68
+ })));
69
+ }
70
+ function extractOfferPartyReference(contract, expectedRole) {
71
+ const terms = Array.isArray(contract.term) ? contract.term : contract.term ? [contract.term] : [];
72
+ for (const term of terms) {
73
+ const parties = Array.isArray(term?.offer?.party) ? term.offer.party : term?.offer?.party ? [term.offer.party] : [];
74
+ for (const party of parties) {
75
+ const roleTexts = extractRoleTexts(party?.role).map((value) => value.toLowerCase());
76
+ if (!roleTexts.includes(expectedRole))
77
+ continue;
78
+ const reference = String(party?.reference?.reference || party?.reference || '').trim();
79
+ if (reference)
80
+ return reference;
81
+ }
82
+ }
83
+ return undefined;
84
+ }
85
+ function extractSignerReference(contract, expectedRole) {
86
+ const signers = Array.isArray(contract.signer) ? contract.signer : contract.signer ? [contract.signer] : [];
87
+ for (const signer of signers) {
88
+ const roleTexts = extractRoleTexts(signer?.type).map((value) => value.toLowerCase());
89
+ if (!roleTexts.includes(expectedRole))
90
+ continue;
91
+ const reference = String(signer?.party?.reference || signer?.party?.identifier?.value || '').trim();
92
+ if (reference)
93
+ return reference;
94
+ }
95
+ return undefined;
96
+ }
97
+ function extractPurposes(contract) {
98
+ const terms = Array.isArray(contract.term) ? contract.term : contract.term ? [contract.term] : [];
99
+ return Array.from(new Set(terms.flatMap((term) => {
100
+ const typeItems = Array.isArray(term?.type) ? term.type : term?.type ? [term.type] : [];
101
+ return typeItems.flatMap((item) => {
102
+ if (!item || typeof item !== 'object')
103
+ return [];
104
+ const text = String(item.text || '').trim();
105
+ const codingItems = Array.isArray(item.coding) ? item.coding : item.coding ? [item.coding] : [];
106
+ const codingValues = codingItems
107
+ .map((coding) => String(coding?.code || coding?.display || '').trim())
108
+ .filter(Boolean);
109
+ return [text, ...codingValues].flatMap((value) => splitCsv(value));
110
+ });
111
+ })));
112
+ }
113
+ function extractCapabilities(contract) {
114
+ const terms = Array.isArray(contract.term) ? contract.term : contract.term ? [contract.term] : [];
115
+ return Array.from(new Set(terms.flatMap((term) => extractCapabilityValues(term?.offer?.securityLabel))));
116
+ }
117
+ function normalizeNow(input) {
118
+ if (input instanceof Date)
119
+ return input.getTime();
120
+ if (input)
121
+ return new Date(input).getTime();
122
+ return Date.now();
123
+ }
124
+ export function buildInterTenantAccessContractResource(claims) {
125
+ const identifier = readClaimWithAliases(claims, ClaimInterTenantAccessContract.identifier) || '';
126
+ const status = readClaimWithAliases(claims, ClaimInterTenantAccessContract.status) || 'executed';
127
+ const issued = readClaimWithAliases(claims, ClaimInterTenantAccessContract.issued) || '';
128
+ const appliesStart = readClaimWithAliases(claims, ClaimInterTenantAccessContract.appliesStart, ['Contract.applies.start']) || '';
129
+ const appliesEnd = readClaimWithAliases(claims, ClaimInterTenantAccessContract.appliesEnd, ['Contract.applies.end']) || '';
130
+ const providerOrganization = readClaimWithAliases(claims, ClaimInterTenantAccessContract.providerOrganization, ['Contract.term.offer.party.provider']) || '';
131
+ const consumerOrganization = readClaimWithAliases(claims, ClaimInterTenantAccessContract.consumerOrganization, ['Contract.term.offer.party.consumer']) || '';
132
+ const providerController = readClaimWithAliases(claims, ClaimInterTenantAccessContract.providerController, ['Contract.signer.provider']) || '';
133
+ const consumerController = readClaimWithAliases(claims, ClaimInterTenantAccessContract.consumerController, ['Contract.signer.consumer']) || '';
134
+ const instantiatesUri = readClaimWithAliases(claims, ClaimInterTenantAccessContract.instantiatesUri, ['Contract.instantiatesUri']) || '';
135
+ const capabilities = splitCsv(readClaimWithAliases(claims, ClaimInterTenantAccessContract.capability, ['Contract.term.offer.securityLabel']));
136
+ const purposes = splitCsv(readClaimWithAliases(claims, ClaimInterTenantAccessContract.purpose, ['Contract.term.type']));
137
+ return {
138
+ resourceType: 'Contract',
139
+ id: identifier || undefined,
140
+ identifier: identifier ? [{ value: identifier }] : undefined,
141
+ status,
142
+ issued: issued || undefined,
143
+ instantiatesUri: instantiatesUri || undefined,
144
+ applies: {
145
+ ...(appliesStart ? { start: appliesStart } : {}),
146
+ ...(appliesEnd ? { end: appliesEnd } : {}),
147
+ },
148
+ signer: [
149
+ providerController
150
+ ? {
151
+ type: [{ text: PROVIDER_CONTROLLER_ROLE }],
152
+ party: { reference: providerController },
153
+ }
154
+ : undefined,
155
+ consumerController
156
+ ? {
157
+ type: [{ text: CONSUMER_CONTROLLER_ROLE }],
158
+ party: { reference: consumerController },
159
+ }
160
+ : undefined,
161
+ ].filter(Boolean),
162
+ term: [{
163
+ type: purposes.map((purpose) => ({ text: purpose })),
164
+ offer: {
165
+ party: [
166
+ providerOrganization
167
+ ? {
168
+ reference: { reference: providerOrganization },
169
+ role: [{ text: PROVIDER_ROLE }],
170
+ }
171
+ : undefined,
172
+ consumerOrganization
173
+ ? {
174
+ reference: { reference: consumerOrganization },
175
+ role: [{ text: CONSUMER_ROLE }],
176
+ }
177
+ : undefined,
178
+ ].filter(Boolean),
179
+ securityLabel: capabilities.map((capability) => ({ text: capability })),
180
+ },
181
+ }],
182
+ };
183
+ }
184
+ export function buildInterTenantAccessContractCredential(input) {
185
+ return {
186
+ '@context': [W3cCredentialContexts.V2],
187
+ type: [
188
+ W3cCredentialTypes.VerifiableCredential,
189
+ ContractCredentialTypes.InterTenantAccessContractCredential,
190
+ ],
191
+ issuer: String(input.issuer || '').trim(),
192
+ validFrom: String(input.validFrom || '').trim(),
193
+ ...(input.validUntil ? { validUntil: String(input.validUntil).trim() } : {}),
194
+ credentialSubject: buildInterTenantAccessContractResource(input.claims),
195
+ ...(input.additionalCredential || {}),
196
+ };
197
+ }
198
+ export function summarizeInterTenantAccessContract(credential) {
199
+ if (!credential || typeof credential !== 'object')
200
+ return undefined;
201
+ const typeRaw = Array.isArray(credential.type) ? credential.type : [credential.type].filter(Boolean);
202
+ if (!typeRaw.includes(ContractCredentialTypes.InterTenantAccessContractCredential))
203
+ return undefined;
204
+ const subject = (credential.credentialSubject || {});
205
+ if (String(subject.resourceType || '').trim() !== 'Contract')
206
+ return undefined;
207
+ return {
208
+ identifier: firstIdentifierValue(subject.identifier) || String(subject.id || '').trim() || undefined,
209
+ status: String(subject.status || '').trim() || undefined,
210
+ issued: String(subject.issued || '').trim() || undefined,
211
+ appliesStart: String(subject.applies?.start || '').trim() || undefined,
212
+ appliesEnd: String(subject.applies?.end || '').trim() || undefined,
213
+ providerOrganizationDid: extractOfferPartyReference(subject, PROVIDER_ROLE),
214
+ consumerOrganizationDid: extractOfferPartyReference(subject, CONSUMER_ROLE),
215
+ providerControllerDid: extractSignerReference(subject, PROVIDER_CONTROLLER_ROLE),
216
+ consumerControllerDid: extractSignerReference(subject, CONSUMER_CONTROLLER_ROLE),
217
+ capabilities: extractCapabilities(subject),
218
+ purposes: extractPurposes(subject),
219
+ };
220
+ }
221
+ export function isInterTenantAccessContractActive(summary, options = {}) {
222
+ if (!summary)
223
+ return false;
224
+ const status = String(summary.status || '').trim().toLowerCase();
225
+ if (!ACTIVE_CONTRACT_STATUSES.has(status))
226
+ return false;
227
+ const nowMs = normalizeNow(options.now);
228
+ const startMs = summary.appliesStart ? Date.parse(summary.appliesStart) : NaN;
229
+ const endMs = summary.appliesEnd ? Date.parse(summary.appliesEnd) : NaN;
230
+ if (!Number.isNaN(startMs) && startMs > nowMs)
231
+ return false;
232
+ if (!Number.isNaN(endMs) && endMs < nowMs)
233
+ return false;
234
+ return true;
235
+ }
236
+ export function matchesInterTenantAccessContract(summary, criteria) {
237
+ if (!isInterTenantAccessContractActive(summary, { now: criteria.now }))
238
+ return false;
239
+ if (String(summary?.providerOrganizationDid || '').trim() !== String(criteria.providerOrganizationDid || '').trim())
240
+ return false;
241
+ if (String(summary?.consumerOrganizationDid || '').trim() !== String(criteria.consumerOrganizationDid || '').trim())
242
+ return false;
243
+ const requiredCapabilities = Array.from(new Set((criteria.requiredCapabilities || []).map((value) => String(value || '').trim()).filter(Boolean)));
244
+ if (requiredCapabilities.length > 0) {
245
+ const contractCapabilities = new Set((summary?.capabilities || []).map((value) => String(value || '').trim()));
246
+ if (!requiredCapabilities.every((value) => contractCapabilities.has(value)))
247
+ return false;
248
+ }
249
+ const purpose = String(criteria.purpose || '').trim();
250
+ if (purpose) {
251
+ const contractPurposes = new Set((summary?.purposes || []).map((value) => String(value || '').trim()));
252
+ if (contractPurposes.size > 0 && !contractPurposes.has(purpose))
253
+ return false;
254
+ }
255
+ return true;
256
+ }
257
+ export function getMatchingInterTenantAccessContractFromVpToken(vpToken, criteria) {
258
+ const credentials = getVpCredentials(vpToken);
259
+ return credentials.find((credential) => matchesInterTenantAccessContract(summarizeInterTenantAccessContract(credential), criteria));
260
+ }
261
+ function splitClaimCsv(value) {
262
+ return Array.from(new Set(String(value || '')
263
+ .split(',')
264
+ .map((item) => item.trim())
265
+ .filter(Boolean)));
266
+ }
267
+ function isConsentLikeRuleActive(rule, now) {
268
+ const nowMs = normalizeNow(now);
269
+ const startMs = Date.parse(String(rule[ClaimConsent.periodStart] || '').trim());
270
+ const endMs = Date.parse(String(rule[ClaimConsent.periodEnd] || '').trim());
271
+ if (!Number.isNaN(startMs) && startMs > nowMs)
272
+ return false;
273
+ if (!Number.isNaN(endMs) && endMs < nowMs)
274
+ return false;
275
+ return true;
276
+ }
277
+ /**
278
+ * Returns the public blockchain-safe reference that a reused consent-style
279
+ * authorization rule should store under `Consent.source-reference` when it
280
+ * delegates use of one inter-tenant contract VC to an employee/researcher.
281
+ *
282
+ * Source-of-truth order:
283
+ * - VC `id`
284
+ * - FHIR `Contract.identifier`
285
+ *
286
+ * In both cases the reference is normalized through the same blockchain-safe
287
+ * sanitization rule already used by consent-access assets.
288
+ */
289
+ export function getInterTenantAccessContractBlockchainReference(credential) {
290
+ if (!credential || typeof credential !== 'object')
291
+ return undefined;
292
+ const vcId = sanitizeBlockchainReference(credential.id);
293
+ if (vcId)
294
+ return vcId;
295
+ const summary = summarizeInterTenantAccessContract(credential);
296
+ return sanitizeBlockchainReference(summary?.identifier);
297
+ }
298
+ /**
299
+ * Evaluates one consent-style rule reused as an organization-to-employee
300
+ * delegation for inter-tenant contract usage.
301
+ *
302
+ * Shared semantic contract:
303
+ * - `Consent.subject` = consumer organization DID
304
+ * - `Consent.actor-identifier` = delegated employee/researcher identifier
305
+ * - `Consent.action` = allowed capabilities/scopes
306
+ * - `Consent.purpose` = allowed purpose
307
+ * - `Consent.source-reference` = blockchain-safe hash/reference of the
308
+ * underlying contract VC
309
+ */
310
+ export function matchesInterTenantContractAuthorizationConsentRule(rule, contractCredential, criteria) {
311
+ const summary = summarizeInterTenantAccessContract(contractCredential);
312
+ if (!isInterTenantAccessContractActive(summary, { now: criteria.now })) {
313
+ return false;
314
+ }
315
+ if (String(rule[ClaimConsent.decision] || '').trim() !== 'permit')
316
+ return false;
317
+ if (!isConsentLikeRuleActive(rule, criteria.now))
318
+ return false;
319
+ if (String(rule[ClaimConsent.subject] || '').trim() !== String(criteria.consumerOrganizationDid || '').trim())
320
+ return false;
321
+ if (String(rule[ClaimConsent.actorIdentifier] || '').trim() !== String(criteria.actorIdentifier || '').trim())
322
+ return false;
323
+ const expectedReference = getInterTenantAccessContractBlockchainReference(contractCredential);
324
+ const ruleReference = String(rule[ClaimConsent.sourceReference]
325
+ || rule[ClaimConsent.eventBasedOn]
326
+ || '').trim();
327
+ if (!expectedReference || ruleReference !== expectedReference)
328
+ return false;
329
+ const purpose = String(criteria.purpose || '').trim();
330
+ if (purpose) {
331
+ const rulePurpose = String(rule[ClaimConsent.purpose] || '').trim();
332
+ if (rulePurpose && rulePurpose !== purpose)
333
+ return false;
334
+ }
335
+ const actorRole = String(criteria.actorRole || '').trim();
336
+ if (actorRole) {
337
+ const allowedRoles = splitClaimCsv(rule[ClaimConsent.actorRole]);
338
+ if (allowedRoles.length > 0 && !allowedRoles.includes(actorRole))
339
+ return false;
340
+ }
341
+ const requiredCapabilities = Array.from(new Set((criteria.requiredCapabilities || []).map((value) => String(value || '').trim()).filter(Boolean)));
342
+ if (requiredCapabilities.length > 0) {
343
+ const allowedCapabilities = new Set(splitClaimCsv(rule[ClaimConsent.action]));
344
+ if (!requiredCapabilities.every((value) => allowedCapabilities.has(value) || allowedCapabilities.has('*')))
345
+ return false;
346
+ }
347
+ if (requiredCapabilities.length > 0) {
348
+ const contractCapabilities = new Set((summary?.capabilities || []).map((value) => String(value || '').trim()));
349
+ if (!requiredCapabilities.every((value) => contractCapabilities.has(value)))
350
+ return false;
351
+ }
352
+ if (purpose) {
353
+ const contractPurposes = new Set((summary?.purposes || []).map((value) => String(value || '').trim()));
354
+ if (contractPurposes.size > 0 && !contractPurposes.has(purpose))
355
+ return false;
356
+ }
357
+ return true;
358
+ }
@@ -0,0 +1,43 @@
1
+ import type { ClaimsRecord } from '../models/resource-document';
2
+ export type OrganizationAuthorizationUrnInput = Readonly<{
3
+ identifierType: string;
4
+ identifierValue: string;
5
+ }>;
6
+ export type MemberAuthorizationUrnInput = Readonly<{
7
+ organizationUrn?: string;
8
+ identifierType?: string;
9
+ identifierValue?: string;
10
+ memberId: string;
11
+ /** Optional compatibility input. Role must be persisted separately, not inside the canonical member URN. */
12
+ roleCode?: string;
13
+ }>;
14
+ /**
15
+ * Canonical organization authorization identifier used by ledger, consent-like
16
+ * rules, and inter-tenant authorization persistence.
17
+ *
18
+ * Format:
19
+ * - `urn:org:<identifierType-lowercase>:<identifierValue-as-canonical-input>`
20
+ *
21
+ * Examples:
22
+ * - `urn:org:tax:VATES-B12345678`
23
+ * - `urn:org:tax:acme-id`
24
+ */
25
+ export declare function buildOrganizationAuthorizationUrn(input: OrganizationAuthorizationUrnInput): string;
26
+ /**
27
+ * Accepts either the legacy `TYPE|VALUE` input or an already-canonical
28
+ * `urn:org:...` identifier and always returns the canonical URN form.
29
+ */
30
+ export declare function normalizeOrganizationAuthorizationUrn(input: string): string;
31
+ export declare function buildOrganizationAuthorizationUrnFromClaims(claims?: ClaimsRecord): string;
32
+ /**
33
+ * Canonical member authorization identifier rooted in one canonical
34
+ * `urn:org:...`.
35
+ *
36
+ * Format:
37
+ * - `urn:org:<type>:<value>:member:<memberId>`
38
+ *
39
+ * Important rule:
40
+ * - role must be stored as a separate claim, not inside the canonical member
41
+ * identifier
42
+ */
43
+ export declare function buildMemberAuthorizationUrn(input: MemberAuthorizationUrnInput): string;
@@ -0,0 +1,87 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { ClaimsOrganizationSchemaorg } from '../constants/schemaorg.js';
3
+ const ORGANIZATION_URN_PREFIX = 'urn:org:';
4
+ function normalizeTypeSegment(identifierType) {
5
+ return String(identifierType || '').trim().toLowerCase();
6
+ }
7
+ function normalizeValueSegment(identifierValue) {
8
+ return String(identifierValue || '').trim();
9
+ }
10
+ /**
11
+ * Canonical organization authorization identifier used by ledger, consent-like
12
+ * rules, and inter-tenant authorization persistence.
13
+ *
14
+ * Format:
15
+ * - `urn:org:<identifierType-lowercase>:<identifierValue-as-canonical-input>`
16
+ *
17
+ * Examples:
18
+ * - `urn:org:tax:VATES-B12345678`
19
+ * - `urn:org:tax:acme-id`
20
+ */
21
+ export function buildOrganizationAuthorizationUrn(input) {
22
+ const identifierType = normalizeTypeSegment(input.identifierType);
23
+ const identifierValue = normalizeValueSegment(input.identifierValue);
24
+ if (!identifierType || !identifierValue) {
25
+ throw new Error('Organization authorization URN requires identifierType and identifierValue.');
26
+ }
27
+ return `${ORGANIZATION_URN_PREFIX}${identifierType}:${identifierValue}`;
28
+ }
29
+ /**
30
+ * Accepts either the legacy `TYPE|VALUE` input or an already-canonical
31
+ * `urn:org:...` identifier and always returns the canonical URN form.
32
+ */
33
+ export function normalizeOrganizationAuthorizationUrn(input) {
34
+ const normalized = String(input || '').trim();
35
+ if (!normalized) {
36
+ throw new Error('Organization authorization identifier cannot be empty.');
37
+ }
38
+ if (normalized.toLowerCase().startsWith(ORGANIZATION_URN_PREFIX)) {
39
+ const parts = normalized.split(':');
40
+ const identifierType = String(parts[2] || '').trim();
41
+ const value = parts.slice(3).join(':').trim();
42
+ return buildOrganizationAuthorizationUrn({
43
+ identifierType,
44
+ identifierValue: value,
45
+ });
46
+ }
47
+ const legacyMatch = normalized.match(/^([^|:]+)\|(.+)$/);
48
+ if (legacyMatch) {
49
+ return buildOrganizationAuthorizationUrn({
50
+ identifierType: legacyMatch[1],
51
+ identifierValue: legacyMatch[2],
52
+ });
53
+ }
54
+ throw new Error(`Unsupported organization authorization identifier format: ${normalized}`);
55
+ }
56
+ export function buildOrganizationAuthorizationUrnFromClaims(claims) {
57
+ const identifierType = String(claims?.[ClaimsOrganizationSchemaorg.identifierType] || '').trim();
58
+ const identifierValue = String(claims?.[ClaimsOrganizationSchemaorg.identifierValue] || '').trim();
59
+ return buildOrganizationAuthorizationUrn({
60
+ identifierType,
61
+ identifierValue,
62
+ });
63
+ }
64
+ /**
65
+ * Canonical member authorization identifier rooted in one canonical
66
+ * `urn:org:...`.
67
+ *
68
+ * Format:
69
+ * - `urn:org:<type>:<value>:member:<memberId>`
70
+ *
71
+ * Important rule:
72
+ * - role must be stored as a separate claim, not inside the canonical member
73
+ * identifier
74
+ */
75
+ export function buildMemberAuthorizationUrn(input) {
76
+ const organizationUrn = input.organizationUrn
77
+ ? normalizeOrganizationAuthorizationUrn(input.organizationUrn)
78
+ : buildOrganizationAuthorizationUrn({
79
+ identifierType: String(input.identifierType || '').trim(),
80
+ identifierValue: String(input.identifierValue || '').trim(),
81
+ });
82
+ const memberId = String(input.memberId || '').trim();
83
+ if (!memberId) {
84
+ throw new Error('Member authorization URN requires memberId.');
85
+ }
86
+ return `${organizationUrn}:member:${memberId}`;
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.0.17",
3
+ "version": "2.0.18",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },