gdc-common-utils-ts 2.0.16 → 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.
- package/dist/constants/verifiable-credentials.d.ts +7 -0
- package/dist/constants/verifiable-credentials.js +7 -0
- package/dist/examples/contract-examples.d.ts +1 -0
- package/dist/examples/contract-examples.js +1 -0
- package/dist/examples/index.d.ts +1 -0
- package/dist/examples/index.js +1 -0
- package/dist/examples/inter-tenant-access-contract.d.ts +124 -0
- package/dist/examples/inter-tenant-access-contract.js +134 -0
- package/dist/examples/shared.d.ts +5 -0
- package/dist/examples/shared.js +5 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/inter-tenant-access-contract.d.ts +105 -0
- package/dist/models/inter-tenant-access-contract.js +78 -0
- package/dist/utils/bundle-reader.d.ts +19 -0
- package/dist/utils/bundle-reader.js +43 -0
- package/dist/utils/evidence-blockchain-references.d.ts +2 -0
- package/dist/utils/evidence-blockchain-references.js +4 -0
- package/dist/utils/gw-core-commercial-contract.d.ts +41 -0
- package/dist/utils/gw-core-commercial-contract.js +71 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/inter-tenant-access-contract.d.ts +44 -0
- package/dist/utils/inter-tenant-access-contract.js +358 -0
- package/dist/utils/legal-organization-verification-result.d.ts +37 -0
- package/dist/utils/legal-organization-verification-result.js +106 -0
- package/dist/utils/organization-authorization-urn.d.ts +43 -0
- package/dist/utils/organization-authorization-urn.js +87 -0
- package/package.json +1 -1
|
@@ -2,6 +2,38 @@ import { getHighestIssueSeverity, isIssueSeverityCode, IssueSeverity, } from '..
|
|
|
2
2
|
function cloneEntry(value) {
|
|
3
3
|
return JSON.parse(JSON.stringify(value));
|
|
4
4
|
}
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return value && typeof value === 'object' ? value : {};
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Returns the canonical claims view for one bundle entry array index.
|
|
10
|
+
*
|
|
11
|
+
* The helper accepts the full bundle-like object plus the entry position to
|
|
12
|
+
* inspect inside `bundle.data[]` or `bundle.entry[]`.
|
|
13
|
+
*/
|
|
14
|
+
export function getClaimsInBundleEntryAt(bundle, index) {
|
|
15
|
+
const bundleRecord = asRecord(bundle);
|
|
16
|
+
const entries = Array.isArray(bundleRecord.data)
|
|
17
|
+
? bundleRecord.data
|
|
18
|
+
: (Array.isArray(bundleRecord.entry) ? bundleRecord.entry : []);
|
|
19
|
+
if (!Number.isInteger(index) || index < 0 || index >= entries.length) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
const entryRecord = asRecord(entries[index]);
|
|
23
|
+
const resource = asRecord(entryRecord.resource);
|
|
24
|
+
const resourceMeta = asRecord(resource.meta);
|
|
25
|
+
return asRecord(resourceMeta.claims);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns the merged claims view for the first `data[]` or `entry[]` item in a
|
|
29
|
+
* bundle-like body.
|
|
30
|
+
*
|
|
31
|
+
* Use this for the common "one operation, one returned entry" flows when the
|
|
32
|
+
* caller does not want to manually navigate `body.data[0]`.
|
|
33
|
+
*/
|
|
34
|
+
export function getClaimsInFirstDataEntry(bundle) {
|
|
35
|
+
return getClaimsInBundleEntryAt(bundle, 0);
|
|
36
|
+
}
|
|
5
37
|
/**
|
|
6
38
|
* Runtime-neutral reader for built or received FHIR-like bundles.
|
|
7
39
|
*
|
|
@@ -48,6 +80,17 @@ export class BundleReader {
|
|
|
48
80
|
}
|
|
49
81
|
return this.resolveEntryIdentifier(entries[index]);
|
|
50
82
|
}
|
|
83
|
+
/** Returns merged claims for one entry array index. */
|
|
84
|
+
getEntryClaimsByArrayIndex(index) {
|
|
85
|
+
return getClaimsInBundleEntryAt(this.bundle, index);
|
|
86
|
+
}
|
|
87
|
+
/** Returns merged claims for the currently opened entry. */
|
|
88
|
+
getActiveEntryClaims() {
|
|
89
|
+
if (this.activeEntryIndex === null) {
|
|
90
|
+
throw new Error('BundleReader does not have one active entry. Call openEntry(index) first.');
|
|
91
|
+
}
|
|
92
|
+
return this.getEntryClaimsByArrayIndex(this.activeEntryIndex);
|
|
93
|
+
}
|
|
51
94
|
/** Returns the first entry array index whose resolved identifier matches the requested value. */
|
|
52
95
|
getEntryIndexByIdentifier(identifier) {
|
|
53
96
|
const normalizedIdentifier = normalizeOptionalString(identifier);
|
|
@@ -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
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ClaimsOfferSchemaorg, ClaimsOrderSchemaorg } from '../constants/schemaorg';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical GW CORE onboarding/reissue flow identifiers for commercial
|
|
4
|
+
* expectations.
|
|
5
|
+
*
|
|
6
|
+
* These identifiers are intentionally flow-level, not transport-level:
|
|
7
|
+
* - they describe whether one GW response is expected to mint a commercial
|
|
8
|
+
* Offer and whether a follow-up `Order/_batch` step is part of the contract
|
|
9
|
+
* - they do not describe how the caller reached the flow (PDF, ICA proof,
|
|
10
|
+
* signed bundle, SDK facade, etc.)
|
|
11
|
+
*/
|
|
12
|
+
export declare const GwCoreCommercialFlow: Readonly<{
|
|
13
|
+
readonly LegalOrganizationTransaction: "legal-organization-transaction";
|
|
14
|
+
readonly LegalOrganizationActivateLegacy: "legal-organization-activate-legacy";
|
|
15
|
+
readonly LegalOrganizationIssueReissue: "legal-organization-issue-reissue";
|
|
16
|
+
readonly IndividualOrganizationCommercial: "individual-organization-commercial";
|
|
17
|
+
readonly IndividualOrganizationEmbeddedRegistration: "individual-organization-embedded-registration";
|
|
18
|
+
}>;
|
|
19
|
+
export type GwCoreCommercialFlow = typeof GwCoreCommercialFlow[keyof typeof GwCoreCommercialFlow];
|
|
20
|
+
export type GwCoreCommercialContract = Readonly<{
|
|
21
|
+
flow: GwCoreCommercialFlow;
|
|
22
|
+
mintsOffer: boolean;
|
|
23
|
+
requiresOrderConfirmation: boolean;
|
|
24
|
+
offerClaim?: typeof ClaimsOfferSchemaorg.identifier;
|
|
25
|
+
orderClaim?: typeof ClaimsOrderSchemaorg.acceptedOfferIdentifier;
|
|
26
|
+
programmingHint: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Shared commercial contract table for the GW CORE onboarding/reissue flows
|
|
30
|
+
* that currently matter to SDKs, BFFs and route-level tests.
|
|
31
|
+
*
|
|
32
|
+
* Reading guide:
|
|
33
|
+
* - `mintsOffer=true` means the response is expected to expose the canonical
|
|
34
|
+
* commercial claim in `meta.claims['org.schema.Offer.identifier']`
|
|
35
|
+
* - `requiresOrderConfirmation=true` means the caller must later submit
|
|
36
|
+
* `Order.acceptedOffer.identifier` through `Order/_batch`
|
|
37
|
+
* - `mintsOffer=false` means the flow is not expected to create a new
|
|
38
|
+
* commercial Offer, even if it returns activation material or other claims
|
|
39
|
+
*/
|
|
40
|
+
export declare const GW_CORE_COMMERCIAL_CONTRACTS: Readonly<Record<GwCoreCommercialFlow, GwCoreCommercialContract>>;
|
|
41
|
+
export declare function readGwCoreCommercialContract(flow: GwCoreCommercialFlow): GwCoreCommercialContract;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ClaimsOfferSchemaorg, ClaimsOrderSchemaorg } from '../constants/schemaorg.js';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical GW CORE onboarding/reissue flow identifiers for commercial
|
|
4
|
+
* expectations.
|
|
5
|
+
*
|
|
6
|
+
* These identifiers are intentionally flow-level, not transport-level:
|
|
7
|
+
* - they describe whether one GW response is expected to mint a commercial
|
|
8
|
+
* Offer and whether a follow-up `Order/_batch` step is part of the contract
|
|
9
|
+
* - they do not describe how the caller reached the flow (PDF, ICA proof,
|
|
10
|
+
* signed bundle, SDK facade, etc.)
|
|
11
|
+
*/
|
|
12
|
+
export const GwCoreCommercialFlow = Object.freeze({
|
|
13
|
+
LegalOrganizationTransaction: 'legal-organization-transaction',
|
|
14
|
+
LegalOrganizationActivateLegacy: 'legal-organization-activate-legacy',
|
|
15
|
+
LegalOrganizationIssueReissue: 'legal-organization-issue-reissue',
|
|
16
|
+
IndividualOrganizationCommercial: 'individual-organization-commercial',
|
|
17
|
+
IndividualOrganizationEmbeddedRegistration: 'individual-organization-embedded-registration',
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Shared commercial contract table for the GW CORE onboarding/reissue flows
|
|
21
|
+
* that currently matter to SDKs, BFFs and route-level tests.
|
|
22
|
+
*
|
|
23
|
+
* Reading guide:
|
|
24
|
+
* - `mintsOffer=true` means the response is expected to expose the canonical
|
|
25
|
+
* commercial claim in `meta.claims['org.schema.Offer.identifier']`
|
|
26
|
+
* - `requiresOrderConfirmation=true` means the caller must later submit
|
|
27
|
+
* `Order.acceptedOffer.identifier` through `Order/_batch`
|
|
28
|
+
* - `mintsOffer=false` means the flow is not expected to create a new
|
|
29
|
+
* commercial Offer, even if it returns activation material or other claims
|
|
30
|
+
*/
|
|
31
|
+
export const GW_CORE_COMMERCIAL_CONTRACTS = Object.freeze({
|
|
32
|
+
[GwCoreCommercialFlow.LegalOrganizationTransaction]: {
|
|
33
|
+
flow: GwCoreCommercialFlow.LegalOrganizationTransaction,
|
|
34
|
+
mintsOffer: true,
|
|
35
|
+
requiresOrderConfirmation: true,
|
|
36
|
+
offerClaim: ClaimsOfferSchemaorg.identifier,
|
|
37
|
+
orderClaim: ClaimsOrderSchemaorg.acceptedOfferIdentifier,
|
|
38
|
+
programmingHint: 'Use this for first-time legal organization onboarding; read meta.claims[org.schema.Offer.identifier] and then confirm Order/_batch.',
|
|
39
|
+
},
|
|
40
|
+
[GwCoreCommercialFlow.LegalOrganizationActivateLegacy]: {
|
|
41
|
+
flow: GwCoreCommercialFlow.LegalOrganizationActivateLegacy,
|
|
42
|
+
mintsOffer: true,
|
|
43
|
+
requiresOrderConfirmation: true,
|
|
44
|
+
offerClaim: ClaimsOfferSchemaorg.identifier,
|
|
45
|
+
orderClaim: ClaimsOrderSchemaorg.acceptedOfferIdentifier,
|
|
46
|
+
programmingHint: 'Legacy ICA-proof activation still mints one commercial Offer and still requires Order/_batch confirmation.',
|
|
47
|
+
},
|
|
48
|
+
[GwCoreCommercialFlow.LegalOrganizationIssueReissue]: {
|
|
49
|
+
flow: GwCoreCommercialFlow.LegalOrganizationIssueReissue,
|
|
50
|
+
mintsOffer: false,
|
|
51
|
+
requiresOrderConfirmation: false,
|
|
52
|
+
programmingHint: 'Use this only to reissue controller activation material for an already existing tenant; do not expect a new Offer or Order step.',
|
|
53
|
+
},
|
|
54
|
+
[GwCoreCommercialFlow.IndividualOrganizationCommercial]: {
|
|
55
|
+
flow: GwCoreCommercialFlow.IndividualOrganizationCommercial,
|
|
56
|
+
mintsOffer: true,
|
|
57
|
+
requiresOrderConfirmation: true,
|
|
58
|
+
offerClaim: ClaimsOfferSchemaorg.identifier,
|
|
59
|
+
orderClaim: ClaimsOrderSchemaorg.acceptedOfferIdentifier,
|
|
60
|
+
programmingHint: 'Use this for the family/individual commercial bootstrap facade that returns one Offer and then requires Order/_batch.',
|
|
61
|
+
},
|
|
62
|
+
[GwCoreCommercialFlow.IndividualOrganizationEmbeddedRegistration]: {
|
|
63
|
+
flow: GwCoreCommercialFlow.IndividualOrganizationEmbeddedRegistration,
|
|
64
|
+
mintsOffer: false,
|
|
65
|
+
requiresOrderConfirmation: false,
|
|
66
|
+
programmingHint: 'Use this only for embedded legacy subject registration inside an already active tenant; it signals status but does not mint a commercial Offer.',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
export function readGwCoreCommercialContract(flow) {
|
|
70
|
+
return GW_CORE_COMMERCIAL_CONTRACTS[flow];
|
|
71
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export * from './fhir-cid';
|
|
|
31
31
|
export * from './fhir-search';
|
|
32
32
|
export * from './family-organization-summary';
|
|
33
33
|
export * from './gw-core-path';
|
|
34
|
+
export * from './gw-core-commercial-contract';
|
|
35
|
+
export * from './legal-organization-verification-result';
|
|
34
36
|
export * from './communication-fhir-r4';
|
|
35
37
|
export * from './communication-document-reference';
|
|
36
38
|
export * from './communication-bundle-document-request';
|
|
@@ -52,9 +54,11 @@ export * from './fhir-validator';
|
|
|
52
54
|
export * from './family-registration-test-data';
|
|
53
55
|
export * from './individual-form-pdf';
|
|
54
56
|
export * from './individual-organization-claims';
|
|
57
|
+
export * from './inter-tenant-access-contract';
|
|
55
58
|
export * from './legal-organization-onboarding-editor';
|
|
56
59
|
export * from './organization-lifecycle';
|
|
57
60
|
export * from './organization-did-binding';
|
|
61
|
+
export * from './organization-authorization-urn';
|
|
58
62
|
export * from './individual-organization-lifecycle';
|
|
59
63
|
export * from './individual-onboarding-editor';
|
|
60
64
|
export * from './individual-onboarding-document-reference';
|
package/dist/utils/index.js
CHANGED
|
@@ -31,6 +31,8 @@ export * from './fhir-cid.js';
|
|
|
31
31
|
export * from './fhir-search.js';
|
|
32
32
|
export * from './family-organization-summary.js';
|
|
33
33
|
export * from './gw-core-path.js';
|
|
34
|
+
export * from './gw-core-commercial-contract.js';
|
|
35
|
+
export * from './legal-organization-verification-result.js';
|
|
34
36
|
export * from './communication-fhir-r4.js';
|
|
35
37
|
export * from './communication-document-reference.js';
|
|
36
38
|
export * from './communication-bundle-document-request.js';
|
|
@@ -52,9 +54,11 @@ export * from './fhir-validator.js';
|
|
|
52
54
|
export * from './family-registration-test-data.js';
|
|
53
55
|
export * from './individual-form-pdf.js';
|
|
54
56
|
export * from './individual-organization-claims.js';
|
|
57
|
+
export * from './inter-tenant-access-contract.js';
|
|
55
58
|
export * from './legal-organization-onboarding-editor.js';
|
|
56
59
|
export * from './organization-lifecycle.js';
|
|
57
60
|
export * from './organization-did-binding.js';
|
|
61
|
+
export * from './organization-authorization-urn.js';
|
|
58
62
|
export * from './individual-organization-lifecycle.js';
|
|
59
63
|
export * from './individual-onboarding-editor.js';
|
|
60
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
|
+
}
|