@docknetwork/wallet-sdk-core 1.7.7-alpha.0 → 1.9.0
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/lib/cloud-wallet.d.ts +79 -3
- package/lib/cloud-wallet.d.ts.map +1 -1
- package/lib/cloud-wallet.js +147 -14
- package/lib/cloud-wallet.js.map +1 -1
- package/lib/credential-provider.d.ts.map +1 -1
- package/lib/credential-provider.js +10 -4
- package/lib/credential-provider.js.map +1 -1
- package/lib/delegation/delegation-chain.d.ts +8 -0
- package/lib/delegation/delegation-chain.d.ts.map +1 -0
- package/lib/delegation/delegation-chain.js +33 -0
- package/lib/delegation/delegation-chain.js.map +1 -0
- package/lib/delegation/delegation-fixtures.d.ts +69 -0
- package/lib/delegation/delegation-fixtures.d.ts.map +1 -0
- package/lib/delegation/delegation-fixtures.js +553 -0
- package/lib/delegation/delegation-fixtures.js.map +1 -0
- package/lib/delegation/delegation-issuance.d.ts +19 -0
- package/lib/delegation/delegation-issuance.d.ts.map +1 -0
- package/lib/delegation/delegation-issuance.js +60 -0
- package/lib/delegation/delegation-issuance.js.map +1 -0
- package/lib/delegation/delegation-offer.d.ts +84 -0
- package/lib/delegation/delegation-offer.d.ts.map +1 -0
- package/lib/delegation/delegation-offer.js +349 -0
- package/lib/delegation/delegation-offer.js.map +1 -0
- package/lib/delegation/delegation-policy-validation.d.ts +28 -0
- package/lib/delegation/delegation-policy-validation.d.ts.map +1 -0
- package/lib/delegation/delegation-policy-validation.js +170 -0
- package/lib/delegation/delegation-policy-validation.js.map +1 -0
- package/lib/delegation/delegation-policy.d.ts +21 -0
- package/lib/delegation/delegation-policy.d.ts.map +1 -0
- package/lib/delegation/delegation-policy.js +73 -0
- package/lib/delegation/delegation-policy.js.map +1 -0
- package/lib/delegation/delegation-tree.d.ts +17 -0
- package/lib/delegation/delegation-tree.d.ts.map +1 -0
- package/lib/delegation/delegation-tree.js +58 -0
- package/lib/delegation/delegation-tree.js.map +1 -0
- package/lib/delegation/delegation-types.d.ts +56 -0
- package/lib/delegation/delegation-types.d.ts.map +1 -0
- package/lib/delegation/delegation-types.js +3 -0
- package/lib/delegation/delegation-types.js.map +1 -0
- package/lib/delegation/delegation-utils.d.ts +3 -0
- package/lib/delegation/delegation-utils.d.ts.map +1 -0
- package/lib/delegation/delegation-utils.js +10 -0
- package/lib/delegation/delegation-utils.js.map +1 -0
- package/lib/did-provider.d.ts +2 -1
- package/lib/did-provider.d.ts.map +1 -1
- package/lib/did-provider.js +11 -7
- package/lib/did-provider.js.map +1 -1
- package/lib/message-provider.js +1 -1
- package/lib/message-provider.js.map +1 -1
- package/lib/verification-controller.d.ts +30 -11
- package/lib/verification-controller.d.ts.map +1 -1
- package/lib/verification-controller.js +372 -68
- package/lib/verification-controller.js.map +1 -1
- package/package.json +3 -3
- package/src/cloud-wallet.test.js +369 -0
- package/src/cloud-wallet.ts +206 -18
- package/src/credential-provider.ts +13 -4
- package/src/delegation/delegation-chain.test.ts +64 -0
- package/src/delegation/delegation-chain.ts +34 -0
- package/src/delegation/delegation-fixtures.ts +552 -0
- package/src/delegation/delegation-issuance.ts +92 -0
- package/src/delegation/delegation-offer.ts +488 -0
- package/src/delegation/delegation-policy-validation.test.ts +237 -0
- package/src/delegation/delegation-policy-validation.ts +281 -0
- package/src/delegation/delegation-policy.ts +100 -0
- package/src/delegation/delegation-tree.test.ts +110 -0
- package/src/delegation/delegation-tree.ts +60 -0
- package/src/delegation/delegation-types.ts +65 -0
- package/src/delegation/delegation-utils.ts +10 -0
- package/src/did-provider.ts +10 -6
- package/src/globals.d.ts +6 -0
- package/src/message-provider.ts +1 -1
- package/src/verification-controller.test.ts +23 -0
- package/src/verification-controller.ts +534 -82
- package/tsconfig.build.json +2 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import {DelegationPolicy} from './delegation-types';
|
|
2
|
+
import {
|
|
3
|
+
delegationPolicyPharmacy,
|
|
4
|
+
delegationPolicyTravelAgent,
|
|
5
|
+
} from './delegation-fixtures';
|
|
6
|
+
import {
|
|
7
|
+
assertPolicyConformsToParent,
|
|
8
|
+
validateDelegationPolicy,
|
|
9
|
+
} from './delegation-policy-validation';
|
|
10
|
+
|
|
11
|
+
function clonePolicy(policy: any): DelegationPolicy {
|
|
12
|
+
return JSON.parse(JSON.stringify(policy));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('validateDelegationPolicy', () => {
|
|
16
|
+
it('accepts the travel-agent fixture', () => {
|
|
17
|
+
expect(() => validateDelegationPolicy(delegationPolicyTravelAgent)).not.toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('accepts the pharmacy fixture', () => {
|
|
21
|
+
expect(() => validateDelegationPolicy(delegationPolicyPharmacy)).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('accepts a policy without a top-level `name`', () => {
|
|
25
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
26
|
+
delete (policy as any).name;
|
|
27
|
+
expect(() => validateDelegationPolicy(policy)).not.toThrow();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects a non-object', () => {
|
|
31
|
+
expect(() => validateDelegationPolicy(null)).toThrow(/must be an object/);
|
|
32
|
+
expect(() => validateDelegationPolicy('foo')).toThrow(/must be an object/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('rejects a wrong type field', () => {
|
|
36
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
37
|
+
(policy as any).type = 'NotAPolicy';
|
|
38
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/type must be/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects an unsupported delegationTarget', () => {
|
|
42
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
43
|
+
policy.ruleset.delegationTarget = 'multi-credential' as any;
|
|
44
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/delegationTarget/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('rejects maxDelegationDepth out of range', () => {
|
|
48
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
49
|
+
policy.ruleset.overallConstraints.maxDelegationDepth = 99;
|
|
50
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/maxDelegationDepth/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('rejects an invalid lifetime unit', () => {
|
|
54
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
55
|
+
policy.ruleset.overallConstraints.delegatedCredentialLifetime.unit = 'weeks';
|
|
56
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/unit/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('rejects a non-positive lifetime value', () => {
|
|
60
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
61
|
+
policy.ruleset.overallConstraints.delegatedCredentialLifetime.value = 0;
|
|
62
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/value/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('rejects duplicate capability names', () => {
|
|
66
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
67
|
+
policy.ruleset.capabilities.push({...policy.ruleset.capabilities[0]});
|
|
68
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/duplicate capability/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('rejects an unsupported capability schema type', () => {
|
|
72
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
73
|
+
(policy.ruleset.capabilities[0].schema as any).type = 'object';
|
|
74
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/schema\.type/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('rejects duplicate roleIds', () => {
|
|
78
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
79
|
+
policy.ruleset.roles[1].roleId = policy.ruleset.roles[0].roleId;
|
|
80
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/duplicate roleId/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('rejects an orphan parentRoleId', () => {
|
|
84
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
85
|
+
policy.ruleset.roles[1].parentRoleId = 'does-not-exist';
|
|
86
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/unknown parentRoleId/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects more than one root role', () => {
|
|
90
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
91
|
+
policy.ruleset.roles[1].parentRoleId = null;
|
|
92
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/exactly one root role/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('rejects a grant referencing an unknown capability', () => {
|
|
96
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
97
|
+
policy.ruleset.roles[0].capabilityGrants[0].capability = 'Phantom';
|
|
98
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/unknown capability/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('rejects a grant whose schema.type does not match the capability', () => {
|
|
102
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
103
|
+
(policy.ruleset.roles[0].capabilityGrants[0].schema as any).type = 'integer';
|
|
104
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/schema\.type/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects a child role granting a capability the ancestor does not', () => {
|
|
108
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
109
|
+
policy.ruleset.roles[0].capabilityGrants = policy.ruleset.roles[0].capabilityGrants.filter(
|
|
110
|
+
g => g.capability !== 'Reserve Hotels',
|
|
111
|
+
);
|
|
112
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(
|
|
113
|
+
/grants "Reserve Hotels" but ancestor/,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('rejects a child integer maximum exceeding the ancestor maximum', () => {
|
|
118
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
119
|
+
const hotel = policy.ruleset.roles.find(r => r.label === 'Hotel Sub-agent')!;
|
|
120
|
+
const purchase = hotel.capabilityGrants.find(g => g.capability === 'Purchase')!;
|
|
121
|
+
(purchase.schema as any).maximum = 200;
|
|
122
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/maximum exceeds parent/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('rejects a child array enum that is not a subset of the ancestor enum', () => {
|
|
126
|
+
const policy = clonePolicy(delegationPolicyPharmacy);
|
|
127
|
+
const pharmacy = policy.ruleset.roles.find(r => r.label === 'Pharmacy')!;
|
|
128
|
+
const claims = pharmacy.capabilityGrants.find(g => g.capability === 'Allowed Claims')!;
|
|
129
|
+
(claims.schema as any).items.enum = ['Refund'];
|
|
130
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/items\.enum is not a subset/);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('rejects child explicit attributes when not a subset of an ancestor explicit list', () => {
|
|
134
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
135
|
+
policy.ruleset.roles[0].attributes = ['subject.firstName'];
|
|
136
|
+
const child = policy.ruleset.roles.find(r => r.label === 'Corporate Account Manager')!;
|
|
137
|
+
child.attributes = ['subject.lastName'];
|
|
138
|
+
expect(() => validateDelegationPolicy(policy)).toThrow(/attributes are not a subset/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('allows a child wildcard under a narrowed ancestor', () => {
|
|
142
|
+
const policy = clonePolicy(delegationPolicyTravelAgent);
|
|
143
|
+
policy.ruleset.roles[0].attributes = ['subject.firstName'];
|
|
144
|
+
expect(() => validateDelegationPolicy(policy)).not.toThrow();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('assertPolicyConformsToParent', () => {
|
|
149
|
+
const PARENT_ROLE = 'e79c0d16-8739-4e54-94d7-53d9f1c97c71';
|
|
150
|
+
const baseOpts = {delegationRole: PARENT_ROLE, remainingDepth: 3};
|
|
151
|
+
|
|
152
|
+
it('accepts a policy identical to the parent', () => {
|
|
153
|
+
expect(() =>
|
|
154
|
+
assertPolicyConformsToParent(
|
|
155
|
+
clonePolicy(delegationPolicyTravelAgent),
|
|
156
|
+
delegationPolicyTravelAgent,
|
|
157
|
+
baseOpts,
|
|
158
|
+
),
|
|
159
|
+
).not.toThrow();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('rejects when remaining delegation depth is zero', () => {
|
|
163
|
+
expect(() =>
|
|
164
|
+
assertPolicyConformsToParent(
|
|
165
|
+
clonePolicy(delegationPolicyTravelAgent),
|
|
166
|
+
delegationPolicyTravelAgent,
|
|
167
|
+
{delegationRole: PARENT_ROLE, remainingDepth: 0},
|
|
168
|
+
),
|
|
169
|
+
).toThrow(/no remaining delegation depth/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('rejects when child maxDelegationDepth exceeds parent', () => {
|
|
173
|
+
const child = clonePolicy(delegationPolicyTravelAgent);
|
|
174
|
+
child.ruleset.overallConstraints.maxDelegationDepth = 9;
|
|
175
|
+
const parent = clonePolicy(delegationPolicyTravelAgent);
|
|
176
|
+
parent.ruleset.overallConstraints.maxDelegationDepth = 2;
|
|
177
|
+
expect(() => assertPolicyConformsToParent(child, parent, baseOpts)).toThrow(
|
|
178
|
+
/maxDelegationDepth/,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('rejects when child lifetime exceeds parent lifetime (cross-unit)', () => {
|
|
183
|
+
const child = clonePolicy(delegationPolicyTravelAgent);
|
|
184
|
+
child.ruleset.overallConstraints.delegatedCredentialLifetime = {value: 400, unit: 'days'};
|
|
185
|
+
const parent = clonePolicy(delegationPolicyTravelAgent);
|
|
186
|
+
parent.ruleset.overallConstraints.delegatedCredentialLifetime = {value: 1, unit: 'years'};
|
|
187
|
+
expect(() => assertPolicyConformsToParent(child, parent, baseOpts)).toThrow(
|
|
188
|
+
/delegatedCredentialLifetime/,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('rejects when the delegationRole is missing from the policy', () => {
|
|
193
|
+
expect(() =>
|
|
194
|
+
assertPolicyConformsToParent(
|
|
195
|
+
clonePolicy(delegationPolicyTravelAgent),
|
|
196
|
+
delegationPolicyTravelAgent,
|
|
197
|
+
{delegationRole: 'missing-role', remainingDepth: 3},
|
|
198
|
+
),
|
|
199
|
+
).toThrow(/not found in delegationPolicy/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('rejects when the child grants a capability the parent does not', () => {
|
|
203
|
+
const child = clonePolicy(delegationPolicyTravelAgent);
|
|
204
|
+
const childRoot = child.ruleset.roles.find(r => r.roleId === PARENT_ROLE)!;
|
|
205
|
+
childRoot.capabilityGrants.push({
|
|
206
|
+
capability: 'Phantom',
|
|
207
|
+
schema: {type: 'boolean', const: true},
|
|
208
|
+
} as any);
|
|
209
|
+
expect(() =>
|
|
210
|
+
assertPolicyConformsToParent(child, delegationPolicyTravelAgent, baseOpts),
|
|
211
|
+
).toThrow(/parent does not/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('rejects when child integer maximum exceeds parent', () => {
|
|
215
|
+
const child = clonePolicy(delegationPolicyTravelAgent);
|
|
216
|
+
const childRoot = child.ruleset.roles.find(r => r.roleId === PARENT_ROLE)!;
|
|
217
|
+
const purchase = childRoot.capabilityGrants.find(g => g.capability === 'Purchase')!;
|
|
218
|
+
(purchase.schema as any).maximum = 999;
|
|
219
|
+
expect(() =>
|
|
220
|
+
assertPolicyConformsToParent(child, delegationPolicyTravelAgent, baseOpts),
|
|
221
|
+
).toThrow(/maximum exceeds parent/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('rejects when child enum is not a subset of parent enum', () => {
|
|
225
|
+
const PHARMACY_ROOT = '6ed167b3-90be-4f9a-a8d2-542d2f212d79';
|
|
226
|
+
const child = clonePolicy(delegationPolicyPharmacy);
|
|
227
|
+
const childRoot = child.ruleset.roles.find(r => r.roleId === PHARMACY_ROOT)!;
|
|
228
|
+
const claims = childRoot.capabilityGrants.find(g => g.capability === 'Allowed Claims')!;
|
|
229
|
+
(claims.schema as any).items.enum = ['Refund'];
|
|
230
|
+
expect(() =>
|
|
231
|
+
assertPolicyConformsToParent(child, delegationPolicyPharmacy as DelegationPolicy, {
|
|
232
|
+
delegationRole: PHARMACY_ROOT,
|
|
233
|
+
remainingDepth: 3,
|
|
234
|
+
}),
|
|
235
|
+
).toThrow(/items\.enum is not a subset/);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import {
|
|
3
|
+
CapabilityGrant,
|
|
4
|
+
DelegationPolicy,
|
|
5
|
+
Role,
|
|
6
|
+
} from './delegation-types';
|
|
7
|
+
|
|
8
|
+
export const MAX_DELEGATION_DEPTH = 9;
|
|
9
|
+
export const ALLOWED_LIFETIME_UNITS = ['days', 'months', 'years'] as const;
|
|
10
|
+
export const ALLOWED_GRANT_TYPES = ['boolean', 'array', 'integer'] as const;
|
|
11
|
+
export const ALLOWED_DELEGATION_TARGETS = ['single-credential'] as const;
|
|
12
|
+
|
|
13
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function lifetimeToDays(lifetime: {value: number; unit: string}): number {
|
|
18
|
+
const numeric = Number(lifetime?.value);
|
|
19
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
20
|
+
if (lifetime.unit === 'months') return numeric * 30;
|
|
21
|
+
if (lifetime.unit === 'years') return numeric * 365;
|
|
22
|
+
return numeric;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getGrantsByCapability(role: Role): Record<string, CapabilityGrant> {
|
|
26
|
+
const out: Record<string, CapabilityGrant> = {};
|
|
27
|
+
for (const grant of role.capabilityGrants || []) {
|
|
28
|
+
out[grant.capability] = grant;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getAncestorChain(role: Role, rolesById: Record<string, Role>): Role[] {
|
|
34
|
+
const chain: Role[] = [];
|
|
35
|
+
let cursor = role.parentRoleId ? rolesById[role.parentRoleId] : null;
|
|
36
|
+
const visited = new Set<string>();
|
|
37
|
+
while (cursor && !visited.has(cursor.roleId)) {
|
|
38
|
+
visited.add(cursor.roleId);
|
|
39
|
+
chain.push(cursor);
|
|
40
|
+
cursor = cursor.parentRoleId ? rolesById[cursor.parentRoleId] : null;
|
|
41
|
+
}
|
|
42
|
+
return chain;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isSubset<T>(child: T[], parent: T[]): boolean {
|
|
46
|
+
const set = new Set(parent);
|
|
47
|
+
return child.every(v => set.has(v));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getEnum(schema: any): string[] | null {
|
|
51
|
+
if (schema?.type === 'array' && Array.isArray(schema?.items?.enum)) {
|
|
52
|
+
return schema.items.enum;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isWildcardAttrs(attrs: string[]): boolean {
|
|
58
|
+
return attrs.length === 1 && attrs[0] === '*';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertGrantNarrows(
|
|
62
|
+
childSchema: any,
|
|
63
|
+
parentSchema: any,
|
|
64
|
+
describe: (suffix: string) => string,
|
|
65
|
+
) {
|
|
66
|
+
if (childSchema.type === 'integer' && parentSchema.maximum !== undefined) {
|
|
67
|
+
assert(
|
|
68
|
+
childSchema.maximum !== undefined && childSchema.maximum <= parentSchema.maximum,
|
|
69
|
+
describe(`maximum exceeds parent ${parentSchema.maximum}`),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (childSchema.type === 'array') {
|
|
74
|
+
const parentEnum = getEnum(parentSchema);
|
|
75
|
+
if (parentEnum) {
|
|
76
|
+
const childEnum = getEnum(childSchema);
|
|
77
|
+
assert(
|
|
78
|
+
childEnum && isSubset(childEnum, parentEnum),
|
|
79
|
+
describe('items.enum is not a subset of parent'),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate the structural integrity of a delegation policy.
|
|
87
|
+
*
|
|
88
|
+
* Throws on the first failure. Only checks invariants that downstream code
|
|
89
|
+
* relies on or that have security implications — fields like display labels
|
|
90
|
+
* and metadata strings are left to the type contract.
|
|
91
|
+
*/
|
|
92
|
+
export function validateDelegationPolicy(policy: any): asserts policy is DelegationPolicy {
|
|
93
|
+
assert(isPlainObject(policy), 'delegationPolicy must be an object');
|
|
94
|
+
assert(
|
|
95
|
+
policy.type === 'DelegationPolicy',
|
|
96
|
+
`delegationPolicy.type must be 'DelegationPolicy'`,
|
|
97
|
+
);
|
|
98
|
+
assert(
|
|
99
|
+
policy.name === undefined || typeof policy.name === 'string',
|
|
100
|
+
'delegationPolicy.name must be a string when present',
|
|
101
|
+
);
|
|
102
|
+
assert(isPlainObject(policy.ruleset), 'delegationPolicy.ruleset must be an object');
|
|
103
|
+
|
|
104
|
+
const ruleset = policy.ruleset;
|
|
105
|
+
assert(
|
|
106
|
+
Array.isArray(ruleset.roles) && ruleset.roles.length > 0,
|
|
107
|
+
'delegationPolicy.ruleset.roles must be a non-empty array',
|
|
108
|
+
);
|
|
109
|
+
assert(
|
|
110
|
+
Array.isArray(ruleset.capabilities),
|
|
111
|
+
'delegationPolicy.ruleset.capabilities must be an array',
|
|
112
|
+
);
|
|
113
|
+
assert(
|
|
114
|
+
(ALLOWED_DELEGATION_TARGETS as readonly string[]).includes(ruleset.delegationTarget as string),
|
|
115
|
+
`delegationPolicy.ruleset.delegationTarget must be one of: ${ALLOWED_DELEGATION_TARGETS.join(', ')}`,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
assert(isPlainObject(ruleset.overallConstraints), 'overallConstraints must be an object');
|
|
119
|
+
const constraints: any = ruleset.overallConstraints;
|
|
120
|
+
assert(
|
|
121
|
+
Number.isInteger(constraints.maxDelegationDepth) &&
|
|
122
|
+
constraints.maxDelegationDepth >= 0 &&
|
|
123
|
+
constraints.maxDelegationDepth <= MAX_DELEGATION_DEPTH,
|
|
124
|
+
`maxDelegationDepth must be an integer in [0, ${MAX_DELEGATION_DEPTH}]`,
|
|
125
|
+
);
|
|
126
|
+
assert(
|
|
127
|
+
isPlainObject(constraints.delegatedCredentialLifetime),
|
|
128
|
+
'delegatedCredentialLifetime must be an object',
|
|
129
|
+
);
|
|
130
|
+
const lifetime: any = constraints.delegatedCredentialLifetime;
|
|
131
|
+
assert(
|
|
132
|
+
Number.isInteger(lifetime.value) && lifetime.value > 0,
|
|
133
|
+
'delegatedCredentialLifetime.value must be a positive integer',
|
|
134
|
+
);
|
|
135
|
+
assert(
|
|
136
|
+
(ALLOWED_LIFETIME_UNITS as readonly string[]).includes(lifetime.unit),
|
|
137
|
+
`delegatedCredentialLifetime.unit must be one of: ${ALLOWED_LIFETIME_UNITS.join(', ')}`,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const capabilitiesByName: Record<string, any> = {};
|
|
141
|
+
for (const cap of ruleset.capabilities) {
|
|
142
|
+
assert(
|
|
143
|
+
!capabilitiesByName[cap.name],
|
|
144
|
+
`duplicate capability name: ${cap.name}`,
|
|
145
|
+
);
|
|
146
|
+
assert(
|
|
147
|
+
(ALLOWED_GRANT_TYPES as readonly string[]).includes(cap.schema?.type),
|
|
148
|
+
`capability "${cap.name}" schema.type must be one of: ${ALLOWED_GRANT_TYPES.join(', ')}`,
|
|
149
|
+
);
|
|
150
|
+
capabilitiesByName[cap.name] = cap;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rolesById: Record<string, Role> = {};
|
|
154
|
+
let rootCount = 0;
|
|
155
|
+
for (const role of ruleset.roles) {
|
|
156
|
+
assert(!rolesById[role.roleId], `duplicate roleId: ${role.roleId}`);
|
|
157
|
+
rolesById[role.roleId] = role;
|
|
158
|
+
if (role.parentRoleId === null) rootCount++;
|
|
159
|
+
}
|
|
160
|
+
assert(rootCount === 1, `ruleset must have exactly one root role, found ${rootCount}`);
|
|
161
|
+
|
|
162
|
+
for (const role of ruleset.roles) {
|
|
163
|
+
if (role.parentRoleId !== null) {
|
|
164
|
+
assert(
|
|
165
|
+
rolesById[role.parentRoleId],
|
|
166
|
+
`role ${role.roleId} references unknown parentRoleId ${role.parentRoleId}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const role of ruleset.roles) {
|
|
172
|
+
for (const grant of role.capabilityGrants) {
|
|
173
|
+
const cap = capabilitiesByName[grant.capability];
|
|
174
|
+
assert(
|
|
175
|
+
cap,
|
|
176
|
+
`role ${role.roleId} grants unknown capability "${grant.capability}"`,
|
|
177
|
+
);
|
|
178
|
+
assert(
|
|
179
|
+
(grant.schema as any)?.type === cap.schema.type,
|
|
180
|
+
`role ${role.roleId} grant "${grant.capability}" schema.type must be "${cap.schema.type}"`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const role of ruleset.roles) {
|
|
186
|
+
const ancestors = getAncestorChain(role, rolesById);
|
|
187
|
+
if (ancestors.length === 0) continue;
|
|
188
|
+
|
|
189
|
+
const childGrants = getGrantsByCapability(role);
|
|
190
|
+
|
|
191
|
+
for (const ancestor of ancestors) {
|
|
192
|
+
const ancestorGrants = getGrantsByCapability(ancestor);
|
|
193
|
+
|
|
194
|
+
for (const capName of Object.keys(childGrants)) {
|
|
195
|
+
assert(
|
|
196
|
+
ancestorGrants[capName],
|
|
197
|
+
`role ${role.roleId} grants "${capName}" but ancestor ${ancestor.roleId} does not`,
|
|
198
|
+
);
|
|
199
|
+
assertGrantNarrows(
|
|
200
|
+
childGrants[capName].schema,
|
|
201
|
+
ancestorGrants[capName].schema,
|
|
202
|
+
suffix => `role ${role.roleId} grant "${capName}" ${suffix}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!isWildcardAttrs(ancestor.attributes) && !isWildcardAttrs(role.attributes)) {
|
|
207
|
+
assert(
|
|
208
|
+
isSubset(role.attributes, ancestor.attributes),
|
|
209
|
+
`role ${role.roleId} attributes are not a subset of ancestor ${ancestor.roleId} attributes`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Assert that a delegation policy is a valid narrowing of a parent policy.
|
|
218
|
+
*
|
|
219
|
+
* Run after `validateDelegationPolicy(policy)` succeeds. The parent policy is
|
|
220
|
+
* trusted to already be valid (it came from a credential we issued or accepted).
|
|
221
|
+
*/
|
|
222
|
+
export function assertPolicyConformsToParent(
|
|
223
|
+
policy: DelegationPolicy,
|
|
224
|
+
parentPolicy: DelegationPolicy,
|
|
225
|
+
{
|
|
226
|
+
delegationRole,
|
|
227
|
+
remainingDepth,
|
|
228
|
+
}: {delegationRole: string; remainingDepth: number},
|
|
229
|
+
) {
|
|
230
|
+
assert(remainingDepth > 0, 'parent credential has no remaining delegation depth');
|
|
231
|
+
|
|
232
|
+
const childConstraints = policy.ruleset.overallConstraints;
|
|
233
|
+
const parentConstraints = parentPolicy.ruleset.overallConstraints;
|
|
234
|
+
|
|
235
|
+
assert(
|
|
236
|
+
childConstraints.maxDelegationDepth <= parentConstraints.maxDelegationDepth,
|
|
237
|
+
`maxDelegationDepth ${childConstraints.maxDelegationDepth} exceeds parent ${parentConstraints.maxDelegationDepth}`,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const parentDays = lifetimeToDays(parentConstraints.delegatedCredentialLifetime);
|
|
241
|
+
const childDays = lifetimeToDays(childConstraints.delegatedCredentialLifetime);
|
|
242
|
+
if (parentDays > 0) {
|
|
243
|
+
assert(
|
|
244
|
+
childDays <= parentDays,
|
|
245
|
+
`delegatedCredentialLifetime exceeds parent (${parentConstraints.delegatedCredentialLifetime.value} ${parentConstraints.delegatedCredentialLifetime.unit})`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const childRole = policy.ruleset.roles.find(r => r.roleId === delegationRole);
|
|
250
|
+
assert(
|
|
251
|
+
childRole,
|
|
252
|
+
`delegationRole "${delegationRole}" not found in delegationPolicy.ruleset.roles`,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const parentRole = parentPolicy.ruleset.roles.find(r => r.roleId === delegationRole);
|
|
256
|
+
assert(
|
|
257
|
+
parentRole,
|
|
258
|
+
`delegationRole "${delegationRole}" not found in parent credential's policy`,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (!isWildcardAttrs(parentRole.attributes) && !isWildcardAttrs(childRole.attributes)) {
|
|
262
|
+
assert(
|
|
263
|
+
isSubset(childRole.attributes, parentRole.attributes),
|
|
264
|
+
`delegationRole "${delegationRole}" attributes are not a subset of parent's attributes`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const parentGrants = getGrantsByCapability(parentRole);
|
|
269
|
+
for (const grant of childRole.capabilityGrants) {
|
|
270
|
+
const parentGrant = parentGrants[grant.capability];
|
|
271
|
+
assert(
|
|
272
|
+
parentGrant,
|
|
273
|
+
`delegationRole "${delegationRole}" grants "${grant.capability}" which the parent does not`,
|
|
274
|
+
);
|
|
275
|
+
assertGrantNarrows(
|
|
276
|
+
grant.schema,
|
|
277
|
+
parentGrant.schema,
|
|
278
|
+
suffix => `delegationRole "${delegationRole}" grant "${grant.capability}" ${suffix}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import {
|
|
3
|
+
DelegationDetails,
|
|
4
|
+
DelegationPolicy,
|
|
5
|
+
Role,
|
|
6
|
+
RoleNode,
|
|
7
|
+
} from './delegation-types';
|
|
8
|
+
import {delegationService} from '@docknetwork/wallet-sdk-wasm/src/services/delegation';
|
|
9
|
+
import {getDelegationChain} from './delegation-chain';
|
|
10
|
+
import {
|
|
11
|
+
buildDelegationRoleTree,
|
|
12
|
+
getRemainingDelegationDepth,
|
|
13
|
+
getRoleNodeById,
|
|
14
|
+
} from './delegation-tree';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch the delegation policy for a given credential
|
|
18
|
+
* @param credential - The credential containing the delegation policy ID
|
|
19
|
+
* @returns A promise that resolves to the delegation policy for the given schema ID
|
|
20
|
+
*/
|
|
21
|
+
export async function fetchDelegationPolicyJson(
|
|
22
|
+
credential,
|
|
23
|
+
): Promise<DelegationPolicy> {
|
|
24
|
+
assert(
|
|
25
|
+
credential.delegationPolicyId,
|
|
26
|
+
'Credential does not contain a delegation policy ID',
|
|
27
|
+
);
|
|
28
|
+
return delegationService.fetchDelegationPolicyJson(
|
|
29
|
+
credential.delegationPolicyId,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build delegation policy attributes
|
|
35
|
+
* This is needed when issuing a delegatable credential, so that we can include the delegation policy ID and digest in the credential attributes
|
|
36
|
+
* @param delegationPolicy
|
|
37
|
+
* @returns An object containing the delegation policy ID and digest to be included in the credential attributes
|
|
38
|
+
*/
|
|
39
|
+
export async function buildDelegationPolicyAttributes(
|
|
40
|
+
delegationPolicy: DelegationPolicy | DelegationPolicy[],
|
|
41
|
+
) {
|
|
42
|
+
return {
|
|
43
|
+
delegationPolicyId: `data:application/json,${encodeURIComponent(
|
|
44
|
+
JSON.stringify(delegationPolicy),
|
|
45
|
+
)}`,
|
|
46
|
+
delegationPolicyDigest: await delegationService.computePolicyDigestHex(
|
|
47
|
+
delegationPolicy,
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getRole(
|
|
53
|
+
roleId: string,
|
|
54
|
+
delegationPolicy: DelegationPolicy,
|
|
55
|
+
): Role {
|
|
56
|
+
if (!delegationPolicy) return null;
|
|
57
|
+
|
|
58
|
+
return delegationPolicy.ruleset.roles.find(r => r.roleId === roleId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getDelegationOptions(roleTree: RoleNode): RoleNode[] {
|
|
62
|
+
const roles: RoleNode[] = [];
|
|
63
|
+
const traverse = (node: RoleNode) => {
|
|
64
|
+
roles.push(node);
|
|
65
|
+
node.children?.forEach(traverse);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
roleTree.children?.forEach(traverse);
|
|
69
|
+
|
|
70
|
+
return roles;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function getDelegationDetails(
|
|
74
|
+
credential,
|
|
75
|
+
wallet,
|
|
76
|
+
): Promise<DelegationDetails> {
|
|
77
|
+
const delegationChain = await getDelegationChain(credential, wallet);
|
|
78
|
+
const policy = await fetchDelegationPolicyJson(credential);
|
|
79
|
+
const roleTree = buildDelegationRoleTree(policy);
|
|
80
|
+
const role = getRoleNodeById(roleTree.roleId, roleTree);
|
|
81
|
+
const delegationOptions = getDelegationOptions(roleTree);
|
|
82
|
+
const remainingDelegationDepth = getRemainingDelegationDepth(role, policy);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
delegationPolicy: policy,
|
|
86
|
+
roleTree,
|
|
87
|
+
role,
|
|
88
|
+
remainingDelegationDepth,
|
|
89
|
+
delegationChain,
|
|
90
|
+
delegatedBy: {
|
|
91
|
+
role:
|
|
92
|
+
delegationChain?.length > 0
|
|
93
|
+
? getRole(delegationChain[0]?.roleId, policy)
|
|
94
|
+
: null,
|
|
95
|
+
issuerName: credential?.issuer?.name,
|
|
96
|
+
issuerDid: credential?.issuer?.id,
|
|
97
|
+
},
|
|
98
|
+
delegationOptions,
|
|
99
|
+
};
|
|
100
|
+
}
|