iso-ucan 1.0.0 → 2.0.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/dist/src/delegation.d.ts.map +1 -1
- package/dist/src/envelope.d.ts +40 -9
- package/dist/src/envelope.d.ts.map +1 -1
- package/dist/src/invocation.d.ts +1 -2
- package/dist/src/invocation.d.ts.map +1 -1
- package/dist/src/policy.d.ts +114 -0
- package/dist/src/policy.d.ts.map +1 -1
- package/dist/src/types/envelope.d.ts +4 -4
- package/dist/src/types/envelope.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +6 -2
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types.d.ts +9 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils.d.ts +30 -2
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/varsig.d.ts +0 -1
- package/dist/src/varsig.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/delegation.js +7 -1
- package/src/envelope.js +113 -36
- package/src/invocation.js +39 -10
- package/src/policy.js +597 -195
- package/src/types/envelope.ts +4 -4
- package/src/types/policy.ts +5 -2
- package/src/types.ts +9 -1
- package/src/utils.js +64 -10
- package/src/varsig.js +30 -19
package/src/policy.js
CHANGED
|
@@ -1,272 +1,674 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* UCAN policy language.
|
|
3
3
|
*
|
|
4
|
-
* @
|
|
5
|
-
* @param {string} selector The selector string (e.g., ".foo[0].bar").
|
|
6
|
-
* @returns {any} The selected value, or null if the selection fails at an optional part.
|
|
7
|
-
* @throws {Error} If the selection fails at a non-optional part.
|
|
4
|
+
* @see https://github.com/ucan-wg/delegation#policy
|
|
8
5
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} SegmentBase
|
|
9
|
+
* @property {boolean} optional - Whether the segment ends with `?`: when it fails to resolve, the whole selector returns `null`.
|
|
10
|
+
* @property {string} source - The exact text of the selector this segment was parsed from, including a leading `.` and trailing `?`s.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {{ kind: 'identity' }
|
|
13
|
+
* | { kind: 'field', key: string }
|
|
14
|
+
* | { kind: 'index', index: number }
|
|
15
|
+
* | { kind: 'slice', start: number | undefined, end: number | undefined }
|
|
16
|
+
* | { kind: 'iterate' }
|
|
17
|
+
* } SegmentData
|
|
18
|
+
*
|
|
19
|
+
* @typedef {SegmentData & SegmentBase} SelectorSegment
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const IDENT_START = /[A-Za-z_]/
|
|
23
|
+
const IDENT_CHAR = /[A-Za-z0-9_]/
|
|
24
|
+
const INDEX = /^-?\d+$/
|
|
25
|
+
const SLICE = /^(-?\d+)?:(-?\d+)?$/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} selector
|
|
29
|
+
* @param {number} pos
|
|
30
|
+
* @param {string} reason
|
|
31
|
+
*/
|
|
32
|
+
function selectorError(selector, pos, reason) {
|
|
33
|
+
return new TypeError(
|
|
34
|
+
`Invalid selector ${JSON.stringify(selector)} at position ${pos}: ${reason}`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} selector
|
|
40
|
+
* @param {number} pos
|
|
41
|
+
* @param {string} text
|
|
42
|
+
*/
|
|
43
|
+
function parseInteger(selector, pos, text) {
|
|
44
|
+
const n = Number(text)
|
|
45
|
+
if (!Number.isSafeInteger(n)) {
|
|
46
|
+
throw selectorError(selector, pos, `index ${text} is out of range`)
|
|
12
47
|
}
|
|
48
|
+
return n
|
|
49
|
+
}
|
|
13
50
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Parse a bracket segment (`["key"]`, `[]`, `[n]`, `[a:b]`) starting at `start`
|
|
53
|
+
* (which must point at `[`).
|
|
54
|
+
*
|
|
55
|
+
* @param {string} selector
|
|
56
|
+
* @param {number} start
|
|
57
|
+
* @returns {{ segment: SegmentData, end: number }}
|
|
58
|
+
*/
|
|
59
|
+
function parseBracket(selector, start) {
|
|
60
|
+
let pos = start + 1
|
|
17
61
|
|
|
18
|
-
|
|
62
|
+
// Unambiguous field name: ["any string"] with JSON string escapes.
|
|
63
|
+
if (selector[pos] === '"') {
|
|
64
|
+
let end = pos + 1
|
|
65
|
+
while (end < selector.length && selector[end] !== '"') {
|
|
66
|
+
end += selector[end] === '\\' ? 2 : 1
|
|
67
|
+
}
|
|
68
|
+
if (end >= selector.length) {
|
|
69
|
+
throw selectorError(selector, pos, 'unterminated string')
|
|
70
|
+
}
|
|
71
|
+
/** @type {unknown} */
|
|
72
|
+
let key
|
|
73
|
+
try {
|
|
74
|
+
key = JSON.parse(selector.slice(pos, end + 1))
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw selectorError(
|
|
77
|
+
selector,
|
|
78
|
+
pos,
|
|
79
|
+
`invalid string literal (${/** @type {Error} */ (error).message})`
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
pos = end + 1
|
|
83
|
+
if (selector[pos] !== ']') {
|
|
84
|
+
throw selectorError(selector, pos, 'expected "]" after string')
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
segment: { kind: 'field', key: /** @type {string} */ (key) },
|
|
88
|
+
end: pos + 1,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const close = selector.indexOf(']', pos)
|
|
93
|
+
if (close === -1) {
|
|
94
|
+
throw selectorError(selector, start, 'unterminated "["')
|
|
95
|
+
}
|
|
96
|
+
const inner = selector.slice(pos, close)
|
|
19
97
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
98
|
+
if (inner === '') {
|
|
99
|
+
return { segment: { kind: 'iterate' }, end: close + 1 }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (INDEX.test(inner)) {
|
|
103
|
+
return {
|
|
104
|
+
segment: { kind: 'index', index: parseInteger(selector, pos, inner) },
|
|
105
|
+
end: close + 1,
|
|
25
106
|
}
|
|
107
|
+
}
|
|
26
108
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
109
|
+
const slice = SLICE.exec(inner)
|
|
110
|
+
if (slice && (slice[1] !== undefined || slice[2] !== undefined)) {
|
|
111
|
+
return {
|
|
112
|
+
segment: {
|
|
113
|
+
kind: 'slice',
|
|
114
|
+
start:
|
|
115
|
+
slice[1] === undefined
|
|
116
|
+
? undefined
|
|
117
|
+
: parseInteger(selector, pos, slice[1]),
|
|
118
|
+
end:
|
|
119
|
+
slice[2] === undefined
|
|
120
|
+
? undefined
|
|
121
|
+
: parseInteger(selector, pos, slice[2]),
|
|
122
|
+
},
|
|
123
|
+
end: close + 1,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
33
126
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} else if (part === '[]') {
|
|
41
|
-
if (Array.isArray(currentData)) {
|
|
42
|
-
// No-op for arrays, the quantifier will iterate over it.
|
|
43
|
-
} else if (typeof currentData === 'object') {
|
|
44
|
-
currentData = Object.values(currentData)
|
|
45
|
-
} else {
|
|
46
|
-
throw new Error('Cannot extract values from non-collection.')
|
|
47
|
-
}
|
|
48
|
-
} else if (part.startsWith('[') && part.includes(':')) {
|
|
49
|
-
// Slice
|
|
50
|
-
if (!Array.isArray(currentData)) {
|
|
51
|
-
throw new Error('Slice can only be applied to arrays.')
|
|
52
|
-
}
|
|
53
|
-
const [start, end] = part
|
|
54
|
-
.substring(1, part.length - 1)
|
|
55
|
-
.split(':')
|
|
56
|
-
.map((s) => (s ? Number.parseInt(s, 10) : undefined))
|
|
57
|
-
currentData = currentData.slice(start, end)
|
|
58
|
-
} else if (part.startsWith('[')) {
|
|
59
|
-
// Index
|
|
60
|
-
if (!Array.isArray(currentData)) {
|
|
61
|
-
throw new Error('Index can only be applied to arrays.')
|
|
62
|
-
}
|
|
63
|
-
const index = Number.parseInt(part.substring(1, part.length - 1), 10)
|
|
64
|
-
currentData =
|
|
65
|
-
currentData[index < 0 ? currentData.length + index : index]
|
|
66
|
-
}
|
|
127
|
+
throw selectorError(
|
|
128
|
+
selector,
|
|
129
|
+
pos,
|
|
130
|
+
`invalid bracket expression "[${inner}]", expected [], [n], [a:b] or ["key"]`
|
|
131
|
+
)
|
|
132
|
+
}
|
|
67
133
|
|
|
68
|
-
|
|
69
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Parse a UCAN policy selector into segments.
|
|
136
|
+
*
|
|
137
|
+
* Consumes the whole string and throws a `TypeError` on anything that is not
|
|
138
|
+
* valid selector syntax. Supported forms:
|
|
139
|
+
*
|
|
140
|
+
* - `.` identity
|
|
141
|
+
* - `.name` dotted field name, where name matches `[A-Za-z_][A-Za-z0-9_]*`
|
|
142
|
+
* - `["any string"]` unambiguous field name, with JSON string escapes
|
|
143
|
+
* - `[]` collection values
|
|
144
|
+
* - `[n]`, `[-n]` list index
|
|
145
|
+
* - `[a:b]`, `[a:]`, `[:b]` list slice
|
|
146
|
+
* - a trailing `?` on any segment (repeated `?` collapse into one)
|
|
147
|
+
*
|
|
148
|
+
* A selector MUST start with `.` and MUST NOT contain `..`.
|
|
149
|
+
*
|
|
150
|
+
* @see https://github.com/ucan-wg/delegation#selectors
|
|
151
|
+
*
|
|
152
|
+
* @param {string} selector
|
|
153
|
+
* @returns {SelectorSegment[]}
|
|
154
|
+
*/
|
|
155
|
+
export function parseSelector(selector) {
|
|
156
|
+
if (typeof selector !== 'string') {
|
|
157
|
+
throw new TypeError(
|
|
158
|
+
`Invalid selector: expected a string, got ${typeof selector}`
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
if (selector[0] !== '.') {
|
|
162
|
+
throw selectorError(selector, 0, 'must start with "."')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @type {SelectorSegment[]} */
|
|
166
|
+
const segments = []
|
|
167
|
+
let pos = 0
|
|
168
|
+
|
|
169
|
+
while (pos < selector.length) {
|
|
170
|
+
const start = pos
|
|
171
|
+
const char = selector[pos]
|
|
172
|
+
/** @type {SegmentData} */
|
|
173
|
+
let segment
|
|
174
|
+
|
|
175
|
+
if (char === '.') {
|
|
176
|
+
const next = selector[pos + 1]
|
|
177
|
+
if (next === '.') {
|
|
178
|
+
throw selectorError(selector, pos, '".." is not allowed')
|
|
70
179
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
180
|
+
if (next !== undefined && IDENT_START.test(next)) {
|
|
181
|
+
pos += 2
|
|
182
|
+
while (pos < selector.length && IDENT_CHAR.test(selector[pos])) {
|
|
183
|
+
pos++
|
|
184
|
+
}
|
|
185
|
+
segment = { kind: 'field', key: selector.slice(start + 1, pos) }
|
|
186
|
+
} else if (next === '[') {
|
|
187
|
+
const parsed = parseBracket(selector, pos + 1)
|
|
188
|
+
segment = parsed.segment
|
|
189
|
+
pos = parsed.end
|
|
190
|
+
} else if (next === undefined || next === '?') {
|
|
191
|
+
segment = { kind: 'identity' }
|
|
192
|
+
pos++
|
|
193
|
+
} else {
|
|
194
|
+
throw selectorError(
|
|
195
|
+
selector,
|
|
196
|
+
pos + 1,
|
|
197
|
+
`unexpected character ${JSON.stringify(next)} after "."`
|
|
198
|
+
)
|
|
74
199
|
}
|
|
75
|
-
|
|
76
|
-
|
|
200
|
+
} else if (char === '[') {
|
|
201
|
+
const parsed = parseBracket(selector, pos)
|
|
202
|
+
segment = parsed.segment
|
|
203
|
+
pos = parsed.end
|
|
204
|
+
} else {
|
|
205
|
+
throw selectorError(
|
|
206
|
+
selector,
|
|
207
|
+
pos,
|
|
208
|
+
`unexpected character ${JSON.stringify(char)}`
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let optional = false
|
|
213
|
+
while (selector[pos] === '?') {
|
|
214
|
+
optional = true
|
|
215
|
+
pos++
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
segments.push(
|
|
219
|
+
/** @type {SelectorSegment} */ ({
|
|
220
|
+
...segment,
|
|
221
|
+
optional,
|
|
222
|
+
source: selector.slice(start, pos),
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return segments
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Returned by {@link resolve} when a selector can't be resolved.
|
|
232
|
+
*/
|
|
233
|
+
const FAIL = Symbol('fail')
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Plain IPLD map (a JS object that is not an array, bytes, CID or class instance).
|
|
237
|
+
*
|
|
238
|
+
* @param {unknown} value
|
|
239
|
+
* @returns {value is Record<string, unknown>}
|
|
240
|
+
*/
|
|
241
|
+
function isMap(value) {
|
|
242
|
+
if (value === null || typeof value !== 'object') return false
|
|
243
|
+
const proto = Object.getPrototypeOf(value)
|
|
244
|
+
return proto === Object.prototype || proto === null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {unknown} value
|
|
249
|
+
* @returns {value is unknown[] | Uint8Array}
|
|
250
|
+
*/
|
|
251
|
+
function isList(value) {
|
|
252
|
+
return Array.isArray(value) || value instanceof Uint8Array
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Resolve a single segment, or return {@link FAIL}.
|
|
257
|
+
*
|
|
258
|
+
* @param {unknown} value
|
|
259
|
+
* @param {SelectorSegment} segment
|
|
260
|
+
* @returns {unknown}
|
|
261
|
+
*/
|
|
262
|
+
function resolveSegment(value, segment) {
|
|
263
|
+
switch (segment.kind) {
|
|
264
|
+
case 'identity':
|
|
265
|
+
return value
|
|
266
|
+
case 'field':
|
|
267
|
+
if (!isMap(value)) return FAIL
|
|
268
|
+
// Missing keys resolve to null. Only own keys count, so `__proto__`,
|
|
269
|
+
// `constructor` and friends are missing unless they are in the data.
|
|
270
|
+
return Object.hasOwn(value, segment.key) ? value[segment.key] : null
|
|
271
|
+
case 'index': {
|
|
272
|
+
if (!isList(value)) return FAIL
|
|
273
|
+
const index =
|
|
274
|
+
segment.index < 0 ? value.length + segment.index : segment.index
|
|
275
|
+
if (index < 0 || index >= value.length) return FAIL
|
|
276
|
+
return value[index]
|
|
77
277
|
}
|
|
278
|
+
case 'slice':
|
|
279
|
+
if (!isList(value)) return FAIL
|
|
280
|
+
return value.slice(segment.start, segment.end)
|
|
281
|
+
case 'iterate':
|
|
282
|
+
if (isList(value)) return value
|
|
283
|
+
if (isMap(value)) return Object.values(value)
|
|
284
|
+
return FAIL
|
|
285
|
+
default:
|
|
286
|
+
return FAIL
|
|
78
287
|
}
|
|
288
|
+
}
|
|
79
289
|
|
|
80
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Resolve parsed selector segments against data.
|
|
292
|
+
*
|
|
293
|
+
* Segments are resolved left to right. When a segment fails, the selector
|
|
294
|
+
* returns `null` if that segment is optional, otherwise {@link FAIL}. Either
|
|
295
|
+
* way, resolution stops at the first failure.
|
|
296
|
+
*
|
|
297
|
+
* @param {unknown} data
|
|
298
|
+
* @param {SelectorSegment[]} segments
|
|
299
|
+
* @returns {unknown}
|
|
300
|
+
*/
|
|
301
|
+
function resolve(data, segments) {
|
|
302
|
+
let current = data
|
|
303
|
+
for (const segment of segments) {
|
|
304
|
+
const next = resolveSegment(current, segment)
|
|
305
|
+
if (next === FAIL) {
|
|
306
|
+
return segment.optional ? null : FAIL
|
|
307
|
+
}
|
|
308
|
+
current = next
|
|
309
|
+
}
|
|
310
|
+
return current
|
|
81
311
|
}
|
|
82
312
|
|
|
83
313
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* @param {
|
|
314
|
+
* Resolve a selector against data.
|
|
315
|
+
*
|
|
316
|
+
* @param {unknown} data - The data to select from.
|
|
317
|
+
* @param {string} selector - The selector, e.g. `.foo[0].bar`.
|
|
318
|
+
* @returns {{ ok: true, value: unknown } | { ok: false }} `ok: false` when the selector can't be resolved.
|
|
319
|
+
* @throws {TypeError} If the selector is not valid syntax.
|
|
320
|
+
*/
|
|
321
|
+
export function select(data, selector) {
|
|
322
|
+
const value = resolve(data, parseSelector(selector))
|
|
323
|
+
return value === FAIL ? { ok: false } : { ok: true, value }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Compare two numbers that may be `number` or `bigint`.
|
|
328
|
+
*
|
|
329
|
+
* @param {unknown} a
|
|
330
|
+
* @param {unknown} b
|
|
331
|
+
* @returns {-1 | 0 | 1 | undefined} `undefined` when either side is not a number, or is NaN.
|
|
332
|
+
*/
|
|
333
|
+
function compareNumbers(a, b) {
|
|
334
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
335
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return undefined
|
|
336
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
337
|
+
}
|
|
338
|
+
if (typeof a === 'bigint' && typeof b === 'bigint') {
|
|
339
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
340
|
+
}
|
|
341
|
+
if (typeof a === 'bigint' && typeof b === 'number') {
|
|
342
|
+
const r = compareNumbers(b, a)
|
|
343
|
+
return r === undefined ? undefined : /** @type {-1 | 0 | 1} */ (-r || 0)
|
|
344
|
+
}
|
|
345
|
+
if (typeof a === 'number' && typeof b === 'bigint') {
|
|
346
|
+
if (Number.isNaN(a)) return undefined
|
|
347
|
+
if (!Number.isFinite(a)) return a > 0 ? 1 : -1
|
|
348
|
+
if (Number.isInteger(a)) {
|
|
349
|
+
const ai = BigInt(a)
|
|
350
|
+
return ai < b ? -1 : ai > b ? 1 : 0
|
|
351
|
+
}
|
|
352
|
+
// a is not an integer, so it is never equal to b, and
|
|
353
|
+
// floor(a) < a < floor(a) + 1.
|
|
354
|
+
return BigInt(Math.floor(a)) < b ? -1 : 1
|
|
355
|
+
}
|
|
356
|
+
return undefined
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {Uint8Array} a
|
|
361
|
+
* @param {Uint8Array} b
|
|
362
|
+
*/
|
|
363
|
+
function bytesEqual(a, b) {
|
|
364
|
+
if (a.length !== b.length) return false
|
|
365
|
+
for (let i = 0; i < a.length; i++) {
|
|
366
|
+
if (a[i] !== b[i]) return false
|
|
367
|
+
}
|
|
368
|
+
return true
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Deep equality between IPLD values.
|
|
373
|
+
*
|
|
374
|
+
* `number` and `bigint` are compared by numeric value, so `1 == 1n`.
|
|
375
|
+
*
|
|
376
|
+
* @param {unknown} a
|
|
377
|
+
* @param {unknown} b
|
|
87
378
|
* @returns {boolean}
|
|
88
379
|
*/
|
|
89
380
|
function deepEqual(a, b) {
|
|
90
381
|
if (a === b) return true
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
382
|
+
|
|
383
|
+
const numeric = compareNumbers(a, b)
|
|
384
|
+
if (numeric !== undefined) return numeric === 0
|
|
385
|
+
|
|
386
|
+
if (a instanceof Uint8Array || b instanceof Uint8Array) {
|
|
387
|
+
return (
|
|
388
|
+
a instanceof Uint8Array && b instanceof Uint8Array && bytesEqual(a, b)
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
393
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false
|
|
394
|
+
if (a.length !== b.length) return false
|
|
395
|
+
for (let i = 0; i < a.length; i++) {
|
|
396
|
+
if (!deepEqual(a[i], b[i])) return false
|
|
99
397
|
}
|
|
398
|
+
return true
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (isMap(a) && isMap(b)) {
|
|
100
402
|
const keys = Object.keys(a)
|
|
101
403
|
if (keys.length !== Object.keys(b).length) return false
|
|
102
404
|
for (const key of keys) {
|
|
103
|
-
|
|
104
|
-
if (!b.hasOwnProperty(key) || !deepEqual(a[key], b[key])) return false
|
|
405
|
+
if (!Object.hasOwn(b, key) || !deepEqual(a[key], b[key])) return false
|
|
105
406
|
}
|
|
106
407
|
return true
|
|
107
408
|
}
|
|
409
|
+
|
|
410
|
+
// CIDs and other objects with an `equals` method (e.g. multiformats CID).
|
|
411
|
+
if (
|
|
412
|
+
a !== null &&
|
|
413
|
+
b !== null &&
|
|
414
|
+
typeof a === 'object' &&
|
|
415
|
+
typeof b === 'object' &&
|
|
416
|
+
'equals' in a &&
|
|
417
|
+
typeof a.equals === 'function' &&
|
|
418
|
+
Object.getPrototypeOf(a) === Object.getPrototypeOf(b)
|
|
419
|
+
) {
|
|
420
|
+
return Boolean(a.equals(b))
|
|
421
|
+
}
|
|
422
|
+
|
|
108
423
|
return false
|
|
109
424
|
}
|
|
110
425
|
|
|
111
426
|
/**
|
|
112
427
|
* Converts a UCAN glob pattern to a regular expression.
|
|
428
|
+
*
|
|
429
|
+
* `*` matches zero or more characters, including newlines. `\*` matches a
|
|
430
|
+
* literal `*`. The spec defines no other escapes, so a `\` that is not
|
|
431
|
+
* followed by `*` is a literal backslash. For example, the pattern `\\*` is a
|
|
432
|
+
* literal `\` followed by `\*`, and only matches the string `\*`.
|
|
433
|
+
*
|
|
434
|
+
* @see https://github.com/ucan-wg/delegation#glob-matching
|
|
435
|
+
*
|
|
113
436
|
* @param {string} pattern
|
|
114
437
|
* @returns {RegExp}
|
|
115
438
|
*/
|
|
116
|
-
function likeRegex(pattern) {
|
|
117
|
-
|
|
118
|
-
// The glob pattern has `*` as a wildcard (zero or more chars)
|
|
119
|
-
// and `\*` as an escape for a literal asterisk.
|
|
120
|
-
let regexString = '^'
|
|
439
|
+
export function likeRegex(pattern) {
|
|
440
|
+
let regex = '^'
|
|
121
441
|
let i = 0
|
|
122
442
|
while (i < pattern.length) {
|
|
123
443
|
const char = pattern[i]
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (i + 1 < pattern.length) {
|
|
128
|
-
const nextChar = pattern[i + 1]
|
|
129
|
-
if (nextChar === '*') {
|
|
130
|
-
// This is an escaped asterisk `\*`. Match a literal `*`.
|
|
131
|
-
regexString += '\\*'
|
|
132
|
-
i += 2 // Move past both `\` and `*`
|
|
133
|
-
} else {
|
|
134
|
-
// This is another escape, like `\\`. Treat the `\` as a literal character.
|
|
135
|
-
regexString += '\\\\'
|
|
136
|
-
i++
|
|
137
|
-
}
|
|
138
|
-
} else {
|
|
139
|
-
// Trailing backslash. Treat as a literal `\`.
|
|
140
|
-
regexString += '\\\\'
|
|
141
|
-
i++
|
|
142
|
-
}
|
|
444
|
+
if (char === '\\' && pattern[i + 1] === '*') {
|
|
445
|
+
regex += '\\*'
|
|
446
|
+
i += 2
|
|
143
447
|
} else if (char === '*') {
|
|
144
|
-
|
|
145
|
-
regexString += '.*'
|
|
448
|
+
regex += '[\\s\\S]*'
|
|
146
449
|
i++
|
|
147
450
|
} else {
|
|
148
|
-
|
|
149
|
-
regexString += char.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
451
|
+
regex += char.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
150
452
|
i++
|
|
151
453
|
}
|
|
152
454
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return new RegExp(regexString)
|
|
455
|
+
regex += '$'
|
|
456
|
+
return new RegExp(regex)
|
|
156
457
|
}
|
|
157
458
|
|
|
158
459
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
460
|
+
* @typedef {{ op: '==', selector: SelectorSegment[], value: unknown }
|
|
461
|
+
* | { op: '<' | '<=' | '>' | '>=', selector: SelectorSegment[], value: number | bigint }
|
|
462
|
+
* | { op: 'like', selector: SelectorSegment[], regex: RegExp }
|
|
463
|
+
* | { op: 'not', statement: CompiledStatement }
|
|
464
|
+
* | { op: 'and' | 'or', statements: CompiledStatement[] }
|
|
465
|
+
* | { op: 'all' | 'any', selector: SelectorSegment[], statement: CompiledStatement }
|
|
466
|
+
* } CompiledStatement
|
|
165
467
|
*/
|
|
166
|
-
function evaluateStatement(args, statement) {
|
|
167
|
-
const [op, ...params] = statement
|
|
168
468
|
|
|
469
|
+
/**
|
|
470
|
+
* @param {unknown} statement
|
|
471
|
+
* @param {string} reason
|
|
472
|
+
*/
|
|
473
|
+
function statementError(statement, reason) {
|
|
474
|
+
let text
|
|
169
475
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
476
|
+
text = JSON.stringify(statement, (_, v) =>
|
|
477
|
+
typeof v === 'bigint' ? `${v}n` : v
|
|
478
|
+
)
|
|
479
|
+
} catch {
|
|
480
|
+
text = String(statement)
|
|
481
|
+
}
|
|
482
|
+
return new TypeError(`Invalid policy statement ${text}: ${reason}`)
|
|
483
|
+
}
|
|
174
484
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
485
|
+
/**
|
|
486
|
+
* @param {unknown[]} statement
|
|
487
|
+
* @param {number} arity
|
|
488
|
+
*/
|
|
489
|
+
function assertArity(statement, arity) {
|
|
490
|
+
if (statement.length !== arity) {
|
|
491
|
+
throw statementError(
|
|
492
|
+
statement,
|
|
493
|
+
`"${statement[0]}" takes ${arity - 1} argument(s), got ${statement.length - 1}`
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Check the structure of a statement and parse its selectors.
|
|
500
|
+
*
|
|
501
|
+
* @param {unknown} statement
|
|
502
|
+
* @returns {CompiledStatement}
|
|
503
|
+
* @throws {TypeError} If the statement is malformed.
|
|
504
|
+
*/
|
|
505
|
+
function compileStatement(statement) {
|
|
506
|
+
if (!Array.isArray(statement)) {
|
|
507
|
+
throw statementError(statement, 'expected an array')
|
|
508
|
+
}
|
|
509
|
+
const op = statement[0]
|
|
510
|
+
switch (op) {
|
|
511
|
+
case '==':
|
|
512
|
+
case '!=': {
|
|
513
|
+
assertArity(statement, 3)
|
|
514
|
+
/** @type {CompiledStatement} */
|
|
515
|
+
const eq = {
|
|
516
|
+
op: '==',
|
|
517
|
+
selector: parseSelector(statement[1]),
|
|
518
|
+
value: statement[2],
|
|
180
519
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
520
|
+
// `!=` is defined as `["not", ["==", selector, value]]`.
|
|
521
|
+
return op === '==' ? eq : { op: 'not', statement: eq }
|
|
522
|
+
}
|
|
523
|
+
case '<':
|
|
524
|
+
case '<=':
|
|
525
|
+
case '>':
|
|
526
|
+
case '>=': {
|
|
527
|
+
assertArity(statement, 3)
|
|
528
|
+
const value = statement[2]
|
|
529
|
+
if (
|
|
530
|
+
typeof value !== 'bigint' &&
|
|
531
|
+
(typeof value !== 'number' || !Number.isFinite(value))
|
|
532
|
+
) {
|
|
533
|
+
throw statementError(statement, 'value must be a number')
|
|
194
534
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
535
|
+
return { op, selector: parseSelector(statement[1]), value }
|
|
536
|
+
}
|
|
537
|
+
case 'like': {
|
|
538
|
+
assertArity(statement, 3)
|
|
539
|
+
const pattern = statement[2]
|
|
540
|
+
if (typeof pattern !== 'string') {
|
|
541
|
+
throw statementError(statement, 'pattern must be a string')
|
|
202
542
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
208
|
-
return statements.every((stmt) => evaluateStatement(args, stmt))
|
|
543
|
+
return {
|
|
544
|
+
op,
|
|
545
|
+
selector: parseSelector(statement[1]),
|
|
546
|
+
regex: likeRegex(pattern),
|
|
209
547
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
548
|
+
}
|
|
549
|
+
case 'not':
|
|
550
|
+
assertArity(statement, 2)
|
|
551
|
+
return { op, statement: compileStatement(statement[1]) }
|
|
552
|
+
case 'and':
|
|
553
|
+
case 'or':
|
|
554
|
+
assertArity(statement, 2)
|
|
555
|
+
if (!Array.isArray(statement[1])) {
|
|
556
|
+
throw statementError(statement, 'expected an array of statements')
|
|
216
557
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
558
|
+
return { op, statements: statement[1].map(compileStatement) }
|
|
559
|
+
case 'all':
|
|
560
|
+
case 'any':
|
|
561
|
+
assertArity(statement, 3)
|
|
562
|
+
return {
|
|
563
|
+
op,
|
|
564
|
+
selector: parseSelector(statement[1]),
|
|
565
|
+
statement: compileStatement(statement[2]),
|
|
222
566
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
/** @type {[string, import("./types.js").Statement]} */ (params)
|
|
228
|
-
let collection = resolveSelector(args, selector)
|
|
229
|
-
|
|
230
|
-
if (innerStmt[0] === 'any' || innerStmt[0] === 'all') {
|
|
231
|
-
return evaluateStatement(collection, innerStmt)
|
|
232
|
-
}
|
|
567
|
+
default:
|
|
568
|
+
throw statementError(statement, `unknown operator ${String(op)}`)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
233
571
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Check the structure of a policy and parse its selectors.
|
|
574
|
+
*
|
|
575
|
+
* This does not check that `==` / `!=` values are IPLD values. Delegations
|
|
576
|
+
* are fully validated with `assertPolicy` (in `utils.js`) when created or
|
|
577
|
+
* decoded.
|
|
578
|
+
*
|
|
579
|
+
* @param {unknown} policy
|
|
580
|
+
* @returns {CompiledStatement[]}
|
|
581
|
+
* @throws {TypeError} If the policy is malformed.
|
|
582
|
+
*/
|
|
583
|
+
export function compilePolicy(policy) {
|
|
584
|
+
if (!Array.isArray(policy)) {
|
|
585
|
+
throw new TypeError('Invalid policy: expected an array of statements')
|
|
586
|
+
}
|
|
587
|
+
return policy.map(compileStatement)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Evaluates a compiled statement against data.
|
|
592
|
+
*
|
|
593
|
+
* @param {unknown} data
|
|
594
|
+
* @param {CompiledStatement} statement
|
|
595
|
+
* @returns {boolean}
|
|
596
|
+
*/
|
|
597
|
+
function evaluate(data, statement) {
|
|
598
|
+
switch (statement.op) {
|
|
599
|
+
case '==': {
|
|
600
|
+
const selected = resolve(data, statement.selector)
|
|
601
|
+
return selected !== FAIL && deepEqual(selected, statement.value)
|
|
602
|
+
}
|
|
603
|
+
case '<':
|
|
604
|
+
case '<=':
|
|
605
|
+
case '>':
|
|
606
|
+
case '>=': {
|
|
607
|
+
const selected = resolve(data, statement.selector)
|
|
608
|
+
if (selected === FAIL) return false
|
|
609
|
+
const cmp = compareNumbers(selected, statement.value)
|
|
610
|
+
if (cmp === undefined) return false
|
|
611
|
+
if (statement.op === '<') return cmp < 0
|
|
612
|
+
if (statement.op === '<=') return cmp <= 0
|
|
613
|
+
if (statement.op === '>') return cmp > 0
|
|
614
|
+
return cmp >= 0
|
|
615
|
+
}
|
|
616
|
+
case 'like': {
|
|
617
|
+
const selected = resolve(data, statement.selector)
|
|
618
|
+
return typeof selected === 'string' && statement.regex.test(selected)
|
|
619
|
+
}
|
|
620
|
+
case 'not':
|
|
621
|
+
return !evaluate(data, statement.statement)
|
|
622
|
+
case 'and':
|
|
623
|
+
return statement.statements.every((s) => evaluate(data, s))
|
|
624
|
+
case 'or':
|
|
625
|
+
return (
|
|
626
|
+
statement.statements.length === 0 ||
|
|
627
|
+
statement.statements.some((s) => evaluate(data, s))
|
|
628
|
+
)
|
|
629
|
+
case 'all':
|
|
630
|
+
case 'any': {
|
|
631
|
+
const selected = resolve(data, statement.selector)
|
|
632
|
+
/** @type {unknown[]} */
|
|
633
|
+
let items
|
|
634
|
+
if (Array.isArray(selected)) {
|
|
635
|
+
items = selected
|
|
636
|
+
} else if (isMap(selected)) {
|
|
637
|
+
items = Object.values(selected)
|
|
638
|
+
} else {
|
|
639
|
+
// Quantifying over a non-collection (or an unresolvable selector) is false.
|
|
640
|
+
return false
|
|
250
641
|
}
|
|
251
|
-
|
|
252
|
-
|
|
642
|
+
// Like `and` / `or`, an empty collection is true for both quantifiers.
|
|
643
|
+
if (items.length === 0) return true
|
|
644
|
+
return statement.op === 'all'
|
|
645
|
+
? items.every((item) => evaluate(item, statement.statement))
|
|
646
|
+
: items.some((item) => evaluate(item, statement.statement))
|
|
253
647
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return false
|
|
648
|
+
default:
|
|
649
|
+
return false
|
|
257
650
|
}
|
|
258
|
-
return false
|
|
259
651
|
}
|
|
260
652
|
|
|
261
653
|
/**
|
|
262
654
|
* Validates invocation arguments against a UCAN policy.
|
|
263
655
|
*
|
|
656
|
+
* A malformed policy (unknown operator, wrong arity, invalid selector, ...)
|
|
657
|
+
* never validates: the result is `false` for any `args`.
|
|
658
|
+
*
|
|
264
659
|
* @template {unknown} Args
|
|
265
660
|
* @param {Args} args The arguments of the eventual invocation.
|
|
266
661
|
* @param {import("./types.js").Policy<Args>} policy An array of policy statements.
|
|
267
662
|
* @returns {boolean} True if the args are valid according to the policy, false otherwise.
|
|
268
663
|
*/
|
|
269
664
|
export function validate(args, policy) {
|
|
665
|
+
/** @type {CompiledStatement[]} */
|
|
666
|
+
let compiled
|
|
667
|
+
try {
|
|
668
|
+
compiled = compilePolicy(policy)
|
|
669
|
+
} catch {
|
|
670
|
+
return false
|
|
671
|
+
}
|
|
270
672
|
// The top-level policy is an implicit 'and'.
|
|
271
|
-
return
|
|
673
|
+
return compiled.every((statement) => evaluate(args, statement))
|
|
272
674
|
}
|