gdc-common-utils-ts 1.4.22 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/examples/consent-access.d.ts +138 -0
- package/dist/examples/consent-access.js +104 -0
- package/dist/examples/index.d.ts +1 -0
- package/dist/examples/index.js +1 -0
- package/dist/models/consent-access.d.ts +79 -0
- package/dist/models/consent-access.js +2 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/utils/consent.d.ts +74 -0
- package/dist/utils/consent.js +485 -0
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export declare const EXAMPLE_CONSENT_ACCESS_SUBJECT: "did:web:api.acme.org:individual:123";
|
|
2
|
+
export declare const EXAMPLE_CONSENT_ACCESS_PROVIDER_DID: "did:web:hospital.acme.org";
|
|
3
|
+
export declare const EXAMPLE_CONSENT_ACCESS_PROVIDER_EMAIL: "doctor.oncall@example.org";
|
|
4
|
+
export declare const EXAMPLE_CONSENT_ACCESS_RELATED_PERSON_EMAIL: "parent.guardian@example.org";
|
|
5
|
+
export declare const EXAMPLE_CONSENT_ACCESS_JURISDICTION: "ES";
|
|
6
|
+
export declare const EXAMPLE_CONSENT_ACCESS_RULES: Readonly<{
|
|
7
|
+
physicianByEmailContinuousCare: {
|
|
8
|
+
readonly 'Consent.date': "2026-05-20";
|
|
9
|
+
readonly 'Consent.period-end'?: string | undefined;
|
|
10
|
+
readonly 'Consent.period-start'?: string | undefined;
|
|
11
|
+
readonly 'Consent.resourceType'?: string | undefined;
|
|
12
|
+
readonly '@context': "org.hl7.fhir.api";
|
|
13
|
+
readonly 'Consent.identifier': string;
|
|
14
|
+
readonly 'Consent.subject': "did:web:api.acme.org:individual:123";
|
|
15
|
+
readonly 'Consent.actor-identifier': string;
|
|
16
|
+
readonly 'Consent.actor-role': string;
|
|
17
|
+
readonly 'Consent.decision': "permit" | "deny";
|
|
18
|
+
readonly 'Consent.purpose': string;
|
|
19
|
+
readonly 'Consent.action': string;
|
|
20
|
+
};
|
|
21
|
+
physicianByEmailEmergency: {
|
|
22
|
+
readonly 'Consent.date': "2026-05-20";
|
|
23
|
+
readonly 'Consent.period-end'?: string | undefined;
|
|
24
|
+
readonly 'Consent.period-start'?: string | undefined;
|
|
25
|
+
readonly 'Consent.resourceType'?: string | undefined;
|
|
26
|
+
readonly '@context': "org.hl7.fhir.api";
|
|
27
|
+
readonly 'Consent.identifier': string;
|
|
28
|
+
readonly 'Consent.subject': "did:web:api.acme.org:individual:123";
|
|
29
|
+
readonly 'Consent.actor-identifier': string;
|
|
30
|
+
readonly 'Consent.actor-role': string;
|
|
31
|
+
readonly 'Consent.decision': "permit" | "deny";
|
|
32
|
+
readonly 'Consent.purpose': string;
|
|
33
|
+
readonly 'Consent.action': string;
|
|
34
|
+
};
|
|
35
|
+
physicianByOrganizationContinuousCare: {
|
|
36
|
+
readonly 'Consent.date': "2026-05-20";
|
|
37
|
+
readonly 'Consent.period-end'?: string | undefined;
|
|
38
|
+
readonly 'Consent.period-start'?: string | undefined;
|
|
39
|
+
readonly 'Consent.resourceType'?: string | undefined;
|
|
40
|
+
readonly '@context': "org.hl7.fhir.api";
|
|
41
|
+
readonly 'Consent.identifier': string;
|
|
42
|
+
readonly 'Consent.subject': "did:web:api.acme.org:individual:123";
|
|
43
|
+
readonly 'Consent.actor-identifier': string;
|
|
44
|
+
readonly 'Consent.actor-role': string;
|
|
45
|
+
readonly 'Consent.decision': "permit" | "deny";
|
|
46
|
+
readonly 'Consent.purpose': string;
|
|
47
|
+
readonly 'Consent.action': string;
|
|
48
|
+
};
|
|
49
|
+
physicianByJurisdictionEmergency: {
|
|
50
|
+
readonly 'Consent.date': "2026-05-20";
|
|
51
|
+
readonly 'Consent.period-end'?: string | undefined;
|
|
52
|
+
readonly 'Consent.period-start'?: string | undefined;
|
|
53
|
+
readonly 'Consent.resourceType'?: string | undefined;
|
|
54
|
+
readonly '@context': "org.hl7.fhir.api";
|
|
55
|
+
readonly 'Consent.identifier': string;
|
|
56
|
+
readonly 'Consent.subject': "did:web:api.acme.org:individual:123";
|
|
57
|
+
readonly 'Consent.actor-identifier': string;
|
|
58
|
+
readonly 'Consent.actor-role': string;
|
|
59
|
+
readonly 'Consent.decision': "permit" | "deny";
|
|
60
|
+
readonly 'Consent.purpose': string;
|
|
61
|
+
readonly 'Consent.action': string;
|
|
62
|
+
};
|
|
63
|
+
nurseByOrganization: {
|
|
64
|
+
readonly 'Consent.date': "2026-05-20";
|
|
65
|
+
readonly 'Consent.period-end'?: string | undefined;
|
|
66
|
+
readonly 'Consent.period-start'?: string | undefined;
|
|
67
|
+
readonly 'Consent.resourceType'?: string | undefined;
|
|
68
|
+
readonly '@context': "org.hl7.fhir.api";
|
|
69
|
+
readonly 'Consent.identifier': string;
|
|
70
|
+
readonly 'Consent.subject': "did:web:api.acme.org:individual:123";
|
|
71
|
+
readonly 'Consent.actor-identifier': string;
|
|
72
|
+
readonly 'Consent.actor-role': string;
|
|
73
|
+
readonly 'Consent.decision': "permit" | "deny";
|
|
74
|
+
readonly 'Consent.purpose': string;
|
|
75
|
+
readonly 'Consent.action': string;
|
|
76
|
+
};
|
|
77
|
+
paramedicByJurisdiction: {
|
|
78
|
+
readonly 'Consent.date': "2026-05-20";
|
|
79
|
+
readonly 'Consent.period-end'?: string | undefined;
|
|
80
|
+
readonly 'Consent.period-start'?: string | undefined;
|
|
81
|
+
readonly 'Consent.resourceType'?: string | undefined;
|
|
82
|
+
readonly '@context': "org.hl7.fhir.api";
|
|
83
|
+
readonly 'Consent.identifier': string;
|
|
84
|
+
readonly 'Consent.subject': "did:web:api.acme.org:individual:123";
|
|
85
|
+
readonly 'Consent.actor-identifier': string;
|
|
86
|
+
readonly 'Consent.actor-role': string;
|
|
87
|
+
readonly 'Consent.decision': "permit" | "deny";
|
|
88
|
+
readonly 'Consent.purpose': string;
|
|
89
|
+
readonly 'Consent.action': string;
|
|
90
|
+
};
|
|
91
|
+
directPhysicianDenyInsideAllowedOrganization: {
|
|
92
|
+
readonly 'Consent.date': "2026-05-20";
|
|
93
|
+
readonly 'Consent.period-end'?: string | undefined;
|
|
94
|
+
readonly 'Consent.period-start'?: string | undefined;
|
|
95
|
+
readonly 'Consent.resourceType'?: string | undefined;
|
|
96
|
+
readonly '@context': "org.hl7.fhir.api";
|
|
97
|
+
readonly 'Consent.identifier': string;
|
|
98
|
+
readonly 'Consent.subject': "did:web:api.acme.org:individual:123";
|
|
99
|
+
readonly 'Consent.actor-identifier': string;
|
|
100
|
+
readonly 'Consent.actor-role': string;
|
|
101
|
+
readonly 'Consent.decision': "permit" | "deny";
|
|
102
|
+
readonly 'Consent.purpose': string;
|
|
103
|
+
readonly 'Consent.action': string;
|
|
104
|
+
};
|
|
105
|
+
relatedPersonByEmail: {
|
|
106
|
+
readonly 'Consent.date': "2026-05-20";
|
|
107
|
+
readonly 'Consent.period-end'?: string | undefined;
|
|
108
|
+
readonly 'Consent.period-start'?: string | undefined;
|
|
109
|
+
readonly 'Consent.resourceType'?: string | undefined;
|
|
110
|
+
readonly '@context': "org.hl7.fhir.api";
|
|
111
|
+
readonly 'Consent.identifier': string;
|
|
112
|
+
readonly 'Consent.subject': "did:web:api.acme.org:individual:123";
|
|
113
|
+
readonly 'Consent.actor-identifier': string;
|
|
114
|
+
readonly 'Consent.actor-role': string;
|
|
115
|
+
readonly 'Consent.decision': "permit" | "deny";
|
|
116
|
+
readonly 'Consent.purpose': string;
|
|
117
|
+
readonly 'Consent.action': string;
|
|
118
|
+
};
|
|
119
|
+
revokedPhysicianEmailConsent: {
|
|
120
|
+
readonly 'Consent.date': "2026-05-20";
|
|
121
|
+
readonly 'Consent.period-end'?: string | undefined;
|
|
122
|
+
readonly 'Consent.period-start'?: string | undefined;
|
|
123
|
+
readonly 'Consent.resourceType'?: string | undefined;
|
|
124
|
+
readonly '@context': "org.hl7.fhir.api";
|
|
125
|
+
readonly 'Consent.identifier': string;
|
|
126
|
+
readonly 'Consent.subject': "did:web:api.acme.org:individual:123";
|
|
127
|
+
readonly 'Consent.actor-identifier': string;
|
|
128
|
+
readonly 'Consent.actor-role': string;
|
|
129
|
+
readonly 'Consent.decision': "permit" | "deny";
|
|
130
|
+
readonly 'Consent.purpose': string;
|
|
131
|
+
readonly 'Consent.action': string;
|
|
132
|
+
};
|
|
133
|
+
}>;
|
|
134
|
+
export declare const EXAMPLE_CONSENT_PHONE_EXTENSION_PENDING: Readonly<{
|
|
135
|
+
target: "tel:+34600111222";
|
|
136
|
+
status: "pending-extension";
|
|
137
|
+
reason: "telephone actor targeting remains an extension concern unless the sector/runtime explicitly enables it";
|
|
138
|
+
}>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
2
|
+
import { HealthcareActorRoles, HealthcareBasicSections, HealthcareConsentPurposes, } from '../constants/healthcare.js';
|
|
3
|
+
import { ResourceTypesFhirR4 } from '../constants/fhir-resource-types.js';
|
|
4
|
+
export const EXAMPLE_CONSENT_ACCESS_SUBJECT = 'did:web:api.acme.org:individual:123';
|
|
5
|
+
export const EXAMPLE_CONSENT_ACCESS_PROVIDER_DID = 'did:web:hospital.acme.org';
|
|
6
|
+
export const EXAMPLE_CONSENT_ACCESS_PROVIDER_EMAIL = 'doctor.oncall@example.org';
|
|
7
|
+
export const EXAMPLE_CONSENT_ACCESS_RELATED_PERSON_EMAIL = 'parent.guardian@example.org';
|
|
8
|
+
export const EXAMPLE_CONSENT_ACCESS_JURISDICTION = 'ES';
|
|
9
|
+
function buildRule(input) {
|
|
10
|
+
return {
|
|
11
|
+
'@context': 'org.hl7.fhir.api',
|
|
12
|
+
'Consent.identifier': input.identifier,
|
|
13
|
+
'Consent.subject': EXAMPLE_CONSENT_ACCESS_SUBJECT,
|
|
14
|
+
'Consent.actor-identifier': input.actorIdentifier,
|
|
15
|
+
'Consent.actor-role': input.actorRole,
|
|
16
|
+
'Consent.decision': input.decision || 'permit',
|
|
17
|
+
'Consent.purpose': input.purpose,
|
|
18
|
+
'Consent.action': input.actions.join(','),
|
|
19
|
+
...(input.resourceTypes?.length ? { 'Consent.resourceType': input.resourceTypes.join(',') } : {}),
|
|
20
|
+
...(input.periodStart ? { 'Consent.period-start': input.periodStart } : {}),
|
|
21
|
+
...(input.periodEnd ? { 'Consent.period-end': input.periodEnd } : {}),
|
|
22
|
+
'Consent.date': '2026-05-20',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const EXAMPLE_CONSENT_ACCESS_RULES = Object.freeze({
|
|
26
|
+
physicianByEmailContinuousCare: buildRule({
|
|
27
|
+
identifier: 'urn:uuid:consent-physician-email-treatment',
|
|
28
|
+
actorIdentifier: EXAMPLE_CONSENT_ACCESS_PROVIDER_EMAIL,
|
|
29
|
+
actorRole: HealthcareActorRoles.Physician,
|
|
30
|
+
purpose: HealthcareConsentPurposes.Treatment,
|
|
31
|
+
actions: [HealthcareBasicSections.AllergiesAndIntolerances.claim],
|
|
32
|
+
resourceTypes: [ResourceTypesFhirR4.Composition, ResourceTypesFhirR4.AllergyIntolerance],
|
|
33
|
+
}),
|
|
34
|
+
physicianByEmailEmergency: buildRule({
|
|
35
|
+
identifier: 'urn:uuid:consent-physician-email-emergency',
|
|
36
|
+
actorIdentifier: EXAMPLE_CONSENT_ACCESS_PROVIDER_EMAIL,
|
|
37
|
+
actorRole: HealthcareActorRoles.Physician,
|
|
38
|
+
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
39
|
+
actions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
40
|
+
resourceTypes: [ResourceTypesFhirR4.Composition, ResourceTypesFhirR4.DocumentReference],
|
|
41
|
+
}),
|
|
42
|
+
physicianByOrganizationContinuousCare: buildRule({
|
|
43
|
+
identifier: 'urn:uuid:consent-physician-org-treatment',
|
|
44
|
+
actorIdentifier: EXAMPLE_CONSENT_ACCESS_PROVIDER_DID,
|
|
45
|
+
actorRole: HealthcareActorRoles.Physician,
|
|
46
|
+
purpose: HealthcareConsentPurposes.Treatment,
|
|
47
|
+
actions: [HealthcareBasicSections.Results.claim],
|
|
48
|
+
resourceTypes: [ResourceTypesFhirR4.Composition, ResourceTypesFhirR4.DiagnosticReport],
|
|
49
|
+
}),
|
|
50
|
+
physicianByJurisdictionEmergency: buildRule({
|
|
51
|
+
identifier: 'urn:uuid:consent-physician-jurisdiction-emergency',
|
|
52
|
+
actorIdentifier: EXAMPLE_CONSENT_ACCESS_JURISDICTION,
|
|
53
|
+
actorRole: HealthcareActorRoles.Physician,
|
|
54
|
+
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
55
|
+
actions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
56
|
+
resourceTypes: [ResourceTypesFhirR4.Composition, ResourceTypesFhirR4.DocumentReference],
|
|
57
|
+
}),
|
|
58
|
+
nurseByOrganization: buildRule({
|
|
59
|
+
identifier: 'urn:uuid:consent-nurse-org-treatment',
|
|
60
|
+
actorIdentifier: EXAMPLE_CONSENT_ACCESS_PROVIDER_DID,
|
|
61
|
+
actorRole: HealthcareActorRoles.NursingProfessional,
|
|
62
|
+
purpose: HealthcareConsentPurposes.Treatment,
|
|
63
|
+
actions: [HealthcareBasicSections.HistoryOfMedicationUse.claim],
|
|
64
|
+
resourceTypes: [ResourceTypesFhirR4.Composition, ResourceTypesFhirR4.MedicationStatement],
|
|
65
|
+
}),
|
|
66
|
+
paramedicByJurisdiction: buildRule({
|
|
67
|
+
identifier: 'urn:uuid:consent-paramedic-jurisdiction-emergency',
|
|
68
|
+
actorIdentifier: EXAMPLE_CONSENT_ACCESS_JURISDICTION,
|
|
69
|
+
actorRole: HealthcareActorRoles.Paramedic,
|
|
70
|
+
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
71
|
+
actions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
72
|
+
resourceTypes: [ResourceTypesFhirR4.Composition, ResourceTypesFhirR4.Observation],
|
|
73
|
+
}),
|
|
74
|
+
directPhysicianDenyInsideAllowedOrganization: buildRule({
|
|
75
|
+
identifier: 'urn:uuid:consent-physician-direct-deny',
|
|
76
|
+
actorIdentifier: EXAMPLE_CONSENT_ACCESS_PROVIDER_EMAIL,
|
|
77
|
+
actorRole: HealthcareActorRoles.Physician,
|
|
78
|
+
decision: 'deny',
|
|
79
|
+
purpose: HealthcareConsentPurposes.Treatment,
|
|
80
|
+
actions: [HealthcareBasicSections.Results.claim],
|
|
81
|
+
resourceTypes: [ResourceTypesFhirR4.DiagnosticReport],
|
|
82
|
+
}),
|
|
83
|
+
relatedPersonByEmail: buildRule({
|
|
84
|
+
identifier: 'urn:uuid:consent-related-person-email',
|
|
85
|
+
actorIdentifier: EXAMPLE_CONSENT_ACCESS_RELATED_PERSON_EMAIL,
|
|
86
|
+
actorRole: 'v3-RoleCode|RESPRSN',
|
|
87
|
+
purpose: HealthcareConsentPurposes.Treatment,
|
|
88
|
+
actions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
89
|
+
resourceTypes: [ResourceTypesFhirR4.Composition, ResourceTypesFhirR4.DocumentReference],
|
|
90
|
+
}),
|
|
91
|
+
revokedPhysicianEmailConsent: buildRule({
|
|
92
|
+
identifier: 'urn:uuid:consent-physician-email-revoked',
|
|
93
|
+
actorIdentifier: EXAMPLE_CONSENT_ACCESS_PROVIDER_EMAIL,
|
|
94
|
+
actorRole: HealthcareActorRoles.Physician,
|
|
95
|
+
purpose: HealthcareConsentPurposes.EmergencyTreatment,
|
|
96
|
+
actions: [HealthcareBasicSections.PatientSummaryDocument.claim],
|
|
97
|
+
periodEnd: '2026-05-01T00:00:00Z',
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
export const EXAMPLE_CONSENT_PHONE_EXTENSION_PENDING = Object.freeze({
|
|
101
|
+
target: 'tel:+34600111222',
|
|
102
|
+
status: 'pending-extension',
|
|
103
|
+
reason: 'telephone actor targeting remains an extension concern unless the sector/runtime explicitly enables it',
|
|
104
|
+
});
|
package/dist/examples/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './organization-controller';
|
|
|
3
3
|
export * from './individual-controller';
|
|
4
4
|
export * from './professional';
|
|
5
5
|
export * from './related-person';
|
|
6
|
+
export * from './consent-access';
|
|
6
7
|
export * from './frontend-session';
|
|
7
8
|
export * from './api-flow-examples';
|
|
8
9
|
export * from './contract-examples';
|
package/dist/examples/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from './organization-controller.js';
|
|
|
3
3
|
export * from './individual-controller.js';
|
|
4
4
|
export * from './professional.js';
|
|
5
5
|
export * from './related-person.js';
|
|
6
|
+
export * from './consent-access.js';
|
|
6
7
|
export * from './frontend-session.js';
|
|
7
8
|
export * from './api-flow-examples.js';
|
|
8
9
|
export * from './contract-examples.js';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ConsentRule } from './consent-rule.js';
|
|
2
|
+
export type ConsentActorKind = 'professional' | 'related-person';
|
|
3
|
+
export type ConsentMatchKind = 'direct' | 'organization' | 'jurisdiction' | 'none';
|
|
4
|
+
export type ConsentTargetKind = 'email' | 'did' | 'organization' | 'jurisdiction' | 'phone' | 'unknown';
|
|
5
|
+
export type NormalizedConsentTarget = Readonly<{
|
|
6
|
+
raw: string;
|
|
7
|
+
kind: ConsentTargetKind;
|
|
8
|
+
canonicalValue: string;
|
|
9
|
+
isDirectTarget: boolean;
|
|
10
|
+
isOrganizationTarget: boolean;
|
|
11
|
+
isJurisdictionTarget: boolean;
|
|
12
|
+
isPhoneExtension: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
export type ConsentActorDescriptor = Readonly<{
|
|
15
|
+
actorKind?: ConsentActorKind;
|
|
16
|
+
email?: string;
|
|
17
|
+
did?: string;
|
|
18
|
+
phone?: string;
|
|
19
|
+
organizationDid?: string;
|
|
20
|
+
organizationUrl?: string;
|
|
21
|
+
jurisdiction?: string;
|
|
22
|
+
}>;
|
|
23
|
+
export type ResolvedConsentActor = Readonly<{
|
|
24
|
+
actorKind: ConsentActorKind;
|
|
25
|
+
directTargets: NormalizedConsentTarget[];
|
|
26
|
+
organizationTargets: NormalizedConsentTarget[];
|
|
27
|
+
jurisdictionTargets: NormalizedConsentTarget[];
|
|
28
|
+
phoneTargets: NormalizedConsentTarget[];
|
|
29
|
+
}>;
|
|
30
|
+
export type ConsentCoverageRequest = Readonly<{
|
|
31
|
+
subject?: string;
|
|
32
|
+
actor: ConsentActorDescriptor;
|
|
33
|
+
actorRole?: string;
|
|
34
|
+
purpose?: string;
|
|
35
|
+
sections?: string[];
|
|
36
|
+
resourceTypes?: string[];
|
|
37
|
+
now?: string | Date;
|
|
38
|
+
}>;
|
|
39
|
+
export type ConsentRuleMatch = Readonly<{
|
|
40
|
+
rule: ConsentRule;
|
|
41
|
+
ruleId?: string;
|
|
42
|
+
decision: 'permit' | 'deny';
|
|
43
|
+
matchKind: ConsentMatchKind;
|
|
44
|
+
target: NormalizedConsentTarget;
|
|
45
|
+
precedence: number;
|
|
46
|
+
section?: string;
|
|
47
|
+
resourceType?: string;
|
|
48
|
+
}>;
|
|
49
|
+
export type MissingPermissionSet = Readonly<{
|
|
50
|
+
sections: string[];
|
|
51
|
+
resourceTypes: string[];
|
|
52
|
+
pairs: Array<{
|
|
53
|
+
section?: string;
|
|
54
|
+
resourceType?: string;
|
|
55
|
+
reason: string;
|
|
56
|
+
}>;
|
|
57
|
+
}>;
|
|
58
|
+
export type EffectiveAccessEvaluation = Readonly<{
|
|
59
|
+
allowed: boolean;
|
|
60
|
+
denied: boolean;
|
|
61
|
+
partial: boolean;
|
|
62
|
+
subject?: string;
|
|
63
|
+
actor: ResolvedConsentActor;
|
|
64
|
+
matchedRules: ConsentRuleMatch[];
|
|
65
|
+
winningRules: ConsentRuleMatch[];
|
|
66
|
+
explicitDenials: ConsentRuleMatch[];
|
|
67
|
+
allowedSections: string[];
|
|
68
|
+
deniedSections: string[];
|
|
69
|
+
allowedResourceTypes: string[];
|
|
70
|
+
deniedResourceTypes: string[];
|
|
71
|
+
missing: MissingPermissionSet;
|
|
72
|
+
}>;
|
|
73
|
+
export type ActiveConsentView = Readonly<{
|
|
74
|
+
activeRules: ConsentRule[];
|
|
75
|
+
byDirectTarget: Record<string, ConsentRule[]>;
|
|
76
|
+
byOrganizationTarget: Record<string, ConsentRule[]>;
|
|
77
|
+
byJurisdictionTarget: Record<string, ConsentRule[]>;
|
|
78
|
+
byPhoneTarget: Record<string, ConsentRule[]>;
|
|
79
|
+
}>;
|
package/dist/models/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './confidential-job';
|
|
|
9
9
|
export * from './confidential-message';
|
|
10
10
|
export * from './confidential-storage';
|
|
11
11
|
export * from './consent-rule';
|
|
12
|
+
export * from './consent-access';
|
|
12
13
|
export * from './crypto';
|
|
13
14
|
export * from './device-license';
|
|
14
15
|
export * from './did';
|
package/dist/models/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from './confidential-job.js';
|
|
|
9
9
|
export * from './confidential-message.js';
|
|
10
10
|
export * from './confidential-storage.js';
|
|
11
11
|
export * from './consent-rule.js';
|
|
12
|
+
export * from './consent-access.js';
|
|
12
13
|
export * from './crypto.js';
|
|
13
14
|
export * from './device-license.js';
|
|
14
15
|
export * from './did.js';
|
package/dist/utils/consent.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ActiveConsentView, ConsentActorDescriptor, ConsentActorKind, ConsentCoverageRequest, EffectiveAccessEvaluation, NormalizedConsentTarget, ResolvedConsentActor } from '../models/consent-access.js';
|
|
2
|
+
import type { ConsentRule } from '../models/consent-rule.js';
|
|
1
3
|
/**
|
|
2
4
|
* Legacy structured actor selector kept for backwards compatibility while the
|
|
3
5
|
* SDK converges on the canonical flat consent actor identifier contract.
|
|
@@ -142,3 +144,75 @@ export declare function buildConsentClaimsSimple(input: BuildConsentClaimsSimple
|
|
|
142
144
|
* @param options.consentIdentifierFactory Optional fallback identifier factory.
|
|
143
145
|
*/
|
|
144
146
|
export declare function buildConsentClaimsSimpleWithCid(input: BuildConsentClaimsSimpleInput, options?: BuildConsentClaimsSimpleOptions): ConsentClaimsWithCidResult;
|
|
147
|
+
/**
|
|
148
|
+
* Normalizes a consent target token into a reusable matching descriptor.
|
|
149
|
+
*
|
|
150
|
+
* The helper keeps the current GW contract:
|
|
151
|
+
* - the first precedence tier is intended for concrete professional email matches
|
|
152
|
+
* - direct runtime selectors may still include email / `did:web` / phone
|
|
153
|
+
* - organization targets are normalized to `did:web:<host>`
|
|
154
|
+
* - jurisdictions resolve to ISO-like uppercase codes
|
|
155
|
+
* - phone targets stay marked as extension-specific
|
|
156
|
+
*
|
|
157
|
+
* @param input Raw target input from a rule or runtime request.
|
|
158
|
+
* @param options.actorKind Optional actor family hint.
|
|
159
|
+
* @param options.preferOrganizationDid When true, base `did:web:<host>` values are treated as organization targets.
|
|
160
|
+
*/
|
|
161
|
+
export declare function normalizeConsentTarget(input: string, options?: {
|
|
162
|
+
actorKind?: ConsentActorKind;
|
|
163
|
+
preferOrganizationDid?: boolean;
|
|
164
|
+
}): NormalizedConsentTarget;
|
|
165
|
+
/**
|
|
166
|
+
* Resolves all actor match targets used by consent evaluation.
|
|
167
|
+
*
|
|
168
|
+
* Organization matching prefers explicit `organizationDid` / `organizationUrl`.
|
|
169
|
+
* When those are absent but an email exists, the email domain is exposed as a
|
|
170
|
+
* fallback organization target through `did:web:<domain>`.
|
|
171
|
+
*
|
|
172
|
+
* @param actor Runtime actor descriptor.
|
|
173
|
+
*/
|
|
174
|
+
export declare function resolveConsentActor(actor: ConsentActorDescriptor): ResolvedConsentActor;
|
|
175
|
+
/**
|
|
176
|
+
* Returns `true` when a consent rule is currently active.
|
|
177
|
+
*
|
|
178
|
+
* A rule is active when:
|
|
179
|
+
* - it matches the requested subject when one is provided
|
|
180
|
+
* - `Consent.period-start` is absent or already effective
|
|
181
|
+
* - `Consent.period-end` is absent or still in the future
|
|
182
|
+
*
|
|
183
|
+
* @param rule Consent rule to inspect.
|
|
184
|
+
* @param options.subject Optional subject filter.
|
|
185
|
+
* @param options.now Optional evaluation timestamp.
|
|
186
|
+
*/
|
|
187
|
+
export declare function isConsentRuleActive(rule: ConsentRule, options?: {
|
|
188
|
+
subject?: string;
|
|
189
|
+
now?: string | Date;
|
|
190
|
+
}): boolean;
|
|
191
|
+
/**
|
|
192
|
+
* Builds an aggregated active-consent view grouped by target kind.
|
|
193
|
+
*
|
|
194
|
+
* @param rules Full consent-rule set available for the subject.
|
|
195
|
+
* @param options.subject Optional subject filter.
|
|
196
|
+
* @param options.now Optional evaluation timestamp.
|
|
197
|
+
*/
|
|
198
|
+
export declare function groupActiveConsentsByTarget(rules: ConsentRule[], options?: {
|
|
199
|
+
subject?: string;
|
|
200
|
+
now?: string | Date;
|
|
201
|
+
}): ActiveConsentView;
|
|
202
|
+
/**
|
|
203
|
+
* Evaluates effective consent coverage for a runtime access request.
|
|
204
|
+
*
|
|
205
|
+
* Precedence implemented by the shared evaluator:
|
|
206
|
+
* 1. explicit deny for a concrete email
|
|
207
|
+
* 2. explicit permit for a concrete email
|
|
208
|
+
* 3. organization-scoped decisions
|
|
209
|
+
* 4. jurisdiction-scoped decisions
|
|
210
|
+
* 5. default deny
|
|
211
|
+
*
|
|
212
|
+
* Resource-type evaluation is optional: when a rule does not carry an explicit
|
|
213
|
+
* resource-type filter, it is treated as section-scoped wildcard coverage.
|
|
214
|
+
*
|
|
215
|
+
* @param rules Full consent-rule set available to the caller.
|
|
216
|
+
* @param request Runtime request to evaluate.
|
|
217
|
+
*/
|
|
218
|
+
export declare function evaluateConsentCoverage(rules: ConsentRule[], request: ConsentCoverageRequest): EffectiveAccessEvaluation;
|
package/dist/utils/consent.js
CHANGED
|
@@ -190,3 +190,488 @@ export function buildConsentClaimsSimpleWithCid(input, options = {}) {
|
|
|
190
190
|
claimsCid: assigned.cid,
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
|
+
function normalizeJurisdiction(value) {
|
|
194
|
+
const trimmed = String(value || '').trim();
|
|
195
|
+
if (!trimmed)
|
|
196
|
+
return '';
|
|
197
|
+
if (/^[A-Z]{2}$/i.test(trimmed))
|
|
198
|
+
return trimmed.toUpperCase();
|
|
199
|
+
const isoStd = trimmed.match(/^urn:iso:std:iso:3166\|([a-z]{2})$/i);
|
|
200
|
+
if (isoStd)
|
|
201
|
+
return isoStd[1].toUpperCase();
|
|
202
|
+
const iso = trimmed.match(/^urn:iso:3166(?:-2)?:([a-z]{2})(?:[-:].*)?$/i);
|
|
203
|
+
if (iso)
|
|
204
|
+
return iso[1].toUpperCase();
|
|
205
|
+
return trimmed.toUpperCase();
|
|
206
|
+
}
|
|
207
|
+
function normalizeDidWebFromUrl(value) {
|
|
208
|
+
const raw = String(value || '').trim();
|
|
209
|
+
if (!raw)
|
|
210
|
+
return undefined;
|
|
211
|
+
if (raw.startsWith('did:web:'))
|
|
212
|
+
return raw.toLowerCase();
|
|
213
|
+
try {
|
|
214
|
+
const parsed = raw.includes('://') ? new URL(raw) : new URL(`https://${raw}`);
|
|
215
|
+
return parsed.hostname ? `did:web:${parsed.hostname.toLowerCase()}` : undefined;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function normalizeConsentRoleValue(value) {
|
|
222
|
+
const trimmed = String(value || '').trim();
|
|
223
|
+
if (!trimmed || trimmed === '*')
|
|
224
|
+
return trimmed;
|
|
225
|
+
const [system, code] = trimmed.includes('|') ? trimmed.split('|', 2) : ['', trimmed];
|
|
226
|
+
return system
|
|
227
|
+
? `${system.trim().toLowerCase()}|${code.trim()}`
|
|
228
|
+
: trimmed.toLowerCase();
|
|
229
|
+
}
|
|
230
|
+
function splitCsv(value) {
|
|
231
|
+
return String(value || '')
|
|
232
|
+
.split(',')
|
|
233
|
+
.map((item) => item.trim())
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
}
|
|
236
|
+
function normalizeSectionToken(value) {
|
|
237
|
+
const trimmed = String(value || '').trim();
|
|
238
|
+
if (!trimmed || !trimmed.includes('|'))
|
|
239
|
+
return trimmed;
|
|
240
|
+
const [system, code] = trimmed.split('|', 2);
|
|
241
|
+
const normalizedSystem = system
|
|
242
|
+
.trim()
|
|
243
|
+
.toLowerCase()
|
|
244
|
+
.replace(/^https?:\/\/loinc\.org$/i, 'loinc')
|
|
245
|
+
.replace(/^urn:oid:2\.16\.840\.1\.113883\.6\.1$/i, 'loinc');
|
|
246
|
+
return `${normalizedSystem}|${code.trim()}`;
|
|
247
|
+
}
|
|
248
|
+
function uniqueTargets(targets) {
|
|
249
|
+
const seen = new Set();
|
|
250
|
+
const result = [];
|
|
251
|
+
for (const target of targets) {
|
|
252
|
+
const key = `${target.kind}:${target.canonicalValue}:${target.isDirectTarget ? 'd' : ''}${target.isOrganizationTarget ? 'o' : ''}${target.isJurisdictionTarget ? 'j' : ''}`;
|
|
253
|
+
if (seen.has(key))
|
|
254
|
+
continue;
|
|
255
|
+
seen.add(key);
|
|
256
|
+
result.push(target);
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Normalizes a consent target token into a reusable matching descriptor.
|
|
262
|
+
*
|
|
263
|
+
* The helper keeps the current GW contract:
|
|
264
|
+
* - the first precedence tier is intended for concrete professional email matches
|
|
265
|
+
* - direct runtime selectors may still include email / `did:web` / phone
|
|
266
|
+
* - organization targets are normalized to `did:web:<host>`
|
|
267
|
+
* - jurisdictions resolve to ISO-like uppercase codes
|
|
268
|
+
* - phone targets stay marked as extension-specific
|
|
269
|
+
*
|
|
270
|
+
* @param input Raw target input from a rule or runtime request.
|
|
271
|
+
* @param options.actorKind Optional actor family hint.
|
|
272
|
+
* @param options.preferOrganizationDid When true, base `did:web:<host>` values are treated as organization targets.
|
|
273
|
+
*/
|
|
274
|
+
export function normalizeConsentTarget(input, options = {}) {
|
|
275
|
+
const raw = String(input || '').trim();
|
|
276
|
+
const parsed = parseConsentActorToken(raw);
|
|
277
|
+
if (parsed?.kind === 'email') {
|
|
278
|
+
return {
|
|
279
|
+
raw,
|
|
280
|
+
kind: 'email',
|
|
281
|
+
canonicalValue: parsed.value,
|
|
282
|
+
isDirectTarget: true,
|
|
283
|
+
isOrganizationTarget: false,
|
|
284
|
+
isJurisdictionTarget: false,
|
|
285
|
+
isPhoneExtension: false,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (parsed?.kind === 'phone') {
|
|
289
|
+
return {
|
|
290
|
+
raw,
|
|
291
|
+
kind: 'phone',
|
|
292
|
+
canonicalValue: parsed.value,
|
|
293
|
+
isDirectTarget: true,
|
|
294
|
+
isOrganizationTarget: false,
|
|
295
|
+
isJurisdictionTarget: false,
|
|
296
|
+
isPhoneExtension: true,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (parsed?.kind === 'country') {
|
|
300
|
+
return {
|
|
301
|
+
raw,
|
|
302
|
+
kind: 'jurisdiction',
|
|
303
|
+
canonicalValue: parsed.value,
|
|
304
|
+
isDirectTarget: false,
|
|
305
|
+
isOrganizationTarget: false,
|
|
306
|
+
isJurisdictionTarget: true,
|
|
307
|
+
isPhoneExtension: false,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const organizationDid = normalizeDidWebFromUrl(raw);
|
|
311
|
+
const isEmployeeLikeDid = /^did:web:[^:\s]+:(employee|family|relatedperson|related-person):/i.test(raw);
|
|
312
|
+
if (raw.startsWith('did:')) {
|
|
313
|
+
const preferOrganizationDid = Boolean(options.preferOrganizationDid && !isEmployeeLikeDid);
|
|
314
|
+
return {
|
|
315
|
+
raw,
|
|
316
|
+
kind: preferOrganizationDid ? 'organization' : 'did',
|
|
317
|
+
canonicalValue: raw.toLowerCase(),
|
|
318
|
+
isDirectTarget: !preferOrganizationDid,
|
|
319
|
+
isOrganizationTarget: preferOrganizationDid,
|
|
320
|
+
isJurisdictionTarget: false,
|
|
321
|
+
isPhoneExtension: false,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (organizationDid) {
|
|
325
|
+
return {
|
|
326
|
+
raw,
|
|
327
|
+
kind: 'organization',
|
|
328
|
+
canonicalValue: organizationDid,
|
|
329
|
+
isDirectTarget: false,
|
|
330
|
+
isOrganizationTarget: true,
|
|
331
|
+
isJurisdictionTarget: false,
|
|
332
|
+
isPhoneExtension: false,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const jurisdiction = normalizeJurisdiction(raw);
|
|
336
|
+
if (/^[A-Z]{2}$/.test(jurisdiction)) {
|
|
337
|
+
return {
|
|
338
|
+
raw,
|
|
339
|
+
kind: 'jurisdiction',
|
|
340
|
+
canonicalValue: jurisdiction,
|
|
341
|
+
isDirectTarget: false,
|
|
342
|
+
isOrganizationTarget: false,
|
|
343
|
+
isJurisdictionTarget: true,
|
|
344
|
+
isPhoneExtension: false,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
raw,
|
|
349
|
+
kind: 'unknown',
|
|
350
|
+
canonicalValue: raw,
|
|
351
|
+
isDirectTarget: false,
|
|
352
|
+
isOrganizationTarget: false,
|
|
353
|
+
isJurisdictionTarget: false,
|
|
354
|
+
isPhoneExtension: false,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Resolves all actor match targets used by consent evaluation.
|
|
359
|
+
*
|
|
360
|
+
* Organization matching prefers explicit `organizationDid` / `organizationUrl`.
|
|
361
|
+
* When those are absent but an email exists, the email domain is exposed as a
|
|
362
|
+
* fallback organization target through `did:web:<domain>`.
|
|
363
|
+
*
|
|
364
|
+
* @param actor Runtime actor descriptor.
|
|
365
|
+
*/
|
|
366
|
+
export function resolveConsentActor(actor) {
|
|
367
|
+
const actorKind = actor.actorKind || 'professional';
|
|
368
|
+
const directTargets = [];
|
|
369
|
+
const organizationTargets = [];
|
|
370
|
+
const jurisdictionTargets = [];
|
|
371
|
+
const phoneTargets = [];
|
|
372
|
+
const email = String(actor.email || '').trim();
|
|
373
|
+
if (email) {
|
|
374
|
+
const direct = normalizeConsentTarget(email, { actorKind });
|
|
375
|
+
directTargets.push(direct);
|
|
376
|
+
const domain = email.includes('@') ? email.split('@')[1] : '';
|
|
377
|
+
const fallbackOrganizationDid = normalizeDidWebFromUrl(domain);
|
|
378
|
+
if (fallbackOrganizationDid) {
|
|
379
|
+
organizationTargets.push(normalizeConsentTarget(fallbackOrganizationDid, {
|
|
380
|
+
actorKind,
|
|
381
|
+
preferOrganizationDid: true,
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const did = String(actor.did || '').trim();
|
|
386
|
+
if (did) {
|
|
387
|
+
directTargets.push(normalizeConsentTarget(did, { actorKind }));
|
|
388
|
+
const baseOrgDid = did.startsWith('did:web:') ? `did:web:${did.slice('did:web:'.length).split(':')[0]}` : '';
|
|
389
|
+
if (baseOrgDid) {
|
|
390
|
+
organizationTargets.push(normalizeConsentTarget(baseOrgDid, {
|
|
391
|
+
actorKind,
|
|
392
|
+
preferOrganizationDid: true,
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const phone = normalizePhone(String(actor.phone || ''));
|
|
397
|
+
if (phone) {
|
|
398
|
+
const phoneTarget = normalizeConsentTarget(`tel:${phone}`, { actorKind });
|
|
399
|
+
directTargets.push(phoneTarget);
|
|
400
|
+
phoneTargets.push(phoneTarget);
|
|
401
|
+
}
|
|
402
|
+
const orgDid = String(actor.organizationDid || '').trim();
|
|
403
|
+
if (orgDid) {
|
|
404
|
+
organizationTargets.push(normalizeConsentTarget(orgDid, {
|
|
405
|
+
actorKind,
|
|
406
|
+
preferOrganizationDid: true,
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
const orgUrl = String(actor.organizationUrl || '').trim();
|
|
410
|
+
if (orgUrl) {
|
|
411
|
+
const normalized = normalizeDidWebFromUrl(orgUrl);
|
|
412
|
+
if (normalized) {
|
|
413
|
+
organizationTargets.push(normalizeConsentTarget(normalized, {
|
|
414
|
+
actorKind,
|
|
415
|
+
preferOrganizationDid: true,
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const jurisdiction = normalizeJurisdiction(String(actor.jurisdiction || ''));
|
|
420
|
+
if (jurisdiction) {
|
|
421
|
+
jurisdictionTargets.push(normalizeConsentTarget(jurisdiction, { actorKind }));
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
actorKind,
|
|
425
|
+
directTargets: uniqueTargets(directTargets),
|
|
426
|
+
organizationTargets: uniqueTargets(organizationTargets),
|
|
427
|
+
jurisdictionTargets: uniqueTargets(jurisdictionTargets),
|
|
428
|
+
phoneTargets: uniqueTargets(phoneTargets),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Returns `true` when a consent rule is currently active.
|
|
433
|
+
*
|
|
434
|
+
* A rule is active when:
|
|
435
|
+
* - it matches the requested subject when one is provided
|
|
436
|
+
* - `Consent.period-start` is absent or already effective
|
|
437
|
+
* - `Consent.period-end` is absent or still in the future
|
|
438
|
+
*
|
|
439
|
+
* @param rule Consent rule to inspect.
|
|
440
|
+
* @param options.subject Optional subject filter.
|
|
441
|
+
* @param options.now Optional evaluation timestamp.
|
|
442
|
+
*/
|
|
443
|
+
export function isConsentRuleActive(rule, options = {}) {
|
|
444
|
+
if (options.subject && String(rule['Consent.subject'] || '').trim() !== String(options.subject || '').trim()) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const now = options.now instanceof Date
|
|
448
|
+
? options.now.getTime()
|
|
449
|
+
: options.now
|
|
450
|
+
? new Date(options.now).getTime()
|
|
451
|
+
: Date.now();
|
|
452
|
+
const periodStart = String(rule['Consent.period-start'] || '').trim();
|
|
453
|
+
const periodEnd = String(rule['Consent.period-end'] || '').trim();
|
|
454
|
+
if (periodStart && !Number.isNaN(Date.parse(periodStart)) && Date.parse(periodStart) > now)
|
|
455
|
+
return false;
|
|
456
|
+
if (periodEnd && !Number.isNaN(Date.parse(periodEnd)) && Date.parse(periodEnd) < now)
|
|
457
|
+
return false;
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
function groupRulesBy(rules, predicate) {
|
|
461
|
+
const groups = {};
|
|
462
|
+
for (const rule of rules) {
|
|
463
|
+
for (const token of splitCsv(rule['Consent.actor-identifier'])) {
|
|
464
|
+
const normalized = normalizeConsentTarget(token, { preferOrganizationDid: true });
|
|
465
|
+
if (!predicate(normalized))
|
|
466
|
+
continue;
|
|
467
|
+
groups[normalized.canonicalValue] ||= [];
|
|
468
|
+
groups[normalized.canonicalValue].push(rule);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return groups;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Builds an aggregated active-consent view grouped by target kind.
|
|
475
|
+
*
|
|
476
|
+
* @param rules Full consent-rule set available for the subject.
|
|
477
|
+
* @param options.subject Optional subject filter.
|
|
478
|
+
* @param options.now Optional evaluation timestamp.
|
|
479
|
+
*/
|
|
480
|
+
export function groupActiveConsentsByTarget(rules, options = {}) {
|
|
481
|
+
const activeRules = rules.filter((rule) => isConsentRuleActive(rule, options));
|
|
482
|
+
return {
|
|
483
|
+
activeRules,
|
|
484
|
+
byDirectTarget: groupRulesBy(activeRules, (target) => target.isDirectTarget && !target.isPhoneExtension),
|
|
485
|
+
byOrganizationTarget: groupRulesBy(activeRules, (target) => target.isOrganizationTarget),
|
|
486
|
+
byJurisdictionTarget: groupRulesBy(activeRules, (target) => target.isJurisdictionTarget),
|
|
487
|
+
byPhoneTarget: groupRulesBy(activeRules, (target) => target.isPhoneExtension),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function normalizeRequestedList(values, wildcard = '*') {
|
|
491
|
+
const normalized = (values || []).map((value) => String(value || '').trim()).filter(Boolean);
|
|
492
|
+
return normalized.length ? Array.from(new Set(normalized)) : [wildcard];
|
|
493
|
+
}
|
|
494
|
+
function extractRuleResourceTypes(rule) {
|
|
495
|
+
const candidates = [
|
|
496
|
+
rule['Consent.resourceType'],
|
|
497
|
+
rule['Consent.resource-type'],
|
|
498
|
+
rule['Consent.resource'],
|
|
499
|
+
rule['Consent.data-type'],
|
|
500
|
+
];
|
|
501
|
+
for (const candidate of candidates) {
|
|
502
|
+
const values = splitCsv(candidate);
|
|
503
|
+
if (values.length > 0)
|
|
504
|
+
return values;
|
|
505
|
+
}
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
function ruleMatchesRole(rule, actorRole) {
|
|
509
|
+
const ruleRoles = splitCsv(rule['Consent.actor-role']).map(normalizeConsentRoleValue).filter(Boolean);
|
|
510
|
+
if (ruleRoles.length === 0 || ruleRoles.includes('*'))
|
|
511
|
+
return true;
|
|
512
|
+
const requestedRole = normalizeConsentRoleValue(String(actorRole || ''));
|
|
513
|
+
return !!requestedRole && ruleRoles.includes(requestedRole);
|
|
514
|
+
}
|
|
515
|
+
function ruleMatchesPurpose(rule, purpose) {
|
|
516
|
+
const rulePurpose = String(rule['Consent.purpose'] || '').trim();
|
|
517
|
+
if (!purpose || !rulePurpose)
|
|
518
|
+
return true;
|
|
519
|
+
return rulePurpose === purpose;
|
|
520
|
+
}
|
|
521
|
+
function ruleMatchesSection(rule, section) {
|
|
522
|
+
if (!section || section === '*')
|
|
523
|
+
return true;
|
|
524
|
+
const requestedSection = normalizeSectionToken(section);
|
|
525
|
+
const actions = splitCsv(rule['Consent.action']).map(normalizeSectionToken);
|
|
526
|
+
if (actions.length === 0)
|
|
527
|
+
return false;
|
|
528
|
+
return actions.includes(requestedSection) || actions.includes('*');
|
|
529
|
+
}
|
|
530
|
+
function ruleMatchesResourceType(rule, resourceType) {
|
|
531
|
+
if (!resourceType || resourceType === '*')
|
|
532
|
+
return true;
|
|
533
|
+
const resourceTypes = extractRuleResourceTypes(rule);
|
|
534
|
+
if (resourceTypes.length === 0)
|
|
535
|
+
return true;
|
|
536
|
+
return resourceTypes.includes(resourceType) || resourceTypes.includes('*');
|
|
537
|
+
}
|
|
538
|
+
function resolveRuleMatch(rule, actor) {
|
|
539
|
+
for (const token of splitCsv(rule['Consent.actor-identifier'])) {
|
|
540
|
+
const normalized = normalizeConsentTarget(token, { preferOrganizationDid: true });
|
|
541
|
+
if (actor.directTargets.some((target) => target.canonicalValue === normalized.canonicalValue)) {
|
|
542
|
+
return { matchKind: 'direct', target: normalized, precedenceBase: 10 };
|
|
543
|
+
}
|
|
544
|
+
if (actor.organizationTargets.some((target) => target.canonicalValue === normalized.canonicalValue)) {
|
|
545
|
+
return { matchKind: 'organization', target: normalized, precedenceBase: 20 };
|
|
546
|
+
}
|
|
547
|
+
if (actor.jurisdictionTargets.some((target) => target.canonicalValue === normalized.canonicalValue)) {
|
|
548
|
+
return { matchKind: 'jurisdiction', target: normalized, precedenceBase: 30 };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return { matchKind: 'none' };
|
|
552
|
+
}
|
|
553
|
+
function toRuleMatch(rule, actor, section, resourceType) {
|
|
554
|
+
const resolved = resolveRuleMatch(rule, actor);
|
|
555
|
+
if (!resolved.target || resolved.matchKind === 'none' || resolved.precedenceBase === undefined)
|
|
556
|
+
return undefined;
|
|
557
|
+
const decision = rule['Consent.decision'];
|
|
558
|
+
const precedence = resolved.precedenceBase + (decision === 'deny' ? 0 : 1);
|
|
559
|
+
return {
|
|
560
|
+
rule,
|
|
561
|
+
ruleId: String((rule.id) || '').trim() || undefined,
|
|
562
|
+
decision,
|
|
563
|
+
matchKind: resolved.matchKind,
|
|
564
|
+
target: resolved.target,
|
|
565
|
+
precedence,
|
|
566
|
+
section: section === '*' ? undefined : section,
|
|
567
|
+
resourceType: resourceType === '*' ? undefined : resourceType,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function emptyMissing() {
|
|
571
|
+
return { sections: [], resourceTypes: [], pairs: [] };
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Evaluates effective consent coverage for a runtime access request.
|
|
575
|
+
*
|
|
576
|
+
* Precedence implemented by the shared evaluator:
|
|
577
|
+
* 1. explicit deny for a concrete email
|
|
578
|
+
* 2. explicit permit for a concrete email
|
|
579
|
+
* 3. organization-scoped decisions
|
|
580
|
+
* 4. jurisdiction-scoped decisions
|
|
581
|
+
* 5. default deny
|
|
582
|
+
*
|
|
583
|
+
* Resource-type evaluation is optional: when a rule does not carry an explicit
|
|
584
|
+
* resource-type filter, it is treated as section-scoped wildcard coverage.
|
|
585
|
+
*
|
|
586
|
+
* @param rules Full consent-rule set available to the caller.
|
|
587
|
+
* @param request Runtime request to evaluate.
|
|
588
|
+
*/
|
|
589
|
+
export function evaluateConsentCoverage(rules, request) {
|
|
590
|
+
const actor = resolveConsentActor(request.actor);
|
|
591
|
+
const sections = normalizeRequestedList(request.sections);
|
|
592
|
+
const resourceTypes = normalizeRequestedList(request.resourceTypes);
|
|
593
|
+
const activeRules = rules.filter((rule) => isConsentRuleActive(rule, {
|
|
594
|
+
subject: request.subject,
|
|
595
|
+
now: request.now,
|
|
596
|
+
}));
|
|
597
|
+
const matchedRules = [];
|
|
598
|
+
const winningRules = [];
|
|
599
|
+
const explicitDenials = [];
|
|
600
|
+
const allowedSections = new Set();
|
|
601
|
+
const deniedSections = new Set();
|
|
602
|
+
const allowedResourceTypes = new Set();
|
|
603
|
+
const deniedResourceTypes = new Set();
|
|
604
|
+
const missing = emptyMissing();
|
|
605
|
+
for (const section of sections) {
|
|
606
|
+
for (const resourceType of resourceTypes) {
|
|
607
|
+
const candidates = activeRules
|
|
608
|
+
.filter((rule) => ruleMatchesRole(rule, request.actorRole)
|
|
609
|
+
&& ruleMatchesPurpose(rule, request.purpose)
|
|
610
|
+
&& ruleMatchesSection(rule, section)
|
|
611
|
+
&& ruleMatchesResourceType(rule, resourceType))
|
|
612
|
+
.map((rule) => toRuleMatch(rule, actor, section, resourceType))
|
|
613
|
+
.filter((match) => Boolean(match))
|
|
614
|
+
.sort((a, b) => a.precedence - b.precedence);
|
|
615
|
+
matchedRules.push(...candidates);
|
|
616
|
+
const winner = candidates[0];
|
|
617
|
+
if (!winner) {
|
|
618
|
+
missing.pairs.push({
|
|
619
|
+
section: section === '*' ? undefined : section,
|
|
620
|
+
resourceType: resourceType === '*' ? undefined : resourceType,
|
|
621
|
+
reason: 'default-deny-no-active-consent',
|
|
622
|
+
});
|
|
623
|
+
if (section !== '*')
|
|
624
|
+
missing.sections.push(section);
|
|
625
|
+
if (resourceType !== '*')
|
|
626
|
+
missing.resourceTypes.push(resourceType);
|
|
627
|
+
if (section !== '*')
|
|
628
|
+
deniedSections.add(section);
|
|
629
|
+
if (resourceType !== '*')
|
|
630
|
+
deniedResourceTypes.add(resourceType);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
winningRules.push(winner);
|
|
634
|
+
if (winner.decision === 'deny') {
|
|
635
|
+
explicitDenials.push(winner);
|
|
636
|
+
if (section !== '*')
|
|
637
|
+
deniedSections.add(section);
|
|
638
|
+
if (resourceType !== '*')
|
|
639
|
+
deniedResourceTypes.add(resourceType);
|
|
640
|
+
missing.pairs.push({
|
|
641
|
+
section: section === '*' ? undefined : section,
|
|
642
|
+
resourceType: resourceType === '*' ? undefined : resourceType,
|
|
643
|
+
reason: `explicit-${winner.matchKind}-deny`,
|
|
644
|
+
});
|
|
645
|
+
if (section !== '*')
|
|
646
|
+
missing.sections.push(section);
|
|
647
|
+
if (resourceType !== '*')
|
|
648
|
+
missing.resourceTypes.push(resourceType);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
if (section !== '*')
|
|
652
|
+
allowedSections.add(section);
|
|
653
|
+
if (resourceType !== '*')
|
|
654
|
+
allowedResourceTypes.add(resourceType);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
allowed: missing.pairs.length === 0,
|
|
660
|
+
denied: missing.pairs.length > 0 && allowedSections.size === 0 && allowedResourceTypes.size === 0,
|
|
661
|
+
partial: missing.pairs.length > 0 && (allowedSections.size > 0 || allowedResourceTypes.size > 0),
|
|
662
|
+
subject: request.subject,
|
|
663
|
+
actor,
|
|
664
|
+
matchedRules,
|
|
665
|
+
winningRules,
|
|
666
|
+
explicitDenials,
|
|
667
|
+
allowedSections: Array.from(allowedSections),
|
|
668
|
+
deniedSections: Array.from(deniedSections),
|
|
669
|
+
allowedResourceTypes: Array.from(allowedResourceTypes),
|
|
670
|
+
deniedResourceTypes: Array.from(deniedResourceTypes),
|
|
671
|
+
missing: {
|
|
672
|
+
sections: Array.from(new Set(missing.sections)),
|
|
673
|
+
resourceTypes: Array.from(new Set(missing.resourceTypes)),
|
|
674
|
+
pairs: missing.pairs,
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
}
|