@statewalker/webrun-biscuit 0.1.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/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/authorizer.d.ts +112 -0
- package/dist/authorizer.d.ts.map +1 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/builder.d.ts +52 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/crypto.d.ts +22 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/datalog.d.ts +203 -0
- package/dist/datalog.d.ts.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3130 -0
- package/dist/parser.d.ts +80 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/print.d.ts +12 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/proto.d.ts +132 -0
- package/dist/proto.d.ts.map +1 -0
- package/dist/version.d.ts +42 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/authorizer.ts +581 -0
- package/src/base64.ts +42 -0
- package/src/builder.ts +360 -0
- package/src/crypto.ts +208 -0
- package/src/datalog.ts +722 -0
- package/src/index.ts +93 -0
- package/src/parser.ts +606 -0
- package/src/print.ts +147 -0
- package/src/proto.ts +770 -0
- package/src/version.ts +158 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3130 @@
|
|
|
1
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
2
|
+
import { p256 } from "@noble/curves/nist.js";
|
|
3
|
+
//#region src/crypto.ts
|
|
4
|
+
/**
|
|
5
|
+
* Biscuit cryptography: chained block signatures, sealing, third-party
|
|
6
|
+
* (external) signatures. Ed25519 and ECDSA/secp256r1, per SPECIFICATIONS.md.
|
|
7
|
+
*/
|
|
8
|
+
var SignatureError = class extends Error {};
|
|
9
|
+
const ALG_ED25519 = 0;
|
|
10
|
+
const ALG_SECP256R1 = 1;
|
|
11
|
+
function concat(...parts) {
|
|
12
|
+
const len = parts.reduce((a, p) => a + p.length, 0);
|
|
13
|
+
const out = new Uint8Array(len);
|
|
14
|
+
let o = 0;
|
|
15
|
+
for (const p of parts) {
|
|
16
|
+
out.set(p, o);
|
|
17
|
+
o += p.length;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
const ascii = (s) => Uint8Array.from(s, (c) => c.charCodeAt(0));
|
|
22
|
+
const bytesEqual = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
23
|
+
/** little-endian i32, as used for algorithm ids and payload versions */
|
|
24
|
+
function le32(n) {
|
|
25
|
+
const b = /* @__PURE__ */ new Uint8Array(4);
|
|
26
|
+
new DataView(b.buffer).setInt32(0, n, true);
|
|
27
|
+
return b;
|
|
28
|
+
}
|
|
29
|
+
function verifySignature(key, payload, sig) {
|
|
30
|
+
try {
|
|
31
|
+
if (key.algorithm === ALG_ED25519) return ed25519.verify(sig, payload, key.key);
|
|
32
|
+
if (key.algorithm === ALG_SECP256R1) return p256.verify(sig, payload, key.key, {
|
|
33
|
+
format: "der",
|
|
34
|
+
prehash: true,
|
|
35
|
+
lowS: false
|
|
36
|
+
});
|
|
37
|
+
return false;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function publicKeyFromSecret(secret, algorithm) {
|
|
43
|
+
if (algorithm === ALG_ED25519) return ed25519.getPublicKey(secret);
|
|
44
|
+
if (algorithm === ALG_SECP256R1) return p256.getPublicKey(secret, true);
|
|
45
|
+
throw new SignatureError(`unknown algorithm ${algorithm}`);
|
|
46
|
+
}
|
|
47
|
+
function blockPayloadV0(data, nextKey, externalSig) {
|
|
48
|
+
return concat(data, externalSig ?? /* @__PURE__ */ new Uint8Array(0), le32(nextKey.algorithm), nextKey.key);
|
|
49
|
+
}
|
|
50
|
+
function authorityPayloadV1(data, nextKey, version) {
|
|
51
|
+
return concat(ascii("\0BLOCK\0\0VERSION\0"), le32(version), ascii("\0PAYLOAD\0"), data, ascii("\0ALGORITHM\0"), le32(nextKey.algorithm), ascii("\0NEXTKEY\0"), nextKey.key);
|
|
52
|
+
}
|
|
53
|
+
function blockPayloadV1(data, nextKey, externalSig, previousSignature, version) {
|
|
54
|
+
return concat(ascii("\0BLOCK\0\0VERSION\0"), le32(version), ascii("\0PAYLOAD\0"), data, ascii("\0ALGORITHM\0"), le32(nextKey.algorithm), ascii("\0NEXTKEY\0"), nextKey.key, ascii("\0PREVSIG\0"), previousSignature, ...externalSig ? [ascii("\0EXTERNALSIG\0"), externalSig] : []);
|
|
55
|
+
}
|
|
56
|
+
function externalPayloadV1(data, previousSignature, version) {
|
|
57
|
+
return concat(ascii("\0EXTERNAL\0\0VERSION\0"), le32(version), ascii("\0PAYLOAD\0"), data, ascii("\0PREVSIG\0"), previousSignature);
|
|
58
|
+
}
|
|
59
|
+
function sealPayloadV0(block) {
|
|
60
|
+
return concat(block.block, le32(block.nextKey.algorithm), block.nextKey.key, block.signature);
|
|
61
|
+
}
|
|
62
|
+
function sign(payload, secret, algorithm) {
|
|
63
|
+
if (algorithm === ALG_ED25519) return ed25519.sign(payload, secret);
|
|
64
|
+
if (algorithm === ALG_SECP256R1) return p256.sign(payload, secret, {
|
|
65
|
+
format: "der",
|
|
66
|
+
prehash: true
|
|
67
|
+
});
|
|
68
|
+
throw new SignatureError(`unknown algorithm ${algorithm}`);
|
|
69
|
+
}
|
|
70
|
+
/** a fresh keypair for the given algorithm */
|
|
71
|
+
function generateKeypair(algorithm = 0) {
|
|
72
|
+
const secretKey = algorithm === ALG_ED25519 ? ed25519.utils.randomSecretKey() : p256.utils.randomSecretKey();
|
|
73
|
+
return {
|
|
74
|
+
secretKey,
|
|
75
|
+
publicKey: publicKeyFromSecret(secretKey, algorithm)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Verifies the full signature chain and the proof. Throws on failure. */
|
|
79
|
+
function verifyToken(token, rootPublicKey, rootAlgorithm = 0) {
|
|
80
|
+
const root = {
|
|
81
|
+
algorithm: rootAlgorithm,
|
|
82
|
+
key: rootPublicKey
|
|
83
|
+
};
|
|
84
|
+
if (token.authority.externalSignature) throw new SignatureError("the authority block must not carry an external signature");
|
|
85
|
+
const authVersion = token.authority.version ?? 0;
|
|
86
|
+
if (!verifySignature(root, authVersion === 0 ? blockPayloadV0(token.authority.block, token.authority.nextKey) : authVersion === 1 ? authorityPayloadV1(token.authority.block, token.authority.nextKey, authVersion) : (() => {
|
|
87
|
+
throw new SignatureError(`unsupported block version ${authVersion}`);
|
|
88
|
+
})(), token.authority.signature)) throw new SignatureError("invalid authority block signature");
|
|
89
|
+
let currentKey = token.authority.nextKey;
|
|
90
|
+
let previousSignature = token.authority.signature;
|
|
91
|
+
for (const block of token.blocks) {
|
|
92
|
+
const version = block.version ?? 0;
|
|
93
|
+
const externalSig = block.externalSignature?.signature;
|
|
94
|
+
let payload;
|
|
95
|
+
if (version === 0) payload = blockPayloadV0(block.block, block.nextKey, externalSig);
|
|
96
|
+
else if (version === 1) payload = blockPayloadV1(block.block, block.nextKey, externalSig, previousSignature, version);
|
|
97
|
+
else throw new SignatureError(`unsupported block version ${version}`);
|
|
98
|
+
if (!verifySignature(currentKey, payload, block.signature)) throw new SignatureError("invalid block signature");
|
|
99
|
+
if (block.externalSignature) {
|
|
100
|
+
if (version !== 1) throw new SignatureError("unsupported third party block version");
|
|
101
|
+
const ext = externalPayloadV1(block.block, previousSignature, version);
|
|
102
|
+
if (!verifySignature(block.externalSignature.publicKey, ext, block.externalSignature.signature)) throw new SignatureError("invalid external signature");
|
|
103
|
+
}
|
|
104
|
+
currentKey = block.nextKey;
|
|
105
|
+
previousSignature = block.signature;
|
|
106
|
+
}
|
|
107
|
+
if (token.proof.kind === "nextSecret") {
|
|
108
|
+
const derived = publicKeyFromSecret(token.proof.value, currentKey.algorithm);
|
|
109
|
+
if (!bytesEqual(derived, currentKey.key)) throw new SignatureError("the last public key does not match the private key");
|
|
110
|
+
} else {
|
|
111
|
+
const last = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
|
|
112
|
+
if (!verifySignature(currentKey, sealPayloadV0(last), token.proof.value)) throw new SignatureError("invalid seal signature");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Revocation identifier of each block: its signature bytes. */
|
|
116
|
+
function revocationIds(token) {
|
|
117
|
+
return [token.authority, ...token.blocks].map((b) => b.signature);
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/datalog.ts
|
|
121
|
+
/**
|
|
122
|
+
* Biscuit Datalog: term model, fact/rule evaluation with origin tracking, and
|
|
123
|
+
* the stack-based expression virtual machine.
|
|
124
|
+
*/
|
|
125
|
+
const AUTHORIZER = 4294967295;
|
|
126
|
+
const hex$2 = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
127
|
+
/** Canonical string key: JS Map/Set are reference-keyed, so every structural
|
|
128
|
+
* comparison and every de-duplication in the engine goes through this. */
|
|
129
|
+
function termKey(t) {
|
|
130
|
+
switch (t.t) {
|
|
131
|
+
case "var": return `v${t.v}`;
|
|
132
|
+
case "int": return `i${t.v}`;
|
|
133
|
+
case "str": return `s${t.v.length}:${t.v}`;
|
|
134
|
+
case "date": return `d${t.v}`;
|
|
135
|
+
case "bytes": return `b${hex$2(t.v)}`;
|
|
136
|
+
case "bool": return t.v ? "T" : "F";
|
|
137
|
+
case "null": return "N";
|
|
138
|
+
case "set": return `S[${t.v.map(termKey).sort().join(",")}]`;
|
|
139
|
+
case "array": return `A[${t.v.map(termKey).join(",")}]`;
|
|
140
|
+
case "map": return `M[${t.v.map(([k, v]) => `${mapKeyKey(k)}=>${termKey(v)}`).sort().join(",")}]`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const mapKeyKey = (k) => k.t === "int" ? `i${k.v}` : `s${k.v.length}:${k.v}`;
|
|
144
|
+
const termEq = (a, b) => termKey(a) === termKey(b);
|
|
145
|
+
/** de-duplicated, deterministically ordered set contents */
|
|
146
|
+
function normalizeSet(items) {
|
|
147
|
+
const seen = /* @__PURE__ */ new Map();
|
|
148
|
+
for (const i of items) seen.set(termKey(i), i);
|
|
149
|
+
return [...seen.entries()].sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).map((e) => e[1]);
|
|
150
|
+
}
|
|
151
|
+
const factKey = (f) => `${f.predicate.name}/${f.predicate.terms.length}(${f.predicate.terms.map(termKey).join(",")})`;
|
|
152
|
+
var Origin = class Origin {
|
|
153
|
+
ids;
|
|
154
|
+
constructor(ids = []) {
|
|
155
|
+
this.ids = ids;
|
|
156
|
+
}
|
|
157
|
+
static of(...ids) {
|
|
158
|
+
return new Origin([...new Set(ids)].sort((a, b) => a - b));
|
|
159
|
+
}
|
|
160
|
+
union(other) {
|
|
161
|
+
return Origin.of(...this.ids, ...other.ids);
|
|
162
|
+
}
|
|
163
|
+
with(id) {
|
|
164
|
+
return Origin.of(...this.ids, id);
|
|
165
|
+
}
|
|
166
|
+
get key() {
|
|
167
|
+
return this.ids.join(",");
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var TrustedOrigins = class TrustedOrigins {
|
|
171
|
+
set;
|
|
172
|
+
constructor(ids) {
|
|
173
|
+
this.set = new Set(ids);
|
|
174
|
+
}
|
|
175
|
+
static default() {
|
|
176
|
+
return new TrustedOrigins([AUTHORIZER, 0]);
|
|
177
|
+
}
|
|
178
|
+
/** the trusted set is a superset of the fact's origin */
|
|
179
|
+
contains(origin) {
|
|
180
|
+
for (const id of origin.ids) if (!this.set.has(id)) return false;
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
get key() {
|
|
184
|
+
return [...this.set].sort((a, b) => a - b).join(",");
|
|
185
|
+
}
|
|
186
|
+
/** the trusted block ids, for deriving a new set from this one */
|
|
187
|
+
ids() {
|
|
188
|
+
return this.set;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function trustedOriginsFromScopes(ruleScopes, defaults, currentBlock, publicKeyToBlockIds) {
|
|
192
|
+
if (ruleScopes.length === 0) {
|
|
193
|
+
const ids = new Set(defaults.ids());
|
|
194
|
+
ids.add(currentBlock);
|
|
195
|
+
ids.add(AUTHORIZER);
|
|
196
|
+
return new TrustedOrigins(ids);
|
|
197
|
+
}
|
|
198
|
+
const ids = /* @__PURE__ */ new Set([AUTHORIZER, currentBlock]);
|
|
199
|
+
for (const scope of ruleScopes) if (scope.kind === "authority") ids.add(0);
|
|
200
|
+
else if (scope.kind === "previous") {
|
|
201
|
+
if (currentBlock !== 4294967295) for (let i = 0; i <= currentBlock; i++) ids.add(i);
|
|
202
|
+
} else for (const id of publicKeyToBlockIds.get(scope.key) ?? []) ids.add(id);
|
|
203
|
+
return new TrustedOrigins(ids);
|
|
204
|
+
}
|
|
205
|
+
var ExecutionError = class extends Error {
|
|
206
|
+
kind;
|
|
207
|
+
constructor(kind, message) {
|
|
208
|
+
super(message ?? kind);
|
|
209
|
+
this.kind = kind;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const U = {
|
|
213
|
+
Negate: 0,
|
|
214
|
+
Parens: 1,
|
|
215
|
+
Length: 2,
|
|
216
|
+
TypeOf: 3,
|
|
217
|
+
Ffi: 4
|
|
218
|
+
};
|
|
219
|
+
const B = {
|
|
220
|
+
LessThan: 0,
|
|
221
|
+
GreaterThan: 1,
|
|
222
|
+
LessOrEqual: 2,
|
|
223
|
+
GreaterOrEqual: 3,
|
|
224
|
+
Equal: 4,
|
|
225
|
+
Contains: 5,
|
|
226
|
+
Prefix: 6,
|
|
227
|
+
Suffix: 7,
|
|
228
|
+
Regex: 8,
|
|
229
|
+
Add: 9,
|
|
230
|
+
Sub: 10,
|
|
231
|
+
Mul: 11,
|
|
232
|
+
Div: 12,
|
|
233
|
+
And: 13,
|
|
234
|
+
Or: 14,
|
|
235
|
+
Intersection: 15,
|
|
236
|
+
Union: 16,
|
|
237
|
+
BitwiseAnd: 17,
|
|
238
|
+
BitwiseOr: 18,
|
|
239
|
+
BitwiseXor: 19,
|
|
240
|
+
NotEqual: 20,
|
|
241
|
+
HeterogeneousEqual: 21,
|
|
242
|
+
HeterogeneousNotEqual: 22,
|
|
243
|
+
LazyAnd: 23,
|
|
244
|
+
LazyOr: 24,
|
|
245
|
+
All: 25,
|
|
246
|
+
Any: 26,
|
|
247
|
+
Get: 27,
|
|
248
|
+
Ffi: 28,
|
|
249
|
+
TryOr: 29
|
|
250
|
+
};
|
|
251
|
+
const BinaryOp = B;
|
|
252
|
+
const UnaryOp = U;
|
|
253
|
+
const I64_MIN = -(2n ** 63n);
|
|
254
|
+
const I64_MAX = 2n ** 63n - 1n;
|
|
255
|
+
function checkedI64(v) {
|
|
256
|
+
if (v < I64_MIN || v > I64_MAX) throw new ExecutionError("Overflow");
|
|
257
|
+
return {
|
|
258
|
+
t: "int",
|
|
259
|
+
v
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const utf8len = (s) => new TextEncoder().encode(s).length;
|
|
263
|
+
function typeName(t) {
|
|
264
|
+
switch (t.t) {
|
|
265
|
+
case "int": return "integer";
|
|
266
|
+
case "str": return "string";
|
|
267
|
+
case "date": return "date";
|
|
268
|
+
case "bytes": return "bytes";
|
|
269
|
+
case "bool": return "bool";
|
|
270
|
+
case "set": return "set";
|
|
271
|
+
case "null": return "null";
|
|
272
|
+
case "array": return "array";
|
|
273
|
+
case "map": return "map";
|
|
274
|
+
default: throw new ExecutionError("InvalidType");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function unary(op, ffi, v, externs) {
|
|
278
|
+
switch (op) {
|
|
279
|
+
case U.Negate:
|
|
280
|
+
if (v.t !== "bool") throw new ExecutionError("InvalidType");
|
|
281
|
+
return {
|
|
282
|
+
t: "bool",
|
|
283
|
+
v: !v.v
|
|
284
|
+
};
|
|
285
|
+
case U.Parens: return v;
|
|
286
|
+
case U.Length:
|
|
287
|
+
if (v.t === "str") return {
|
|
288
|
+
t: "int",
|
|
289
|
+
v: BigInt(utf8len(v.v))
|
|
290
|
+
};
|
|
291
|
+
if (v.t === "bytes") return {
|
|
292
|
+
t: "int",
|
|
293
|
+
v: BigInt(v.v.length)
|
|
294
|
+
};
|
|
295
|
+
if (v.t === "set" || v.t === "array" || v.t === "map") return {
|
|
296
|
+
t: "int",
|
|
297
|
+
v: BigInt(v.v.length)
|
|
298
|
+
};
|
|
299
|
+
throw new ExecutionError("InvalidType");
|
|
300
|
+
case U.TypeOf: return {
|
|
301
|
+
t: "str",
|
|
302
|
+
v: typeName(v)
|
|
303
|
+
};
|
|
304
|
+
case U.Ffi: {
|
|
305
|
+
const f = externs.get(ffi);
|
|
306
|
+
if (!f) throw new ExecutionError("UndefinedExtern", ffi);
|
|
307
|
+
return f(v);
|
|
308
|
+
}
|
|
309
|
+
default: throw new ExecutionError("InvalidType");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function setContains(set, value) {
|
|
313
|
+
const k = termKey(value);
|
|
314
|
+
return set.some((t) => termKey(t) === k);
|
|
315
|
+
}
|
|
316
|
+
function binary(op, ffi, l, r, externs) {
|
|
317
|
+
const bool = (v) => ({
|
|
318
|
+
t: "bool",
|
|
319
|
+
v
|
|
320
|
+
});
|
|
321
|
+
const strictEqable = l.t === r.t && l.t !== "var" || l.t === "set" && r.t === "set" || l.t === "map" && r.t === "map";
|
|
322
|
+
switch (op) {
|
|
323
|
+
case B.LessThan:
|
|
324
|
+
case B.GreaterThan:
|
|
325
|
+
case B.LessOrEqual:
|
|
326
|
+
case B.GreaterOrEqual: {
|
|
327
|
+
if (!(l.t === "int" && r.t === "int" || l.t === "date" && r.t === "date")) throw new ExecutionError("InvalidType");
|
|
328
|
+
const a = l.v;
|
|
329
|
+
const b = r.v;
|
|
330
|
+
return bool(op === B.LessThan ? a < b : op === B.GreaterThan ? a > b : op === B.LessOrEqual ? a <= b : a >= b);
|
|
331
|
+
}
|
|
332
|
+
case B.Equal:
|
|
333
|
+
case B.NotEqual: {
|
|
334
|
+
if (!strictEqable) throw new ExecutionError("InvalidType");
|
|
335
|
+
const eq = termEq(l, r);
|
|
336
|
+
return bool(op === B.Equal ? eq : !eq);
|
|
337
|
+
}
|
|
338
|
+
case B.HeterogeneousEqual: return bool(l.t === r.t && termEq(l, r));
|
|
339
|
+
case B.HeterogeneousNotEqual: return bool(!(l.t === r.t && termEq(l, r)));
|
|
340
|
+
case B.Add:
|
|
341
|
+
if (l.t === "int" && r.t === "int") return checkedI64(l.v + r.v);
|
|
342
|
+
if (l.t === "str" && r.t === "str") return {
|
|
343
|
+
t: "str",
|
|
344
|
+
v: l.v + r.v
|
|
345
|
+
};
|
|
346
|
+
throw new ExecutionError("InvalidType");
|
|
347
|
+
case B.Sub:
|
|
348
|
+
if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
|
|
349
|
+
return checkedI64(l.v - r.v);
|
|
350
|
+
case B.Mul:
|
|
351
|
+
if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
|
|
352
|
+
return checkedI64(l.v * r.v);
|
|
353
|
+
case B.Div:
|
|
354
|
+
if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
|
|
355
|
+
if (r.v === 0n) throw new ExecutionError("DivideByZero");
|
|
356
|
+
return checkedI64(l.v / r.v);
|
|
357
|
+
case B.BitwiseAnd:
|
|
358
|
+
if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
|
|
359
|
+
return {
|
|
360
|
+
t: "int",
|
|
361
|
+
v: BigInt.asIntN(64, l.v & r.v)
|
|
362
|
+
};
|
|
363
|
+
case B.BitwiseOr:
|
|
364
|
+
if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
|
|
365
|
+
return {
|
|
366
|
+
t: "int",
|
|
367
|
+
v: BigInt.asIntN(64, l.v | r.v)
|
|
368
|
+
};
|
|
369
|
+
case B.BitwiseXor:
|
|
370
|
+
if (l.t !== "int" || r.t !== "int") throw new ExecutionError("InvalidType");
|
|
371
|
+
return {
|
|
372
|
+
t: "int",
|
|
373
|
+
v: BigInt.asIntN(64, l.v ^ r.v)
|
|
374
|
+
};
|
|
375
|
+
case B.And:
|
|
376
|
+
if (l.t !== "bool" || r.t !== "bool") throw new ExecutionError("InvalidType");
|
|
377
|
+
return bool(l.v && r.v);
|
|
378
|
+
case B.Or:
|
|
379
|
+
if (l.t !== "bool" || r.t !== "bool") throw new ExecutionError("InvalidType");
|
|
380
|
+
return bool(l.v || r.v);
|
|
381
|
+
case B.Prefix:
|
|
382
|
+
if (l.t === "str" && r.t === "str") return bool(l.v.startsWith(r.v));
|
|
383
|
+
if (l.t === "array" && r.t === "array") return bool(r.v.every((x, i) => i < l.v.length && termEq(l.v[i], x)));
|
|
384
|
+
throw new ExecutionError("InvalidType");
|
|
385
|
+
case B.Suffix:
|
|
386
|
+
if (l.t === "str" && r.t === "str") return bool(l.v.endsWith(r.v));
|
|
387
|
+
if (l.t === "array" && r.t === "array") {
|
|
388
|
+
const off = l.v.length - r.v.length;
|
|
389
|
+
return bool(off >= 0 && r.v.every((x, i) => termEq(l.v[off + i], x)));
|
|
390
|
+
}
|
|
391
|
+
throw new ExecutionError("InvalidType");
|
|
392
|
+
case B.Regex:
|
|
393
|
+
if (l.t !== "str" || r.t !== "str") throw new ExecutionError("InvalidType");
|
|
394
|
+
try {
|
|
395
|
+
return bool(new RegExp(r.v).test(l.v));
|
|
396
|
+
} catch {
|
|
397
|
+
return bool(false);
|
|
398
|
+
}
|
|
399
|
+
case B.Contains:
|
|
400
|
+
if (l.t === "str" && r.t === "str") return bool(l.v.includes(r.v));
|
|
401
|
+
if (l.t === "set" && r.t === "set") return bool(r.v.every((x) => setContains(l.v, x)));
|
|
402
|
+
if (l.t === "set") return bool(setContains(l.v, r));
|
|
403
|
+
if (l.t === "array") return bool(setContains(l.v, r));
|
|
404
|
+
if (l.t === "map") return bool(l.v.some(([k]) => r.t === "int" ? k.t === "int" && k.v === r.v : r.t === "str" && k.t === "str" && k.v === r.v));
|
|
405
|
+
throw new ExecutionError("InvalidType");
|
|
406
|
+
case B.Intersection:
|
|
407
|
+
if (l.t !== "set" || r.t !== "set") throw new ExecutionError("InvalidType");
|
|
408
|
+
return {
|
|
409
|
+
t: "set",
|
|
410
|
+
v: normalizeSet(l.v.filter((x) => setContains(r.v, x)))
|
|
411
|
+
};
|
|
412
|
+
case B.Union:
|
|
413
|
+
if (l.t !== "set" || r.t !== "set") throw new ExecutionError("InvalidType");
|
|
414
|
+
return {
|
|
415
|
+
t: "set",
|
|
416
|
+
v: normalizeSet([...l.v, ...r.v])
|
|
417
|
+
};
|
|
418
|
+
case B.Get:
|
|
419
|
+
if (l.t === "array" && r.t === "int") {
|
|
420
|
+
const i = r.v < 0n || r.v >= BigInt(l.v.length) ? -1 : Number(r.v);
|
|
421
|
+
return i < 0 ? { t: "null" } : l.v[i];
|
|
422
|
+
}
|
|
423
|
+
if (l.t === "map" && (r.t === "int" || r.t === "str")) {
|
|
424
|
+
const found = l.v.find(([k]) => r.t === "int" ? k.t === "int" && k.v === r.v : k.t === "str" && k.v === r.v);
|
|
425
|
+
return found ? found[1] : { t: "null" };
|
|
426
|
+
}
|
|
427
|
+
throw new ExecutionError("InvalidType");
|
|
428
|
+
case B.Ffi: {
|
|
429
|
+
const f = externs.get(ffi);
|
|
430
|
+
if (!f) throw new ExecutionError("UndefinedExtern", ffi);
|
|
431
|
+
return f(l, r);
|
|
432
|
+
}
|
|
433
|
+
default: throw new ExecutionError("InvalidType");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function evaluateExpression(ops, values, externs = /* @__PURE__ */ new Map()) {
|
|
437
|
+
const stack = [];
|
|
438
|
+
for (const op of ops) switch (op.kind) {
|
|
439
|
+
case "value":
|
|
440
|
+
if (op.value.t === "var") {
|
|
441
|
+
const bound = values.get(op.value.v);
|
|
442
|
+
if (bound === void 0) throw new ExecutionError("UnknownVariable", String(op.value.v));
|
|
443
|
+
stack.push({
|
|
444
|
+
s: "term",
|
|
445
|
+
v: bound
|
|
446
|
+
});
|
|
447
|
+
} else stack.push({
|
|
448
|
+
s: "term",
|
|
449
|
+
v: op.value
|
|
450
|
+
});
|
|
451
|
+
break;
|
|
452
|
+
case "unary": {
|
|
453
|
+
const a = stack.pop();
|
|
454
|
+
if (!a || a.s !== "term") throw new ExecutionError("InvalidStack");
|
|
455
|
+
stack.push({
|
|
456
|
+
s: "term",
|
|
457
|
+
v: unary(op.op, op.ffi, a.v, externs)
|
|
458
|
+
});
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case "closure":
|
|
462
|
+
stack.push({
|
|
463
|
+
s: "closure",
|
|
464
|
+
params: op.params,
|
|
465
|
+
ops: op.ops
|
|
466
|
+
});
|
|
467
|
+
break;
|
|
468
|
+
case "binary": {
|
|
469
|
+
const right = stack.pop();
|
|
470
|
+
const left = stack.pop();
|
|
471
|
+
if (!right || !left) throw new ExecutionError("InvalidStack");
|
|
472
|
+
if (right.s === "term" && left.s === "term") stack.push({
|
|
473
|
+
s: "term",
|
|
474
|
+
v: binary(op.op, op.ffi, left.v, right.v, externs)
|
|
475
|
+
});
|
|
476
|
+
else {
|
|
477
|
+
const closure = right.s === "closure" ? right : left.s === "closure" ? left : null;
|
|
478
|
+
const term = right.s === "term" ? right : left.s === "term" ? left : null;
|
|
479
|
+
if (!closure || !term) throw new ExecutionError("InvalidStack");
|
|
480
|
+
for (const p of closure.params) if (values.has(p)) throw new ExecutionError("ShadowedVariable", String(p));
|
|
481
|
+
stack.push({
|
|
482
|
+
s: "term",
|
|
483
|
+
v: evaluateWithClosure(op.op, term.v, closure.ops, closure.params, values, externs)
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (stack.length !== 1) throw new ExecutionError("InvalidStack");
|
|
490
|
+
const top = stack[0];
|
|
491
|
+
if (top.s !== "term") throw new ExecutionError("InvalidStack");
|
|
492
|
+
return top.v;
|
|
493
|
+
}
|
|
494
|
+
function evaluateWithClosure(op, left, ops, params, values, externs) {
|
|
495
|
+
if (op === B.TryOr && params.length === 0) try {
|
|
496
|
+
return evaluateExpression(ops, values, externs);
|
|
497
|
+
} catch {
|
|
498
|
+
return left;
|
|
499
|
+
}
|
|
500
|
+
if ((op === B.LazyOr || op === B.LazyAnd) && params.length === 0) {
|
|
501
|
+
if (left.t !== "bool") throw new ExecutionError("InvalidType");
|
|
502
|
+
if (op === B.LazyOr && left.v) return {
|
|
503
|
+
t: "bool",
|
|
504
|
+
v: true
|
|
505
|
+
};
|
|
506
|
+
if (op === B.LazyAnd && !left.v) return {
|
|
507
|
+
t: "bool",
|
|
508
|
+
v: false
|
|
509
|
+
};
|
|
510
|
+
return evaluateExpression(ops, values, externs);
|
|
511
|
+
}
|
|
512
|
+
if ((op === B.All || op === B.Any) && params.length === 1) {
|
|
513
|
+
const param = params[0];
|
|
514
|
+
let items;
|
|
515
|
+
if (left.t === "set" || left.t === "array") items = left.v;
|
|
516
|
+
else if (left.t === "map") items = left.v.map(([k, v]) => ({
|
|
517
|
+
t: "array",
|
|
518
|
+
v: [k.t === "int" ? {
|
|
519
|
+
t: "int",
|
|
520
|
+
v: k.v
|
|
521
|
+
} : {
|
|
522
|
+
t: "str",
|
|
523
|
+
v: k.v
|
|
524
|
+
}, v]
|
|
525
|
+
}));
|
|
526
|
+
else throw new ExecutionError("InvalidType");
|
|
527
|
+
const wanted = op === B.All;
|
|
528
|
+
for (const item of items) {
|
|
529
|
+
const scoped = new Map(values);
|
|
530
|
+
scoped.set(param, item);
|
|
531
|
+
const res = evaluateExpression(ops, scoped, externs);
|
|
532
|
+
if (res.t !== "bool") throw new ExecutionError("InvalidType");
|
|
533
|
+
if (res.v !== wanted) return {
|
|
534
|
+
t: "bool",
|
|
535
|
+
v: !wanted
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
t: "bool",
|
|
540
|
+
v: wanted
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
throw new ExecutionError("InvalidType");
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Note the divergence: the reference defaults `max_time` to **1 millisecond**,
|
|
547
|
+
* which is unreachably tight for a cold JS engine and would make ordinary
|
|
548
|
+
* tokens fail non-deterministically. 1 second is generous by comparison, so a
|
|
549
|
+
* caller exposed to untrusted tokens should lower it deliberately rather than
|
|
550
|
+
* rely on this default as a denial-of-service bound.
|
|
551
|
+
*/
|
|
552
|
+
const DEFAULT_LIMITS = {
|
|
553
|
+
maxFacts: 1e3,
|
|
554
|
+
maxIterations: 100,
|
|
555
|
+
maxTimeMs: 1e3
|
|
556
|
+
};
|
|
557
|
+
function matchPredicate(rule, fact) {
|
|
558
|
+
if (rule.name !== fact.name || rule.terms.length !== fact.terms.length) return false;
|
|
559
|
+
for (let i = 0; i < rule.terms.length; i++) {
|
|
560
|
+
const rt = rule.terms[i];
|
|
561
|
+
const ft = fact.terms[i];
|
|
562
|
+
if (ft.t === "var") return false;
|
|
563
|
+
if (rt.t === "var") continue;
|
|
564
|
+
if (rt.t !== ft.t || !termEq(rt, ft)) return false;
|
|
565
|
+
}
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
function variablesOf(rule) {
|
|
569
|
+
const out = /* @__PURE__ */ new Set();
|
|
570
|
+
for (const p of rule.body) for (const t of p.terms) if (t.t === "var") out.add(t.v);
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
/** backtracking join over the rule body, mirroring Rust's CombineIt */
|
|
574
|
+
function* combine(predicates, facts, bindings, variables) {
|
|
575
|
+
if (predicates.length === 0) {
|
|
576
|
+
for (const v of variables) if (!bindings.has(v)) return;
|
|
577
|
+
yield [Origin.of(), new Map(bindings)];
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const [head, ...rest] = predicates;
|
|
581
|
+
for (const [origin, fact] of facts.get(`${head.name}/${head.terms.length}`) ?? []) {
|
|
582
|
+
if (!matchPredicate(head, fact.predicate)) continue;
|
|
583
|
+
const next = new Map(bindings);
|
|
584
|
+
let ok = true;
|
|
585
|
+
for (let i = 0; i < head.terms.length; i++) {
|
|
586
|
+
const rt = head.terms[i];
|
|
587
|
+
if (rt.t !== "var") continue;
|
|
588
|
+
const existing = next.get(rt.v);
|
|
589
|
+
if (existing === void 0) next.set(rt.v, fact.predicate.terms[i]);
|
|
590
|
+
else if (!termEq(existing, fact.predicate.terms[i])) {
|
|
591
|
+
ok = false;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (!ok) continue;
|
|
596
|
+
for (const [subOrigin, result] of combine(rest, facts, next, variables)) yield [subOrigin.union(origin), result];
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
var World = class {
|
|
600
|
+
/** facts indexed by origin key */
|
|
601
|
+
facts = /* @__PURE__ */ new Map();
|
|
602
|
+
rules = [];
|
|
603
|
+
externs = /* @__PURE__ */ new Map();
|
|
604
|
+
iterations = 0;
|
|
605
|
+
generation = 0;
|
|
606
|
+
indexCache = /* @__PURE__ */ new Map();
|
|
607
|
+
addFact(origin, fact) {
|
|
608
|
+
let bucket = this.facts.get(origin.key);
|
|
609
|
+
if (!bucket) {
|
|
610
|
+
bucket = {
|
|
611
|
+
origin,
|
|
612
|
+
items: /* @__PURE__ */ new Map()
|
|
613
|
+
};
|
|
614
|
+
this.facts.set(origin.key, bucket);
|
|
615
|
+
}
|
|
616
|
+
const key = factKey(fact);
|
|
617
|
+
if (!bucket.items.has(key)) {
|
|
618
|
+
bucket.items.set(key, fact);
|
|
619
|
+
this.generation++;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
addRule(origin, trusted, rule) {
|
|
623
|
+
this.rules.push({
|
|
624
|
+
origin,
|
|
625
|
+
trusted,
|
|
626
|
+
rule
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
factCount() {
|
|
630
|
+
let n = 0;
|
|
631
|
+
for (const b of this.facts.values()) n += b.items.size;
|
|
632
|
+
return n;
|
|
633
|
+
}
|
|
634
|
+
visible(trusted) {
|
|
635
|
+
const cached = this.indexCache.get(trusted.key);
|
|
636
|
+
if (cached && cached.generation === this.generation) return cached.index;
|
|
637
|
+
const index = /* @__PURE__ */ new Map();
|
|
638
|
+
for (const bucket of this.facts.values()) {
|
|
639
|
+
if (!trusted.contains(bucket.origin)) continue;
|
|
640
|
+
for (const f of bucket.items.values()) {
|
|
641
|
+
const key = `${f.predicate.name}/${f.predicate.terms.length}`;
|
|
642
|
+
const list = index.get(key);
|
|
643
|
+
if (list) list.push([bucket.origin, f]);
|
|
644
|
+
else index.set(key, [[bucket.origin, f]]);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
this.indexCache.set(trusted.key, {
|
|
648
|
+
generation: this.generation,
|
|
649
|
+
index
|
|
650
|
+
});
|
|
651
|
+
return index;
|
|
652
|
+
}
|
|
653
|
+
/** naive fixpoint: apply every rule until no new fact appears */
|
|
654
|
+
run(limits = DEFAULT_LIMITS) {
|
|
655
|
+
const deadline = Date.now() + limits.maxTimeMs;
|
|
656
|
+
let index = 0;
|
|
657
|
+
for (;;) {
|
|
658
|
+
const generated = [];
|
|
659
|
+
for (const { origin, trusted, rule } of this.rules) {
|
|
660
|
+
const facts = this.visible(trusted);
|
|
661
|
+
for (const [o, f] of this.apply(rule, facts, origin)) generated.push([o, f]);
|
|
662
|
+
}
|
|
663
|
+
const before = this.factCount();
|
|
664
|
+
for (const [o, f] of generated) this.addFact(o, f);
|
|
665
|
+
if (this.factCount() === before) break;
|
|
666
|
+
index++;
|
|
667
|
+
if (index === limits.maxIterations) throw new ExecutionError("TooManyIterations");
|
|
668
|
+
if (this.factCount() >= limits.maxFacts) throw new ExecutionError("TooManyFacts");
|
|
669
|
+
if (Date.now() >= deadline) throw new ExecutionError("Timeout");
|
|
670
|
+
}
|
|
671
|
+
this.iterations += index;
|
|
672
|
+
}
|
|
673
|
+
*apply(rule, facts, ruleOrigin) {
|
|
674
|
+
const variables = variablesOf(rule);
|
|
675
|
+
for (const [origin, bindings] of combine(rule.body, facts, /* @__PURE__ */ new Map(), variables)) {
|
|
676
|
+
let pass = true;
|
|
677
|
+
for (const ops of rule.expressions) {
|
|
678
|
+
const res = evaluateExpression(ops, bindings, this.externs);
|
|
679
|
+
if (res.t !== "bool") throw new ExecutionError("InvalidType");
|
|
680
|
+
if (!res.v) {
|
|
681
|
+
pass = false;
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (!pass) continue;
|
|
686
|
+
const terms = [];
|
|
687
|
+
let complete = true;
|
|
688
|
+
for (const t of rule.head.terms) if (t.t === "var") {
|
|
689
|
+
const bound = bindings.get(t.v);
|
|
690
|
+
if (bound === void 0) {
|
|
691
|
+
complete = false;
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
terms.push(bound);
|
|
695
|
+
} else terms.push(t);
|
|
696
|
+
if (!complete) continue;
|
|
697
|
+
yield [origin.with(ruleOrigin), { predicate: {
|
|
698
|
+
name: rule.head.name,
|
|
699
|
+
terms
|
|
700
|
+
} }];
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/** `check if` / policies: does at least one combination match? */
|
|
704
|
+
queryMatch(rule, origin, trusted) {
|
|
705
|
+
for (const _ of this.apply(rule, this.visible(trusted), origin)) return true;
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
/** `check all`: every matching combination must satisfy the expressions */
|
|
709
|
+
queryMatchAll(rule, trusted) {
|
|
710
|
+
const variables = variablesOf(rule);
|
|
711
|
+
let found = false;
|
|
712
|
+
for (const [, bindings] of combine(rule.body, this.visible(trusted), /* @__PURE__ */ new Map(), variables)) {
|
|
713
|
+
found = true;
|
|
714
|
+
for (const ops of rule.expressions) {
|
|
715
|
+
const res = evaluateExpression(ops, bindings, this.externs);
|
|
716
|
+
if (res.t !== "bool") throw new ExecutionError("InvalidType");
|
|
717
|
+
if (!res.v) return false;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return found;
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
//#endregion
|
|
724
|
+
//#region src/parser.ts
|
|
725
|
+
/**
|
|
726
|
+
* Parser for the Biscuit Datalog text syntax (SPECIFICATIONS.md grammar).
|
|
727
|
+
* Hand-written recursive descent + precedence climbing; expressions are
|
|
728
|
+
* compiled straight to the opcode form the VM executes.
|
|
729
|
+
*/
|
|
730
|
+
var ParseError = class extends Error {};
|
|
731
|
+
const NAME_START = /[\p{L}]/u;
|
|
732
|
+
const NAME_CHAR = /[\p{L}\p{N}_:]/u;
|
|
733
|
+
var Parser = class {
|
|
734
|
+
src;
|
|
735
|
+
i = 0;
|
|
736
|
+
vars;
|
|
737
|
+
constructor(src, vars) {
|
|
738
|
+
this.src = src;
|
|
739
|
+
this.vars = vars ?? /* @__PURE__ */ new Map();
|
|
740
|
+
}
|
|
741
|
+
/** id -> name, for printing rules back out */
|
|
742
|
+
variableNames() {
|
|
743
|
+
const out = /* @__PURE__ */ new Map();
|
|
744
|
+
for (const [name, id] of this.vars) out.set(id, name);
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
varId(name) {
|
|
748
|
+
let id = this.vars.get(name);
|
|
749
|
+
if (id === void 0) {
|
|
750
|
+
id = this.vars.size + 1;
|
|
751
|
+
this.vars.set(name, id);
|
|
752
|
+
}
|
|
753
|
+
return id;
|
|
754
|
+
}
|
|
755
|
+
ws() {
|
|
756
|
+
for (;;) {
|
|
757
|
+
while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
|
|
758
|
+
if (this.src.startsWith("//", this.i)) {
|
|
759
|
+
const nl = this.src.indexOf("\n", this.i);
|
|
760
|
+
this.i = nl === -1 ? this.src.length : nl + 1;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (this.src.startsWith("/*", this.i)) {
|
|
764
|
+
const end = this.src.indexOf("*/", this.i);
|
|
765
|
+
this.i = end === -1 ? this.src.length : end + 2;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
get eof() {
|
|
772
|
+
this.ws();
|
|
773
|
+
return this.i >= this.src.length;
|
|
774
|
+
}
|
|
775
|
+
peek(s) {
|
|
776
|
+
this.ws();
|
|
777
|
+
return this.src.startsWith(s, this.i);
|
|
778
|
+
}
|
|
779
|
+
eat(s) {
|
|
780
|
+
if (!this.peek(s)) return false;
|
|
781
|
+
this.i += s.length;
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
expect(s) {
|
|
785
|
+
if (!this.eat(s)) throw new ParseError(`expected "${s}" at offset ${this.i}`);
|
|
786
|
+
}
|
|
787
|
+
/** keyword: matches only if not followed by a name character */
|
|
788
|
+
keyword(k) {
|
|
789
|
+
this.ws();
|
|
790
|
+
if (!this.src.startsWith(k, this.i)) return false;
|
|
791
|
+
const after = this.src[this.i + k.length];
|
|
792
|
+
if (after !== void 0 && NAME_CHAR.test(after)) return false;
|
|
793
|
+
this.i += k.length;
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
/** variable names, unlike predicate names, may start with a digit ($0) */
|
|
797
|
+
variableName() {
|
|
798
|
+
this.ws();
|
|
799
|
+
const start = this.i;
|
|
800
|
+
while (this.i < this.src.length && NAME_CHAR.test(this.src[this.i])) this.i++;
|
|
801
|
+
if (this.i === start) throw new ParseError(`expected a variable name at offset ${this.i}`);
|
|
802
|
+
return this.src.slice(start, this.i);
|
|
803
|
+
}
|
|
804
|
+
name() {
|
|
805
|
+
this.ws();
|
|
806
|
+
const start = this.i;
|
|
807
|
+
if (this.i >= this.src.length || !NAME_START.test(this.src[this.i])) throw new ParseError(`expected a name at offset ${this.i}`);
|
|
808
|
+
this.i++;
|
|
809
|
+
while (this.i < this.src.length && NAME_CHAR.test(this.src[this.i])) this.i++;
|
|
810
|
+
return this.src.slice(start, this.i);
|
|
811
|
+
}
|
|
812
|
+
string() {
|
|
813
|
+
this.expect("\"");
|
|
814
|
+
let out = "";
|
|
815
|
+
for (;;) {
|
|
816
|
+
if (this.i >= this.src.length) throw new ParseError("unterminated string");
|
|
817
|
+
const c = this.src[this.i++];
|
|
818
|
+
if (c === "\"") return out;
|
|
819
|
+
if (c !== "\\") {
|
|
820
|
+
out += c;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const e = this.src[this.i++];
|
|
824
|
+
if (e === "n") out += "\n";
|
|
825
|
+
else if (e === "t") out += " ";
|
|
826
|
+
else if (e === "r") out += "\r";
|
|
827
|
+
else if (e === "0") out += "\0";
|
|
828
|
+
else if (e === "\"") out += "\"";
|
|
829
|
+
else if (e === "\\") out += "\\";
|
|
830
|
+
else if (e === "u") {
|
|
831
|
+
const m = /^\{([0-9a-fA-F]+)\}/.exec(this.src.slice(this.i));
|
|
832
|
+
if (m) {
|
|
833
|
+
out += String.fromCodePoint(parseInt(m[1], 16));
|
|
834
|
+
this.i += m[0].length;
|
|
835
|
+
} else {
|
|
836
|
+
out += String.fromCharCode(parseInt(this.src.substr(this.i, 4), 16));
|
|
837
|
+
this.i += 4;
|
|
838
|
+
}
|
|
839
|
+
} else out += e;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
tryDate() {
|
|
843
|
+
this.ws();
|
|
844
|
+
const m = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})/.exec(this.src.slice(this.i));
|
|
845
|
+
if (!m) return null;
|
|
846
|
+
const ms = Date.parse(m[0]);
|
|
847
|
+
if (Number.isNaN(ms)) throw new ParseError(`invalid date ${m[0]}`);
|
|
848
|
+
this.i += m[0].length;
|
|
849
|
+
return BigInt(Math.floor(ms / 1e3));
|
|
850
|
+
}
|
|
851
|
+
/** a term that may be a variable (rule/check context) */
|
|
852
|
+
term(allowVariables = true) {
|
|
853
|
+
this.ws();
|
|
854
|
+
const date = this.tryDate();
|
|
855
|
+
if (date !== null) return {
|
|
856
|
+
t: "date",
|
|
857
|
+
v: date
|
|
858
|
+
};
|
|
859
|
+
if (this.peek("\"")) return {
|
|
860
|
+
t: "str",
|
|
861
|
+
v: this.string()
|
|
862
|
+
};
|
|
863
|
+
if (this.eat("$")) {
|
|
864
|
+
if (!allowVariables) throw new ParseError("variables are not allowed in facts");
|
|
865
|
+
return {
|
|
866
|
+
t: "var",
|
|
867
|
+
v: this.varId(this.variableName())
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (this.keyword("true")) return {
|
|
871
|
+
t: "bool",
|
|
872
|
+
v: true
|
|
873
|
+
};
|
|
874
|
+
if (this.keyword("false")) return {
|
|
875
|
+
t: "bool",
|
|
876
|
+
v: false
|
|
877
|
+
};
|
|
878
|
+
if (this.keyword("null")) return { t: "null" };
|
|
879
|
+
if (this.peek("hex:")) {
|
|
880
|
+
this.i += 4;
|
|
881
|
+
const start = this.i;
|
|
882
|
+
while (this.i < this.src.length && /[0-9a-fA-F]/.test(this.src[this.i])) this.i++;
|
|
883
|
+
const h = this.src.slice(start, this.i);
|
|
884
|
+
if (h.length % 2) throw new ParseError("odd-length hex literal");
|
|
885
|
+
const bytes = new Uint8Array(h.length / 2);
|
|
886
|
+
for (let j = 0; j < bytes.length; j++) bytes[j] = parseInt(h.substr(j * 2, 2), 16);
|
|
887
|
+
return {
|
|
888
|
+
t: "bytes",
|
|
889
|
+
v: bytes
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
if (this.peek("[")) return this.array(allowVariables);
|
|
893
|
+
if (this.peek("{")) return this.setOrMap(allowVariables);
|
|
894
|
+
const m = /^-?\d+/.exec(this.src.slice(this.i));
|
|
895
|
+
if (m) {
|
|
896
|
+
this.i += m[0].length;
|
|
897
|
+
return {
|
|
898
|
+
t: "int",
|
|
899
|
+
v: BigInt(m[0])
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
throw new ParseError(`unexpected term at offset ${this.i}: ${this.src.slice(this.i, this.i + 20)}`);
|
|
903
|
+
}
|
|
904
|
+
array(allowVariables) {
|
|
905
|
+
this.expect("[");
|
|
906
|
+
const items = [];
|
|
907
|
+
if (this.eat("]")) return {
|
|
908
|
+
t: "array",
|
|
909
|
+
v: items
|
|
910
|
+
};
|
|
911
|
+
do
|
|
912
|
+
items.push(this.term(allowVariables));
|
|
913
|
+
while (this.eat(","));
|
|
914
|
+
this.expect("]");
|
|
915
|
+
return {
|
|
916
|
+
t: "array",
|
|
917
|
+
v: items
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
setOrMap(allowVariables) {
|
|
921
|
+
this.expect("{");
|
|
922
|
+
if (this.eat(",")) {
|
|
923
|
+
this.expect("}");
|
|
924
|
+
return {
|
|
925
|
+
t: "set",
|
|
926
|
+
v: []
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
if (this.eat("}")) return {
|
|
930
|
+
t: "map",
|
|
931
|
+
v: []
|
|
932
|
+
};
|
|
933
|
+
const first = this.term(allowVariables);
|
|
934
|
+
if (this.eat(":")) {
|
|
935
|
+
const entries = [[this.asMapKey(first), this.term(allowVariables)]];
|
|
936
|
+
while (this.eat(",")) {
|
|
937
|
+
if (this.peek("}")) break;
|
|
938
|
+
const k = this.asMapKey(this.term(allowVariables));
|
|
939
|
+
this.expect(":");
|
|
940
|
+
entries.push([k, this.term(allowVariables)]);
|
|
941
|
+
}
|
|
942
|
+
this.expect("}");
|
|
943
|
+
return {
|
|
944
|
+
t: "map",
|
|
945
|
+
v: entries
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
const items = [first];
|
|
949
|
+
while (this.eat(",")) {
|
|
950
|
+
if (this.peek("}")) break;
|
|
951
|
+
items.push(this.term(allowVariables));
|
|
952
|
+
}
|
|
953
|
+
this.expect("}");
|
|
954
|
+
return {
|
|
955
|
+
t: "set",
|
|
956
|
+
v: normalizeSet(items)
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
asMapKey(t) {
|
|
960
|
+
if (t.t === "int") return {
|
|
961
|
+
t: "int",
|
|
962
|
+
v: t.v
|
|
963
|
+
};
|
|
964
|
+
if (t.t === "str") return {
|
|
965
|
+
t: "str",
|
|
966
|
+
v: t.v
|
|
967
|
+
};
|
|
968
|
+
throw new ParseError("map keys must be integers or strings");
|
|
969
|
+
}
|
|
970
|
+
predicate(allowVariables = true) {
|
|
971
|
+
const name = this.name();
|
|
972
|
+
this.expect("(");
|
|
973
|
+
const terms = [];
|
|
974
|
+
if (!this.peek(")")) do
|
|
975
|
+
terms.push(this.term(allowVariables));
|
|
976
|
+
while (this.eat(","));
|
|
977
|
+
this.expect(")");
|
|
978
|
+
return {
|
|
979
|
+
name,
|
|
980
|
+
terms
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
/** precedence climbing; each level returns opcodes in postfix order */
|
|
984
|
+
expression() {
|
|
985
|
+
return this.orExpr();
|
|
986
|
+
}
|
|
987
|
+
orExpr() {
|
|
988
|
+
let left = this.andExpr();
|
|
989
|
+
while (this.peek("||")) {
|
|
990
|
+
this.i += 2;
|
|
991
|
+
const right = this.andExpr();
|
|
992
|
+
left = [
|
|
993
|
+
...left,
|
|
994
|
+
{
|
|
995
|
+
kind: "closure",
|
|
996
|
+
params: [],
|
|
997
|
+
ops: right
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
kind: "binary",
|
|
1001
|
+
op: BinaryOp.LazyOr
|
|
1002
|
+
}
|
|
1003
|
+
];
|
|
1004
|
+
}
|
|
1005
|
+
return left;
|
|
1006
|
+
}
|
|
1007
|
+
andExpr() {
|
|
1008
|
+
let left = this.comparison();
|
|
1009
|
+
while (this.peek("&&")) {
|
|
1010
|
+
this.i += 2;
|
|
1011
|
+
const right = this.comparison();
|
|
1012
|
+
left = [
|
|
1013
|
+
...left,
|
|
1014
|
+
{
|
|
1015
|
+
kind: "closure",
|
|
1016
|
+
params: [],
|
|
1017
|
+
ops: right
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
kind: "binary",
|
|
1021
|
+
op: BinaryOp.LazyAnd
|
|
1022
|
+
}
|
|
1023
|
+
];
|
|
1024
|
+
}
|
|
1025
|
+
return left;
|
|
1026
|
+
}
|
|
1027
|
+
comparison() {
|
|
1028
|
+
const left = this.bitXor();
|
|
1029
|
+
for (const [tok, op] of [
|
|
1030
|
+
["<=", BinaryOp.LessOrEqual],
|
|
1031
|
+
[">=", BinaryOp.GreaterOrEqual],
|
|
1032
|
+
["===", BinaryOp.Equal],
|
|
1033
|
+
["!==", BinaryOp.NotEqual],
|
|
1034
|
+
["==", BinaryOp.HeterogeneousEqual],
|
|
1035
|
+
["!=", BinaryOp.HeterogeneousNotEqual],
|
|
1036
|
+
["<", BinaryOp.LessThan],
|
|
1037
|
+
[">", BinaryOp.GreaterThan]
|
|
1038
|
+
]) if (this.peekOperator(tok)) {
|
|
1039
|
+
this.i += tok.length;
|
|
1040
|
+
const right = this.bitXor();
|
|
1041
|
+
return [
|
|
1042
|
+
...left,
|
|
1043
|
+
...right,
|
|
1044
|
+
{
|
|
1045
|
+
kind: "binary",
|
|
1046
|
+
op
|
|
1047
|
+
}
|
|
1048
|
+
];
|
|
1049
|
+
}
|
|
1050
|
+
return left;
|
|
1051
|
+
}
|
|
1052
|
+
/** avoids matching "==" when the source really has "===" */
|
|
1053
|
+
peekOperator(tok) {
|
|
1054
|
+
this.ws();
|
|
1055
|
+
if (!this.src.startsWith(tok, this.i)) return false;
|
|
1056
|
+
const next = this.src[this.i + tok.length];
|
|
1057
|
+
if ((tok === "==" || tok === "!=") && next === "=") return false;
|
|
1058
|
+
if ((tok === "<" || tok === ">") && next === "=") return false;
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
bitXor() {
|
|
1062
|
+
let left = this.bitOr();
|
|
1063
|
+
while (this.peekOperator("^")) {
|
|
1064
|
+
this.i += 1;
|
|
1065
|
+
left = [
|
|
1066
|
+
...left,
|
|
1067
|
+
...this.bitOr(),
|
|
1068
|
+
{
|
|
1069
|
+
kind: "binary",
|
|
1070
|
+
op: BinaryOp.BitwiseXor
|
|
1071
|
+
}
|
|
1072
|
+
];
|
|
1073
|
+
}
|
|
1074
|
+
return left;
|
|
1075
|
+
}
|
|
1076
|
+
bitOr() {
|
|
1077
|
+
let left = this.bitAnd();
|
|
1078
|
+
while (this.peek("|") && !this.peek("||")) {
|
|
1079
|
+
this.i += 1;
|
|
1080
|
+
left = [
|
|
1081
|
+
...left,
|
|
1082
|
+
...this.bitAnd(),
|
|
1083
|
+
{
|
|
1084
|
+
kind: "binary",
|
|
1085
|
+
op: BinaryOp.BitwiseOr
|
|
1086
|
+
}
|
|
1087
|
+
];
|
|
1088
|
+
}
|
|
1089
|
+
return left;
|
|
1090
|
+
}
|
|
1091
|
+
bitAnd() {
|
|
1092
|
+
let left = this.additive();
|
|
1093
|
+
while (this.peek("&") && !this.peek("&&")) {
|
|
1094
|
+
this.i += 1;
|
|
1095
|
+
left = [
|
|
1096
|
+
...left,
|
|
1097
|
+
...this.additive(),
|
|
1098
|
+
{
|
|
1099
|
+
kind: "binary",
|
|
1100
|
+
op: BinaryOp.BitwiseAnd
|
|
1101
|
+
}
|
|
1102
|
+
];
|
|
1103
|
+
}
|
|
1104
|
+
return left;
|
|
1105
|
+
}
|
|
1106
|
+
additive() {
|
|
1107
|
+
let left = this.multiplicative();
|
|
1108
|
+
for (;;) if (this.peekSign("+")) {
|
|
1109
|
+
this.i += 1;
|
|
1110
|
+
left = [
|
|
1111
|
+
...left,
|
|
1112
|
+
...this.multiplicative(),
|
|
1113
|
+
{
|
|
1114
|
+
kind: "binary",
|
|
1115
|
+
op: BinaryOp.Add
|
|
1116
|
+
}
|
|
1117
|
+
];
|
|
1118
|
+
} else if (this.peekSign("-")) {
|
|
1119
|
+
this.i += 1;
|
|
1120
|
+
left = [
|
|
1121
|
+
...left,
|
|
1122
|
+
...this.multiplicative(),
|
|
1123
|
+
{
|
|
1124
|
+
kind: "binary",
|
|
1125
|
+
op: BinaryOp.Sub
|
|
1126
|
+
}
|
|
1127
|
+
];
|
|
1128
|
+
} else return left;
|
|
1129
|
+
}
|
|
1130
|
+
/** `-` starts a negative literal only when it is not an infix position */
|
|
1131
|
+
peekSign(tok) {
|
|
1132
|
+
this.ws();
|
|
1133
|
+
return this.src.startsWith(tok, this.i);
|
|
1134
|
+
}
|
|
1135
|
+
multiplicative() {
|
|
1136
|
+
let left = this.unary();
|
|
1137
|
+
for (;;) if (this.peek("*")) {
|
|
1138
|
+
this.i += 1;
|
|
1139
|
+
left = [
|
|
1140
|
+
...left,
|
|
1141
|
+
...this.unary(),
|
|
1142
|
+
{
|
|
1143
|
+
kind: "binary",
|
|
1144
|
+
op: BinaryOp.Mul
|
|
1145
|
+
}
|
|
1146
|
+
];
|
|
1147
|
+
} else if (this.peek("/")) {
|
|
1148
|
+
this.i += 1;
|
|
1149
|
+
left = [
|
|
1150
|
+
...left,
|
|
1151
|
+
...this.unary(),
|
|
1152
|
+
{
|
|
1153
|
+
kind: "binary",
|
|
1154
|
+
op: BinaryOp.Div
|
|
1155
|
+
}
|
|
1156
|
+
];
|
|
1157
|
+
} else return left;
|
|
1158
|
+
}
|
|
1159
|
+
unary() {
|
|
1160
|
+
if (this.eat("!")) return [...this.unary(), {
|
|
1161
|
+
kind: "unary",
|
|
1162
|
+
op: UnaryOp.Negate
|
|
1163
|
+
}];
|
|
1164
|
+
return this.methods(this.primary());
|
|
1165
|
+
}
|
|
1166
|
+
primary() {
|
|
1167
|
+
this.ws();
|
|
1168
|
+
if (this.peek("(")) {
|
|
1169
|
+
this.i += 1;
|
|
1170
|
+
const inner = this.expression();
|
|
1171
|
+
this.expect(")");
|
|
1172
|
+
return inner;
|
|
1173
|
+
}
|
|
1174
|
+
return [{
|
|
1175
|
+
kind: "value",
|
|
1176
|
+
value: this.term()
|
|
1177
|
+
}];
|
|
1178
|
+
}
|
|
1179
|
+
methods(target) {
|
|
1180
|
+
let out = target;
|
|
1181
|
+
while (this.peek(".")) {
|
|
1182
|
+
this.i += 1;
|
|
1183
|
+
if (this.peek("extern::")) {
|
|
1184
|
+
this.i += 8;
|
|
1185
|
+
const fn = this.name();
|
|
1186
|
+
this.expect("(");
|
|
1187
|
+
if (this.eat(")")) out = [...out, {
|
|
1188
|
+
kind: "unary",
|
|
1189
|
+
op: UnaryOp.Ffi,
|
|
1190
|
+
ffi: fn
|
|
1191
|
+
}];
|
|
1192
|
+
else {
|
|
1193
|
+
const arg = this.expression();
|
|
1194
|
+
this.expect(")");
|
|
1195
|
+
out = [
|
|
1196
|
+
...out,
|
|
1197
|
+
...arg,
|
|
1198
|
+
{
|
|
1199
|
+
kind: "binary",
|
|
1200
|
+
op: BinaryOp.Ffi,
|
|
1201
|
+
ffi: fn
|
|
1202
|
+
}
|
|
1203
|
+
];
|
|
1204
|
+
}
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
const method = this.name();
|
|
1208
|
+
this.expect("(");
|
|
1209
|
+
switch (method) {
|
|
1210
|
+
case "length":
|
|
1211
|
+
this.expect(")");
|
|
1212
|
+
out = [...out, {
|
|
1213
|
+
kind: "unary",
|
|
1214
|
+
op: UnaryOp.Length
|
|
1215
|
+
}];
|
|
1216
|
+
break;
|
|
1217
|
+
case "type":
|
|
1218
|
+
this.expect(")");
|
|
1219
|
+
out = [...out, {
|
|
1220
|
+
kind: "unary",
|
|
1221
|
+
op: UnaryOp.TypeOf
|
|
1222
|
+
}];
|
|
1223
|
+
break;
|
|
1224
|
+
case "any":
|
|
1225
|
+
case "all": {
|
|
1226
|
+
const closure = this.closure();
|
|
1227
|
+
this.expect(")");
|
|
1228
|
+
out = [
|
|
1229
|
+
...out,
|
|
1230
|
+
closure,
|
|
1231
|
+
{
|
|
1232
|
+
kind: "binary",
|
|
1233
|
+
op: method === "any" ? BinaryOp.Any : BinaryOp.All
|
|
1234
|
+
}
|
|
1235
|
+
];
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
case "try_or": {
|
|
1239
|
+
const fallback = this.expression();
|
|
1240
|
+
this.expect(")");
|
|
1241
|
+
out = [
|
|
1242
|
+
{
|
|
1243
|
+
kind: "closure",
|
|
1244
|
+
params: [],
|
|
1245
|
+
ops: out
|
|
1246
|
+
},
|
|
1247
|
+
...fallback,
|
|
1248
|
+
{
|
|
1249
|
+
kind: "binary",
|
|
1250
|
+
op: BinaryOp.TryOr
|
|
1251
|
+
}
|
|
1252
|
+
];
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
default: {
|
|
1256
|
+
const arg = this.expression();
|
|
1257
|
+
this.expect(")");
|
|
1258
|
+
const op = {
|
|
1259
|
+
contains: BinaryOp.Contains,
|
|
1260
|
+
starts_with: BinaryOp.Prefix,
|
|
1261
|
+
ends_with: BinaryOp.Suffix,
|
|
1262
|
+
matches: BinaryOp.Regex,
|
|
1263
|
+
intersection: BinaryOp.Intersection,
|
|
1264
|
+
union: BinaryOp.Union,
|
|
1265
|
+
get: BinaryOp.Get
|
|
1266
|
+
}[method];
|
|
1267
|
+
if (op === void 0) throw new ParseError(`unknown method ${method}`);
|
|
1268
|
+
out = [
|
|
1269
|
+
...out,
|
|
1270
|
+
...arg,
|
|
1271
|
+
{
|
|
1272
|
+
kind: "binary",
|
|
1273
|
+
op
|
|
1274
|
+
}
|
|
1275
|
+
];
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return out;
|
|
1280
|
+
}
|
|
1281
|
+
closure() {
|
|
1282
|
+
const params = [];
|
|
1283
|
+
this.ws();
|
|
1284
|
+
if (this.eat("(")) this.expect(")");
|
|
1285
|
+
else do {
|
|
1286
|
+
this.expect("$");
|
|
1287
|
+
params.push(this.varId(this.variableName()));
|
|
1288
|
+
} while (this.eat(","));
|
|
1289
|
+
this.expect("->");
|
|
1290
|
+
return {
|
|
1291
|
+
kind: "closure",
|
|
1292
|
+
params,
|
|
1293
|
+
ops: this.expression()
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
scopes() {
|
|
1297
|
+
const out = [];
|
|
1298
|
+
do {
|
|
1299
|
+
this.ws();
|
|
1300
|
+
if (this.keyword("authority")) out.push({ kind: "authority" });
|
|
1301
|
+
else if (this.keyword("previous")) out.push({ kind: "previous" });
|
|
1302
|
+
else {
|
|
1303
|
+
const alg = this.name();
|
|
1304
|
+
this.expect("/");
|
|
1305
|
+
const start = this.i;
|
|
1306
|
+
while (this.i < this.src.length && /[0-9a-fA-F]/.test(this.src[this.i])) this.i++;
|
|
1307
|
+
out.push({
|
|
1308
|
+
kind: "publicKey",
|
|
1309
|
+
key: `${alg}/${this.src.slice(start, this.i).toLowerCase()}`
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
} while (this.eat(","));
|
|
1313
|
+
return out;
|
|
1314
|
+
}
|
|
1315
|
+
/** body of a rule / check / policy: predicates, expressions, trusting */
|
|
1316
|
+
ruleBody() {
|
|
1317
|
+
const body = [];
|
|
1318
|
+
const expressions = [];
|
|
1319
|
+
let scopes = [];
|
|
1320
|
+
do {
|
|
1321
|
+
this.ws();
|
|
1322
|
+
if (this.keyword("trusting")) {
|
|
1323
|
+
scopes = this.scopes();
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
const save = this.i;
|
|
1327
|
+
const asPredicate = this.tryPredicate();
|
|
1328
|
+
if (asPredicate) body.push(asPredicate);
|
|
1329
|
+
else {
|
|
1330
|
+
this.i = save;
|
|
1331
|
+
expressions.push(this.expression());
|
|
1332
|
+
}
|
|
1333
|
+
this.ws();
|
|
1334
|
+
if (this.keyword("trusting")) {
|
|
1335
|
+
scopes = this.scopes();
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
} while (this.eat(","));
|
|
1339
|
+
return {
|
|
1340
|
+
body,
|
|
1341
|
+
expressions,
|
|
1342
|
+
scopes
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
/** a predicate is only a predicate if nothing operator-like follows it */
|
|
1346
|
+
tryPredicate() {
|
|
1347
|
+
const save = this.i;
|
|
1348
|
+
try {
|
|
1349
|
+
this.ws();
|
|
1350
|
+
if (!NAME_START.test(this.src[this.i] ?? "")) return null;
|
|
1351
|
+
const p = this.predicate();
|
|
1352
|
+
this.ws();
|
|
1353
|
+
const rest = this.src.slice(this.i);
|
|
1354
|
+
if (rest === "" || /^[,;)]/.test(rest) || /^(or|trusting)\b/.test(rest)) return p;
|
|
1355
|
+
this.i = save;
|
|
1356
|
+
return null;
|
|
1357
|
+
} catch {
|
|
1358
|
+
this.i = save;
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
queries() {
|
|
1363
|
+
const out = [];
|
|
1364
|
+
do {
|
|
1365
|
+
const { body, expressions, scopes } = this.ruleBody();
|
|
1366
|
+
out.push({
|
|
1367
|
+
head: {
|
|
1368
|
+
name: "query",
|
|
1369
|
+
terms: []
|
|
1370
|
+
},
|
|
1371
|
+
body,
|
|
1372
|
+
expressions,
|
|
1373
|
+
scopes
|
|
1374
|
+
});
|
|
1375
|
+
} while (this.keyword("or"));
|
|
1376
|
+
return out;
|
|
1377
|
+
}
|
|
1378
|
+
statement() {
|
|
1379
|
+
this.ws();
|
|
1380
|
+
if (this.keyword("check")) {
|
|
1381
|
+
let kind;
|
|
1382
|
+
if (this.keyword("if")) kind = "one";
|
|
1383
|
+
else if (this.keyword("all")) kind = "all";
|
|
1384
|
+
else throw new ParseError("expected \"if\" or \"all\" after \"check\"");
|
|
1385
|
+
return {
|
|
1386
|
+
k: "check",
|
|
1387
|
+
check: {
|
|
1388
|
+
queries: this.queries(),
|
|
1389
|
+
kind
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
if (this.keyword("reject")) {
|
|
1394
|
+
if (!this.keyword("if")) throw new ParseError("expected \"if\" after \"reject\"");
|
|
1395
|
+
return {
|
|
1396
|
+
k: "check",
|
|
1397
|
+
check: {
|
|
1398
|
+
queries: this.queries(),
|
|
1399
|
+
kind: "reject"
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
if (this.keyword("allow")) {
|
|
1404
|
+
if (!this.keyword("if")) throw new ParseError("expected \"if\" after \"allow\"");
|
|
1405
|
+
return {
|
|
1406
|
+
k: "policy",
|
|
1407
|
+
kind: "allow",
|
|
1408
|
+
queries: this.queries()
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
if (this.keyword("deny")) {
|
|
1412
|
+
if (!this.keyword("if")) throw new ParseError("expected \"if\" after \"deny\"");
|
|
1413
|
+
return {
|
|
1414
|
+
k: "policy",
|
|
1415
|
+
kind: "deny",
|
|
1416
|
+
queries: this.queries()
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
if (this.keyword("trusting")) return {
|
|
1420
|
+
k: "blockScope",
|
|
1421
|
+
scopes: this.scopes()
|
|
1422
|
+
};
|
|
1423
|
+
const head = this.predicate();
|
|
1424
|
+
this.ws();
|
|
1425
|
+
if (this.eat("<-")) {
|
|
1426
|
+
const { body, expressions, scopes } = this.ruleBody();
|
|
1427
|
+
return {
|
|
1428
|
+
k: "rule",
|
|
1429
|
+
rule: {
|
|
1430
|
+
head,
|
|
1431
|
+
body,
|
|
1432
|
+
expressions,
|
|
1433
|
+
scopes
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
if (head.terms.some((t) => t.t === "var")) throw new ParseError("a fact cannot contain variables");
|
|
1438
|
+
return {
|
|
1439
|
+
k: "fact",
|
|
1440
|
+
fact: { predicate: head }
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
parse() {
|
|
1444
|
+
const out = [];
|
|
1445
|
+
while (!this.eof) {
|
|
1446
|
+
out.push(this.statement());
|
|
1447
|
+
this.ws();
|
|
1448
|
+
this.expect(";");
|
|
1449
|
+
}
|
|
1450
|
+
return out;
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
//#endregion
|
|
1454
|
+
//#region src/print.ts
|
|
1455
|
+
/** Datalog pretty-printer (used for error messages and world snapshots). */
|
|
1456
|
+
const hex$1 = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
1457
|
+
/** Datalog string literal: only the quote and the backslash are escaped.
|
|
1458
|
+
* Control characters are emitted raw, unlike JSON.stringify. */
|
|
1459
|
+
const quote = (v) => `"${v.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
1460
|
+
function printTerm(t, varName) {
|
|
1461
|
+
switch (t.t) {
|
|
1462
|
+
case "var": return `$${varName(t.v)}`;
|
|
1463
|
+
case "int": return t.v.toString();
|
|
1464
|
+
case "str": return quote(t.v);
|
|
1465
|
+
case "date": return (/* @__PURE__ */ new Date(Number(t.v) * 1e3)).toISOString().replace(".000Z", "Z");
|
|
1466
|
+
case "bytes": return `hex:${hex$1(t.v)}`;
|
|
1467
|
+
case "bool": return t.v ? "true" : "false";
|
|
1468
|
+
case "null": return "null";
|
|
1469
|
+
case "set": return t.v.length === 0 ? "{,}" : `{${t.v.map((x) => printTerm(x, varName)).join(", ")}}`;
|
|
1470
|
+
case "array": return `[${t.v.map((x) => printTerm(x, varName)).join(", ")}]`;
|
|
1471
|
+
case "map": return t.v.length === 0 ? "{}" : `{${t.v.map(([k, v]) => `${k.t === "int" ? k.v : quote(k.v)}: ${printTerm(v, varName)}`).join(", ")}}`;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
const printPredicate = (p, varName) => `${p.name}(${p.terms.map((t) => printTerm(t, varName)).join(", ")})`;
|
|
1475
|
+
const UNARY = {
|
|
1476
|
+
[UnaryOp.Negate]: (a) => `!${a}`,
|
|
1477
|
+
[UnaryOp.Parens]: (a) => `(${a})`,
|
|
1478
|
+
[UnaryOp.Length]: (a) => `${a}.length()`,
|
|
1479
|
+
[UnaryOp.TypeOf]: (a) => `${a}.type()`
|
|
1480
|
+
};
|
|
1481
|
+
const BINARY = {
|
|
1482
|
+
[BinaryOp.LessThan]: (a, b) => `${a} < ${b}`,
|
|
1483
|
+
[BinaryOp.GreaterThan]: (a, b) => `${a} > ${b}`,
|
|
1484
|
+
[BinaryOp.LessOrEqual]: (a, b) => `${a} <= ${b}`,
|
|
1485
|
+
[BinaryOp.GreaterOrEqual]: (a, b) => `${a} >= ${b}`,
|
|
1486
|
+
[BinaryOp.Equal]: (a, b) => `${a} === ${b}`,
|
|
1487
|
+
[BinaryOp.NotEqual]: (a, b) => `${a} !== ${b}`,
|
|
1488
|
+
[BinaryOp.HeterogeneousEqual]: (a, b) => `${a} == ${b}`,
|
|
1489
|
+
[BinaryOp.HeterogeneousNotEqual]: (a, b) => `${a} != ${b}`,
|
|
1490
|
+
[BinaryOp.Contains]: (a, b) => `${a}.contains(${b})`,
|
|
1491
|
+
[BinaryOp.Prefix]: (a, b) => `${a}.starts_with(${b})`,
|
|
1492
|
+
[BinaryOp.Suffix]: (a, b) => `${a}.ends_with(${b})`,
|
|
1493
|
+
[BinaryOp.Regex]: (a, b) => `${a}.matches(${b})`,
|
|
1494
|
+
[BinaryOp.Add]: (a, b) => `${a} + ${b}`,
|
|
1495
|
+
[BinaryOp.Sub]: (a, b) => `${a} - ${b}`,
|
|
1496
|
+
[BinaryOp.Mul]: (a, b) => `${a} * ${b}`,
|
|
1497
|
+
[BinaryOp.Div]: (a, b) => `${a} / ${b}`,
|
|
1498
|
+
[BinaryOp.And]: (a, b) => `${a} &&! ${b}`,
|
|
1499
|
+
[BinaryOp.Or]: (a, b) => `${a} ||! ${b}`,
|
|
1500
|
+
[BinaryOp.Intersection]: (a, b) => `${a}.intersection(${b})`,
|
|
1501
|
+
[BinaryOp.Union]: (a, b) => `${a}.union(${b})`,
|
|
1502
|
+
[BinaryOp.BitwiseAnd]: (a, b) => `${a} & ${b}`,
|
|
1503
|
+
[BinaryOp.BitwiseOr]: (a, b) => `${a} | ${b}`,
|
|
1504
|
+
[BinaryOp.BitwiseXor]: (a, b) => `${a} ^ ${b}`,
|
|
1505
|
+
[BinaryOp.LazyAnd]: (a, b) => `${a} && ${b}`,
|
|
1506
|
+
[BinaryOp.LazyOr]: (a, b) => `${a} || ${b}`,
|
|
1507
|
+
[BinaryOp.All]: (a, b) => `${a}.all(${b})`,
|
|
1508
|
+
[BinaryOp.Any]: (a, b) => `${a}.any(${b})`,
|
|
1509
|
+
[BinaryOp.Get]: (a, b) => `${a}.get(${b})`,
|
|
1510
|
+
[BinaryOp.TryOr]: (a, b) => `${a}.try_or(${b})`
|
|
1511
|
+
};
|
|
1512
|
+
function printExpression(ops, varName) {
|
|
1513
|
+
const stack = [];
|
|
1514
|
+
for (const op of ops) if (op.kind === "value") stack.push(printTerm(op.value, varName));
|
|
1515
|
+
else if (op.kind === "closure") stack.push(`${op.params.map((p) => `$${varName(p)}`).join(", ")}${op.params.length ? " -> " : ""}${printExpression(op.ops, varName)}`);
|
|
1516
|
+
else if (op.kind === "unary") {
|
|
1517
|
+
const a = stack.pop() ?? "";
|
|
1518
|
+
stack.push(op.ffi ? `${a}.extern::${op.ffi}()` : UNARY[op.op]?.(a) ?? a);
|
|
1519
|
+
} else {
|
|
1520
|
+
const b = stack.pop() ?? "";
|
|
1521
|
+
const a = stack.pop() ?? "";
|
|
1522
|
+
stack.push(op.ffi ? `${a}.extern::${op.ffi}(${b})` : BINARY[op.op]?.(a, b) ?? `${a} ? ${b}`);
|
|
1523
|
+
}
|
|
1524
|
+
return stack.join(" ");
|
|
1525
|
+
}
|
|
1526
|
+
function printScopes(scopes) {
|
|
1527
|
+
if (scopes.length === 0) return "";
|
|
1528
|
+
return ` trusting ${scopes.map((s) => s.kind === "authority" ? "authority" : s.kind === "previous" ? "previous" : s.key).join(", ")}`;
|
|
1529
|
+
}
|
|
1530
|
+
/** the body of a rule, check or policy: predicates, then expressions */
|
|
1531
|
+
function printQuery(r, varName) {
|
|
1532
|
+
return [...r.body.map((p) => printPredicate(p, varName)), ...r.expressions.map((e) => printExpression(e, varName))].join(", ") + printScopes(r.scopes);
|
|
1533
|
+
}
|
|
1534
|
+
function printCheck(c, varName) {
|
|
1535
|
+
return (c.kind === "all" ? "check all " : c.kind === "reject" ? "reject if " : "check if ") + c.queries.map((q) => printQuery(q, varName)).join(" or ");
|
|
1536
|
+
}
|
|
1537
|
+
function printPolicy(kind, queries, varName) {
|
|
1538
|
+
return `${kind} if ` + queries.map((q) => printQuery(q, varName)).join(" or ");
|
|
1539
|
+
}
|
|
1540
|
+
function printRule(r, varName) {
|
|
1541
|
+
const parts = [...r.body.map((p) => printPredicate(p, varName)), ...r.expressions.map((e) => printExpression(e, varName))];
|
|
1542
|
+
return `${printPredicate(r.head, varName)} <- ${parts.join(", ")}` + printScopes(r.scopes);
|
|
1543
|
+
}
|
|
1544
|
+
//#endregion
|
|
1545
|
+
//#region src/proto.ts
|
|
1546
|
+
/**
|
|
1547
|
+
* Strict proto2 codec for the Biscuit schema.
|
|
1548
|
+
*
|
|
1549
|
+
* Written against src/schema.proto. Unlike a generated codec it: decodes
|
|
1550
|
+
* int64/uint64 as BigInt, tracks proto2 `optional` presence, validates UTF-8
|
|
1551
|
+
* strictly, and rejects unknown fields, wrong wire types and missing
|
|
1552
|
+
* `required` fields.
|
|
1553
|
+
*/
|
|
1554
|
+
var ProtoError = class extends Error {};
|
|
1555
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
1556
|
+
const utf8enc = new TextEncoder();
|
|
1557
|
+
var Reader = class Reader {
|
|
1558
|
+
buf;
|
|
1559
|
+
end;
|
|
1560
|
+
pos;
|
|
1561
|
+
constructor(buf, pos = 0, end = buf.length) {
|
|
1562
|
+
this.buf = buf;
|
|
1563
|
+
this.end = end;
|
|
1564
|
+
this.pos = pos;
|
|
1565
|
+
}
|
|
1566
|
+
get done() {
|
|
1567
|
+
return this.pos >= this.end;
|
|
1568
|
+
}
|
|
1569
|
+
byte() {
|
|
1570
|
+
if (this.pos >= this.end) throw new ProtoError("unexpected end of buffer");
|
|
1571
|
+
return this.buf[this.pos++];
|
|
1572
|
+
}
|
|
1573
|
+
varint() {
|
|
1574
|
+
let result = 0n;
|
|
1575
|
+
let shift = 0n;
|
|
1576
|
+
let b;
|
|
1577
|
+
do {
|
|
1578
|
+
if (shift > 63n) throw new ProtoError("varint overflow");
|
|
1579
|
+
b = this.byte();
|
|
1580
|
+
result |= BigInt(b & 127) << shift;
|
|
1581
|
+
shift += 7n;
|
|
1582
|
+
} while (b & 128);
|
|
1583
|
+
return BigInt.asUintN(64, result);
|
|
1584
|
+
}
|
|
1585
|
+
varintNum() {
|
|
1586
|
+
const v = this.varint();
|
|
1587
|
+
if (v > BigInt(Number.MAX_SAFE_INTEGER)) throw new ProtoError("varint too large for a number");
|
|
1588
|
+
return Number(v);
|
|
1589
|
+
}
|
|
1590
|
+
bytes() {
|
|
1591
|
+
const len = this.varintNum();
|
|
1592
|
+
if (this.pos + len > this.end) throw new ProtoError("length-delimited field overruns buffer");
|
|
1593
|
+
const out = this.buf.subarray(this.pos, this.pos + len);
|
|
1594
|
+
this.pos += len;
|
|
1595
|
+
return out;
|
|
1596
|
+
}
|
|
1597
|
+
string() {
|
|
1598
|
+
try {
|
|
1599
|
+
return utf8.decode(this.bytes());
|
|
1600
|
+
} catch {
|
|
1601
|
+
throw new ProtoError("invalid UTF-8 in string field");
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
sub() {
|
|
1605
|
+
const b = this.bytes();
|
|
1606
|
+
return new Reader(b, 0, b.length);
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
function readFields(r, handler) {
|
|
1610
|
+
while (!r.done) {
|
|
1611
|
+
const tag = r.varintNum();
|
|
1612
|
+
const field = tag >> 3;
|
|
1613
|
+
const wire = tag & 7;
|
|
1614
|
+
if (field === 0) throw new ProtoError("invalid field number 0");
|
|
1615
|
+
handler(field, wire, r);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function expect(wire, want, field) {
|
|
1619
|
+
if (wire !== want) throw new ProtoError(`wrong wire type ${wire} for field ${field}`);
|
|
1620
|
+
}
|
|
1621
|
+
function required(v, name) {
|
|
1622
|
+
if (v === void 0) throw new ProtoError(`missing required field ${name}`);
|
|
1623
|
+
return v;
|
|
1624
|
+
}
|
|
1625
|
+
var Writer = class Writer {
|
|
1626
|
+
buf = /* @__PURE__ */ new Uint8Array(256);
|
|
1627
|
+
pos = 0;
|
|
1628
|
+
grow(n) {
|
|
1629
|
+
if (this.pos + n <= this.buf.length) return;
|
|
1630
|
+
let len = this.buf.length;
|
|
1631
|
+
while (len < this.pos + n) len *= 2;
|
|
1632
|
+
const next = new Uint8Array(len);
|
|
1633
|
+
next.set(this.buf.subarray(0, this.pos));
|
|
1634
|
+
this.buf = next;
|
|
1635
|
+
}
|
|
1636
|
+
varint(v) {
|
|
1637
|
+
let x = BigInt.asUintN(64, BigInt(v));
|
|
1638
|
+
this.grow(10);
|
|
1639
|
+
do {
|
|
1640
|
+
let b = Number(x & 127n);
|
|
1641
|
+
x >>= 7n;
|
|
1642
|
+
if (x) b |= 128;
|
|
1643
|
+
this.buf[this.pos++] = b;
|
|
1644
|
+
} while (x);
|
|
1645
|
+
}
|
|
1646
|
+
tag(field, wire) {
|
|
1647
|
+
this.varint(field << 3 | wire);
|
|
1648
|
+
}
|
|
1649
|
+
varintField(field, v) {
|
|
1650
|
+
this.tag(field, 0);
|
|
1651
|
+
this.varint(v);
|
|
1652
|
+
}
|
|
1653
|
+
bytesField(field, v) {
|
|
1654
|
+
this.tag(field, 2);
|
|
1655
|
+
this.varint(v.length);
|
|
1656
|
+
this.grow(v.length);
|
|
1657
|
+
this.buf.set(v, this.pos);
|
|
1658
|
+
this.pos += v.length;
|
|
1659
|
+
}
|
|
1660
|
+
stringField(field, v) {
|
|
1661
|
+
this.bytesField(field, utf8enc.encode(v));
|
|
1662
|
+
}
|
|
1663
|
+
messageField(field, write, v) {
|
|
1664
|
+
const w = new Writer();
|
|
1665
|
+
write(v, w);
|
|
1666
|
+
this.bytesField(field, w.finish());
|
|
1667
|
+
}
|
|
1668
|
+
finish() {
|
|
1669
|
+
return this.buf.slice(0, this.pos);
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
function readPublicKey(r) {
|
|
1673
|
+
let algorithm;
|
|
1674
|
+
let key;
|
|
1675
|
+
readFields(r, (f, w, rr) => {
|
|
1676
|
+
if (f === 1) {
|
|
1677
|
+
expect(w, 0, f);
|
|
1678
|
+
algorithm = rr.varintNum();
|
|
1679
|
+
} else if (f === 2) {
|
|
1680
|
+
expect(w, 2, f);
|
|
1681
|
+
key = rr.bytes();
|
|
1682
|
+
} else throw new ProtoError(`unknown field ${f} in PublicKey`);
|
|
1683
|
+
});
|
|
1684
|
+
const alg = required(algorithm, "PublicKey.algorithm");
|
|
1685
|
+
if (alg !== 0 && alg !== 1) throw new ProtoError(`unknown signature algorithm ${alg}`);
|
|
1686
|
+
return {
|
|
1687
|
+
algorithm: alg,
|
|
1688
|
+
key: required(key, "PublicKey.key")
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
function readExternalSignature(r) {
|
|
1692
|
+
let signature;
|
|
1693
|
+
let publicKey;
|
|
1694
|
+
readFields(r, (f, w, rr) => {
|
|
1695
|
+
if (f === 1) {
|
|
1696
|
+
expect(w, 2, f);
|
|
1697
|
+
signature = rr.bytes();
|
|
1698
|
+
} else if (f === 2) {
|
|
1699
|
+
expect(w, 2, f);
|
|
1700
|
+
publicKey = readPublicKey(rr.sub());
|
|
1701
|
+
} else throw new ProtoError(`unknown field ${f} in ExternalSignature`);
|
|
1702
|
+
});
|
|
1703
|
+
return {
|
|
1704
|
+
signature: required(signature, "ExternalSignature.signature"),
|
|
1705
|
+
publicKey: required(publicKey, "ExternalSignature.publicKey")
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function readSignedBlock(r) {
|
|
1709
|
+
const out = {};
|
|
1710
|
+
readFields(r, (f, w, rr) => {
|
|
1711
|
+
if (f === 1) {
|
|
1712
|
+
expect(w, 2, f);
|
|
1713
|
+
out.block = rr.bytes();
|
|
1714
|
+
} else if (f === 2) {
|
|
1715
|
+
expect(w, 2, f);
|
|
1716
|
+
out.nextKey = readPublicKey(rr.sub());
|
|
1717
|
+
} else if (f === 3) {
|
|
1718
|
+
expect(w, 2, f);
|
|
1719
|
+
out.signature = rr.bytes();
|
|
1720
|
+
} else if (f === 4) {
|
|
1721
|
+
expect(w, 2, f);
|
|
1722
|
+
out.externalSignature = readExternalSignature(rr.sub());
|
|
1723
|
+
} else if (f === 5) {
|
|
1724
|
+
expect(w, 0, f);
|
|
1725
|
+
out.version = rr.varintNum();
|
|
1726
|
+
} else throw new ProtoError(`unknown field ${f} in SignedBlock`);
|
|
1727
|
+
});
|
|
1728
|
+
return {
|
|
1729
|
+
block: required(out.block, "SignedBlock.block"),
|
|
1730
|
+
nextKey: required(out.nextKey, "SignedBlock.nextKey"),
|
|
1731
|
+
signature: required(out.signature, "SignedBlock.signature"),
|
|
1732
|
+
externalSignature: out.externalSignature,
|
|
1733
|
+
version: out.version
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
function decodeBiscuit(buf) {
|
|
1737
|
+
const r = new Reader(buf);
|
|
1738
|
+
let rootKeyId;
|
|
1739
|
+
let authority;
|
|
1740
|
+
const blocks = [];
|
|
1741
|
+
let proof;
|
|
1742
|
+
readFields(r, (f, w, rr) => {
|
|
1743
|
+
if (f === 1) {
|
|
1744
|
+
expect(w, 0, f);
|
|
1745
|
+
rootKeyId = rr.varintNum();
|
|
1746
|
+
} else if (f === 2) {
|
|
1747
|
+
expect(w, 2, f);
|
|
1748
|
+
authority = readSignedBlock(rr.sub());
|
|
1749
|
+
} else if (f === 3) {
|
|
1750
|
+
expect(w, 2, f);
|
|
1751
|
+
blocks.push(readSignedBlock(rr.sub()));
|
|
1752
|
+
} else if (f === 4) {
|
|
1753
|
+
expect(w, 2, f);
|
|
1754
|
+
readFields(rr.sub(), (pf, pw, pr) => {
|
|
1755
|
+
if (pf === 1) {
|
|
1756
|
+
expect(pw, 2, pf);
|
|
1757
|
+
proof = {
|
|
1758
|
+
kind: "nextSecret",
|
|
1759
|
+
value: pr.bytes()
|
|
1760
|
+
};
|
|
1761
|
+
} else if (pf === 2) {
|
|
1762
|
+
expect(pw, 2, pf);
|
|
1763
|
+
proof = {
|
|
1764
|
+
kind: "finalSignature",
|
|
1765
|
+
value: pr.bytes()
|
|
1766
|
+
};
|
|
1767
|
+
} else throw new ProtoError(`unknown field ${pf} in Proof`);
|
|
1768
|
+
});
|
|
1769
|
+
} else throw new ProtoError(`unknown field ${f} in Biscuit`);
|
|
1770
|
+
});
|
|
1771
|
+
return {
|
|
1772
|
+
rootKeyId,
|
|
1773
|
+
authority: required(authority, "Biscuit.authority"),
|
|
1774
|
+
blocks,
|
|
1775
|
+
proof: required(proof, "Biscuit.proof")
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
function readTerm(r) {
|
|
1779
|
+
let out;
|
|
1780
|
+
readFields(r, (f, w, rr) => {
|
|
1781
|
+
switch (f) {
|
|
1782
|
+
case 1:
|
|
1783
|
+
expect(w, 0, f);
|
|
1784
|
+
out = {
|
|
1785
|
+
kind: "variable",
|
|
1786
|
+
value: rr.varintNum()
|
|
1787
|
+
};
|
|
1788
|
+
break;
|
|
1789
|
+
case 2:
|
|
1790
|
+
expect(w, 0, f);
|
|
1791
|
+
out = {
|
|
1792
|
+
kind: "integer",
|
|
1793
|
+
value: BigInt.asIntN(64, rr.varint())
|
|
1794
|
+
};
|
|
1795
|
+
break;
|
|
1796
|
+
case 3:
|
|
1797
|
+
expect(w, 0, f);
|
|
1798
|
+
out = {
|
|
1799
|
+
kind: "string",
|
|
1800
|
+
value: rr.varintNum()
|
|
1801
|
+
};
|
|
1802
|
+
break;
|
|
1803
|
+
case 4:
|
|
1804
|
+
expect(w, 0, f);
|
|
1805
|
+
out = {
|
|
1806
|
+
kind: "date",
|
|
1807
|
+
value: rr.varint()
|
|
1808
|
+
};
|
|
1809
|
+
break;
|
|
1810
|
+
case 5:
|
|
1811
|
+
expect(w, 2, f);
|
|
1812
|
+
out = {
|
|
1813
|
+
kind: "bytes",
|
|
1814
|
+
value: rr.bytes()
|
|
1815
|
+
};
|
|
1816
|
+
break;
|
|
1817
|
+
case 6:
|
|
1818
|
+
expect(w, 0, f);
|
|
1819
|
+
out = {
|
|
1820
|
+
kind: "bool",
|
|
1821
|
+
value: rr.varintNum() !== 0
|
|
1822
|
+
};
|
|
1823
|
+
break;
|
|
1824
|
+
case 7: {
|
|
1825
|
+
expect(w, 2, f);
|
|
1826
|
+
const set = [];
|
|
1827
|
+
readFields(rr.sub(), (sf, sw, sr) => {
|
|
1828
|
+
if (sf !== 1) throw new ProtoError(`unknown field ${sf} in TermSet`);
|
|
1829
|
+
expect(sw, 2, sf);
|
|
1830
|
+
set.push(readTerm(sr.sub()));
|
|
1831
|
+
});
|
|
1832
|
+
out = {
|
|
1833
|
+
kind: "set",
|
|
1834
|
+
value: set
|
|
1835
|
+
};
|
|
1836
|
+
break;
|
|
1837
|
+
}
|
|
1838
|
+
case 8:
|
|
1839
|
+
expect(w, 2, f);
|
|
1840
|
+
rr.bytes();
|
|
1841
|
+
out = { kind: "null" };
|
|
1842
|
+
break;
|
|
1843
|
+
case 9: {
|
|
1844
|
+
expect(w, 2, f);
|
|
1845
|
+
const arr = [];
|
|
1846
|
+
readFields(rr.sub(), (af, aw, ar) => {
|
|
1847
|
+
if (af !== 1) throw new ProtoError(`unknown field ${af} in Array`);
|
|
1848
|
+
expect(aw, 2, af);
|
|
1849
|
+
arr.push(readTerm(ar.sub()));
|
|
1850
|
+
});
|
|
1851
|
+
out = {
|
|
1852
|
+
kind: "array",
|
|
1853
|
+
value: arr
|
|
1854
|
+
};
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
case 10: {
|
|
1858
|
+
expect(w, 2, f);
|
|
1859
|
+
const entries = [];
|
|
1860
|
+
readFields(rr.sub(), (mf, mw, mr) => {
|
|
1861
|
+
if (mf !== 1) throw new ProtoError(`unknown field ${mf} in Map`);
|
|
1862
|
+
expect(mw, 2, mf);
|
|
1863
|
+
entries.push(readMapEntry(mr.sub()));
|
|
1864
|
+
});
|
|
1865
|
+
out = {
|
|
1866
|
+
kind: "map",
|
|
1867
|
+
value: entries
|
|
1868
|
+
};
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
default: throw new ProtoError(`unknown field ${f} in Term`);
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
return required(out, "Term.Content");
|
|
1875
|
+
}
|
|
1876
|
+
function readMapEntry(r) {
|
|
1877
|
+
let key;
|
|
1878
|
+
let value;
|
|
1879
|
+
readFields(r, (f, w, rr) => {
|
|
1880
|
+
if (f === 1) {
|
|
1881
|
+
expect(w, 2, f);
|
|
1882
|
+
readFields(rr.sub(), (kf, kw, kr) => {
|
|
1883
|
+
if (kf === 1) {
|
|
1884
|
+
expect(kw, 0, kf);
|
|
1885
|
+
key = {
|
|
1886
|
+
kind: "integer",
|
|
1887
|
+
value: BigInt.asIntN(64, kr.varint())
|
|
1888
|
+
};
|
|
1889
|
+
} else if (kf === 2) {
|
|
1890
|
+
expect(kw, 0, kf);
|
|
1891
|
+
key = {
|
|
1892
|
+
kind: "string",
|
|
1893
|
+
value: kr.varintNum()
|
|
1894
|
+
};
|
|
1895
|
+
} else throw new ProtoError(`unknown field ${kf} in MapKey`);
|
|
1896
|
+
});
|
|
1897
|
+
} else if (f === 2) {
|
|
1898
|
+
expect(w, 2, f);
|
|
1899
|
+
value = readTerm(rr.sub());
|
|
1900
|
+
} else throw new ProtoError(`unknown field ${f} in MapEntry`);
|
|
1901
|
+
});
|
|
1902
|
+
return {
|
|
1903
|
+
key: required(key, "MapEntry.key"),
|
|
1904
|
+
value: required(value, "MapEntry.value")
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
function readPredicate(r) {
|
|
1908
|
+
let name;
|
|
1909
|
+
const terms = [];
|
|
1910
|
+
readFields(r, (f, w, rr) => {
|
|
1911
|
+
if (f === 1) {
|
|
1912
|
+
expect(w, 0, f);
|
|
1913
|
+
name = rr.varintNum();
|
|
1914
|
+
} else if (f === 2) {
|
|
1915
|
+
expect(w, 2, f);
|
|
1916
|
+
terms.push(readTerm(rr.sub()));
|
|
1917
|
+
} else throw new ProtoError(`unknown field ${f} in Predicate`);
|
|
1918
|
+
});
|
|
1919
|
+
return {
|
|
1920
|
+
name: required(name, "Predicate.name"),
|
|
1921
|
+
terms
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
function readOp(r) {
|
|
1925
|
+
let out;
|
|
1926
|
+
readFields(r, (f, w, rr) => {
|
|
1927
|
+
if (f === 1) {
|
|
1928
|
+
expect(w, 2, f);
|
|
1929
|
+
out = {
|
|
1930
|
+
kind: "value",
|
|
1931
|
+
value: readTerm(rr.sub())
|
|
1932
|
+
};
|
|
1933
|
+
} else if (f === 2 || f === 3) {
|
|
1934
|
+
expect(w, 2, f);
|
|
1935
|
+
let op;
|
|
1936
|
+
let ffiName;
|
|
1937
|
+
readFields(rr.sub(), (of_, ow, or_) => {
|
|
1938
|
+
if (of_ === 1) {
|
|
1939
|
+
expect(ow, 0, of_);
|
|
1940
|
+
op = or_.varintNum();
|
|
1941
|
+
} else if (of_ === 2) {
|
|
1942
|
+
expect(ow, 0, of_);
|
|
1943
|
+
ffiName = or_.varintNum();
|
|
1944
|
+
} else throw new ProtoError(`unknown field ${of_} in Op`);
|
|
1945
|
+
});
|
|
1946
|
+
out = {
|
|
1947
|
+
kind: f === 2 ? "unary" : "binary",
|
|
1948
|
+
op: required(op, "Op.kind"),
|
|
1949
|
+
ffiName
|
|
1950
|
+
};
|
|
1951
|
+
} else if (f === 4) {
|
|
1952
|
+
expect(w, 2, f);
|
|
1953
|
+
const params = [];
|
|
1954
|
+
const ops = [];
|
|
1955
|
+
readFields(rr.sub(), (cf, cw, cr) => {
|
|
1956
|
+
if (cf === 1) {
|
|
1957
|
+
if (cw === 0) params.push(cr.varintNum());
|
|
1958
|
+
else if (cw === 2) {
|
|
1959
|
+
const packed = cr.sub();
|
|
1960
|
+
while (!packed.done) params.push(packed.varintNum());
|
|
1961
|
+
} else throw new ProtoError("bad wire type for OpClosure.params");
|
|
1962
|
+
} else if (cf === 2) {
|
|
1963
|
+
expect(cw, 2, cf);
|
|
1964
|
+
ops.push(readOp(cr.sub()));
|
|
1965
|
+
} else throw new ProtoError(`unknown field ${cf} in OpClosure`);
|
|
1966
|
+
});
|
|
1967
|
+
out = {
|
|
1968
|
+
kind: "closure",
|
|
1969
|
+
params,
|
|
1970
|
+
ops
|
|
1971
|
+
};
|
|
1972
|
+
} else throw new ProtoError(`unknown field ${f} in Op`);
|
|
1973
|
+
});
|
|
1974
|
+
return required(out, "Op.Content");
|
|
1975
|
+
}
|
|
1976
|
+
function readScope(r) {
|
|
1977
|
+
let out;
|
|
1978
|
+
readFields(r, (f, w, rr) => {
|
|
1979
|
+
if (f === 1) {
|
|
1980
|
+
expect(w, 0, f);
|
|
1981
|
+
const v = rr.varintNum();
|
|
1982
|
+
if (v !== 0 && v !== 1) throw new ProtoError(`unknown scope type ${v}`);
|
|
1983
|
+
out = {
|
|
1984
|
+
kind: "type",
|
|
1985
|
+
value: v
|
|
1986
|
+
};
|
|
1987
|
+
} else if (f === 2) {
|
|
1988
|
+
expect(w, 0, f);
|
|
1989
|
+
out = {
|
|
1990
|
+
kind: "publicKey",
|
|
1991
|
+
value: Number(BigInt.asIntN(64, rr.varint()))
|
|
1992
|
+
};
|
|
1993
|
+
} else throw new ProtoError(`unknown field ${f} in Scope`);
|
|
1994
|
+
});
|
|
1995
|
+
return required(out, "Scope.Content");
|
|
1996
|
+
}
|
|
1997
|
+
function readRule(r) {
|
|
1998
|
+
let head;
|
|
1999
|
+
const body = [];
|
|
2000
|
+
const expressions = [];
|
|
2001
|
+
const scope = [];
|
|
2002
|
+
readFields(r, (f, w, rr) => {
|
|
2003
|
+
if (f === 1) {
|
|
2004
|
+
expect(w, 2, f);
|
|
2005
|
+
head = readPredicate(rr.sub());
|
|
2006
|
+
} else if (f === 2) {
|
|
2007
|
+
expect(w, 2, f);
|
|
2008
|
+
body.push(readPredicate(rr.sub()));
|
|
2009
|
+
} else if (f === 3) {
|
|
2010
|
+
expect(w, 2, f);
|
|
2011
|
+
const ops = [];
|
|
2012
|
+
readFields(rr.sub(), (ef, ew, er) => {
|
|
2013
|
+
if (ef !== 1) throw new ProtoError(`unknown field ${ef} in Expression`);
|
|
2014
|
+
expect(ew, 2, ef);
|
|
2015
|
+
ops.push(readOp(er.sub()));
|
|
2016
|
+
});
|
|
2017
|
+
expressions.push(ops);
|
|
2018
|
+
} else if (f === 4) {
|
|
2019
|
+
expect(w, 2, f);
|
|
2020
|
+
scope.push(readScope(rr.sub()));
|
|
2021
|
+
} else throw new ProtoError(`unknown field ${f} in Rule`);
|
|
2022
|
+
});
|
|
2023
|
+
return {
|
|
2024
|
+
head: required(head, "Rule.head"),
|
|
2025
|
+
body,
|
|
2026
|
+
expressions,
|
|
2027
|
+
scope
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
function decodeBlock(buf) {
|
|
2031
|
+
const out = {
|
|
2032
|
+
symbols: [],
|
|
2033
|
+
facts: [],
|
|
2034
|
+
rules: [],
|
|
2035
|
+
checks: [],
|
|
2036
|
+
scope: [],
|
|
2037
|
+
publicKeys: []
|
|
2038
|
+
};
|
|
2039
|
+
readFields(new Reader(buf), (f, w, rr) => {
|
|
2040
|
+
switch (f) {
|
|
2041
|
+
case 1:
|
|
2042
|
+
expect(w, 2, f);
|
|
2043
|
+
out.symbols.push(rr.string());
|
|
2044
|
+
break;
|
|
2045
|
+
case 2:
|
|
2046
|
+
expect(w, 2, f);
|
|
2047
|
+
out.context = rr.string();
|
|
2048
|
+
break;
|
|
2049
|
+
case 3:
|
|
2050
|
+
expect(w, 0, f);
|
|
2051
|
+
out.version = rr.varintNum();
|
|
2052
|
+
break;
|
|
2053
|
+
case 4: {
|
|
2054
|
+
expect(w, 2, f);
|
|
2055
|
+
let pred;
|
|
2056
|
+
readFields(rr.sub(), (ff, fw, fr) => {
|
|
2057
|
+
if (ff !== 1) throw new ProtoError(`unknown field ${ff} in Fact`);
|
|
2058
|
+
expect(fw, 2, ff);
|
|
2059
|
+
pred = readPredicate(fr.sub());
|
|
2060
|
+
});
|
|
2061
|
+
out.facts.push(required(pred, "Fact.predicate"));
|
|
2062
|
+
break;
|
|
2063
|
+
}
|
|
2064
|
+
case 5:
|
|
2065
|
+
expect(w, 2, f);
|
|
2066
|
+
out.rules.push(readRule(rr.sub()));
|
|
2067
|
+
break;
|
|
2068
|
+
case 6: {
|
|
2069
|
+
expect(w, 2, f);
|
|
2070
|
+
const queries = [];
|
|
2071
|
+
let kind;
|
|
2072
|
+
readFields(rr.sub(), (cf, cw, cr) => {
|
|
2073
|
+
if (cf === 1) {
|
|
2074
|
+
expect(cw, 2, cf);
|
|
2075
|
+
queries.push(readRule(cr.sub()));
|
|
2076
|
+
} else if (cf === 2) {
|
|
2077
|
+
expect(cw, 0, cf);
|
|
2078
|
+
kind = cr.varintNum();
|
|
2079
|
+
} else throw new ProtoError(`unknown field ${cf} in Check`);
|
|
2080
|
+
});
|
|
2081
|
+
if (kind !== void 0 && kind !== 0 && kind !== 1 && kind !== 2) throw new ProtoError(`unknown check kind ${kind}`);
|
|
2082
|
+
out.checks.push({
|
|
2083
|
+
queries,
|
|
2084
|
+
kind
|
|
2085
|
+
});
|
|
2086
|
+
break;
|
|
2087
|
+
}
|
|
2088
|
+
case 7:
|
|
2089
|
+
expect(w, 2, f);
|
|
2090
|
+
out.scope.push(readScope(rr.sub()));
|
|
2091
|
+
break;
|
|
2092
|
+
case 8:
|
|
2093
|
+
expect(w, 2, f);
|
|
2094
|
+
out.publicKeys.push(readPublicKey(rr.sub()));
|
|
2095
|
+
break;
|
|
2096
|
+
default: throw new ProtoError(`unknown field ${f} in Block`);
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
return out;
|
|
2100
|
+
}
|
|
2101
|
+
function writePublicKey(v, w) {
|
|
2102
|
+
w.varintField(1, v.algorithm);
|
|
2103
|
+
w.bytesField(2, v.key);
|
|
2104
|
+
}
|
|
2105
|
+
function writeSignedBlock(v, w) {
|
|
2106
|
+
w.bytesField(1, v.block);
|
|
2107
|
+
w.messageField(2, writePublicKey, v.nextKey);
|
|
2108
|
+
w.bytesField(3, v.signature);
|
|
2109
|
+
if (v.externalSignature !== void 0) w.messageField(4, (e, ww) => {
|
|
2110
|
+
ww.bytesField(1, e.signature);
|
|
2111
|
+
ww.messageField(2, writePublicKey, e.publicKey);
|
|
2112
|
+
}, v.externalSignature);
|
|
2113
|
+
if (v.version !== void 0) w.varintField(5, v.version);
|
|
2114
|
+
}
|
|
2115
|
+
function encodeBiscuit(v) {
|
|
2116
|
+
const w = new Writer();
|
|
2117
|
+
if (v.rootKeyId !== void 0) w.varintField(1, v.rootKeyId);
|
|
2118
|
+
w.messageField(2, writeSignedBlock, v.authority);
|
|
2119
|
+
for (const b of v.blocks) w.messageField(3, writeSignedBlock, b);
|
|
2120
|
+
w.messageField(4, (p, ww) => ww.bytesField(p.kind === "nextSecret" ? 1 : 2, p.value), v.proof);
|
|
2121
|
+
return w.finish();
|
|
2122
|
+
}
|
|
2123
|
+
function writeTerm(t, w) {
|
|
2124
|
+
switch (t.kind) {
|
|
2125
|
+
case "variable":
|
|
2126
|
+
w.varintField(1, t.value);
|
|
2127
|
+
break;
|
|
2128
|
+
case "integer":
|
|
2129
|
+
w.varintField(2, t.value);
|
|
2130
|
+
break;
|
|
2131
|
+
case "string":
|
|
2132
|
+
w.varintField(3, t.value);
|
|
2133
|
+
break;
|
|
2134
|
+
case "date":
|
|
2135
|
+
w.varintField(4, t.value);
|
|
2136
|
+
break;
|
|
2137
|
+
case "bytes":
|
|
2138
|
+
w.bytesField(5, t.value);
|
|
2139
|
+
break;
|
|
2140
|
+
case "bool":
|
|
2141
|
+
w.varintField(6, t.value ? 1 : 0);
|
|
2142
|
+
break;
|
|
2143
|
+
case "set":
|
|
2144
|
+
w.messageField(7, (items, ww) => {
|
|
2145
|
+
for (const i of items) ww.messageField(1, writeTerm, i);
|
|
2146
|
+
}, t.value);
|
|
2147
|
+
break;
|
|
2148
|
+
case "null":
|
|
2149
|
+
w.bytesField(8, /* @__PURE__ */ new Uint8Array(0));
|
|
2150
|
+
break;
|
|
2151
|
+
case "array":
|
|
2152
|
+
w.messageField(9, (items, ww) => {
|
|
2153
|
+
for (const i of items) ww.messageField(1, writeTerm, i);
|
|
2154
|
+
}, t.value);
|
|
2155
|
+
break;
|
|
2156
|
+
case "map": w.messageField(10, (entries, ww) => {
|
|
2157
|
+
for (const e of entries) ww.messageField(1, (en, w3) => {
|
|
2158
|
+
w3.messageField(1, (k, w4) => w4.varintField(k.kind === "integer" ? 1 : 2, k.value), en.key);
|
|
2159
|
+
w3.messageField(2, writeTerm, en.value);
|
|
2160
|
+
}, e);
|
|
2161
|
+
}, t.value);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
function writePredicate(p, w) {
|
|
2165
|
+
w.varintField(1, p.name);
|
|
2166
|
+
for (const t of p.terms) w.messageField(2, writeTerm, t);
|
|
2167
|
+
}
|
|
2168
|
+
function writeOp(op, w) {
|
|
2169
|
+
switch (op.kind) {
|
|
2170
|
+
case "value":
|
|
2171
|
+
w.messageField(1, writeTerm, op.value);
|
|
2172
|
+
break;
|
|
2173
|
+
case "unary":
|
|
2174
|
+
case "binary":
|
|
2175
|
+
w.messageField(op.kind === "unary" ? 2 : 3, (o, ww) => {
|
|
2176
|
+
ww.varintField(1, o.op);
|
|
2177
|
+
if (o.ffiName !== void 0) ww.varintField(2, o.ffiName);
|
|
2178
|
+
}, op);
|
|
2179
|
+
break;
|
|
2180
|
+
case "closure": w.messageField(4, (c, ww) => {
|
|
2181
|
+
for (const p of c.params) ww.varintField(1, p);
|
|
2182
|
+
for (const o of c.ops) ww.messageField(2, writeOp, o);
|
|
2183
|
+
}, op);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function writeScope(s, w) {
|
|
2187
|
+
w.varintField(s.kind === "type" ? 1 : 2, s.value);
|
|
2188
|
+
}
|
|
2189
|
+
function writeRule(r, w) {
|
|
2190
|
+
w.messageField(1, writePredicate, r.head);
|
|
2191
|
+
for (const p of r.body) w.messageField(2, writePredicate, p);
|
|
2192
|
+
for (const e of r.expressions) w.messageField(3, (ops, ww) => {
|
|
2193
|
+
for (const o of ops) ww.messageField(1, writeOp, o);
|
|
2194
|
+
}, e);
|
|
2195
|
+
for (const s of r.scope) w.messageField(4, writeScope, s);
|
|
2196
|
+
}
|
|
2197
|
+
function encodeBlock(b) {
|
|
2198
|
+
const w = new Writer();
|
|
2199
|
+
for (const s of b.symbols) w.stringField(1, s);
|
|
2200
|
+
if (b.context !== void 0) w.stringField(2, b.context);
|
|
2201
|
+
if (b.version !== void 0) w.varintField(3, b.version);
|
|
2202
|
+
for (const f of b.facts) w.messageField(4, (p, ww) => ww.messageField(1, writePredicate, p), f);
|
|
2203
|
+
for (const r of b.rules) w.messageField(5, writeRule, r);
|
|
2204
|
+
for (const c of b.checks) w.messageField(6, (ch, ww) => {
|
|
2205
|
+
for (const q of ch.queries) ww.messageField(1, writeRule, q);
|
|
2206
|
+
if (ch.kind !== void 0) ww.varintField(2, ch.kind);
|
|
2207
|
+
}, c);
|
|
2208
|
+
for (const s of b.scope) w.messageField(7, writeScope, s);
|
|
2209
|
+
for (const k of b.publicKeys) w.messageField(8, writePublicKey, k);
|
|
2210
|
+
return w.finish();
|
|
2211
|
+
}
|
|
2212
|
+
//#endregion
|
|
2213
|
+
//#region src/version.ts
|
|
2214
|
+
/**
|
|
2215
|
+
* Datalog block versioning.
|
|
2216
|
+
*
|
|
2217
|
+
* Every block declares the Datalog version it was generated at. An
|
|
2218
|
+
* implementation must reject versions outside the supported window, and must
|
|
2219
|
+
* reject a block that uses a feature newer than the version it declares —
|
|
2220
|
+
* otherwise a token could smuggle newer semantics past an older verifier.
|
|
2221
|
+
*
|
|
2222
|
+
* Mirrors `get_schema_version` / `check_compatibility` in the reference
|
|
2223
|
+
* implementation (biscuit-auth `src/datalog/mod.rs`).
|
|
2224
|
+
*/
|
|
2225
|
+
const MIN_SCHEMA_VERSION = 3;
|
|
2226
|
+
const MAX_SCHEMA_VERSION = 6;
|
|
2227
|
+
const DATALOG_3_1 = 4;
|
|
2228
|
+
const DATALOG_3_2 = 5;
|
|
2229
|
+
const DATALOG_3_3 = 6;
|
|
2230
|
+
var VersionError = class extends Error {};
|
|
2231
|
+
/** null, or a set containing null — arrays and maps are deliberately not
|
|
2232
|
+
* flagged here, matching the reference implementation */
|
|
2233
|
+
const isV33Term = (t) => t.t === "null" || t.t === "set" && t.v.some((x) => x.t === "null");
|
|
2234
|
+
const hasV33Predicate = (p) => p.terms.some(isV33Term);
|
|
2235
|
+
const hasV31Op = (expressions) => expressions.some((ops) => ops.some((op) => op.kind === "binary" && (op.op === BinaryOp.BitwiseAnd || op.op === BinaryOp.BitwiseOr || op.op === BinaryOp.BitwiseXor || op.op === BinaryOp.NotEqual)));
|
|
2236
|
+
const hasV33Op = (expressions) => expressions.some((ops) => ops.some((op) => {
|
|
2237
|
+
if (op.kind === "value") return isV33Term(op.value);
|
|
2238
|
+
if (op.kind === "closure") return true;
|
|
2239
|
+
if (op.kind === "unary") return op.op === UnaryOp.TypeOf || op.op === UnaryOp.Ffi;
|
|
2240
|
+
return op.op === BinaryOp.HeterogeneousEqual || op.op === BinaryOp.HeterogeneousNotEqual || op.op === BinaryOp.LazyAnd || op.op === BinaryOp.LazyOr || op.op === BinaryOp.All || op.op === BinaryOp.Any || op.op === BinaryOp.Ffi;
|
|
2241
|
+
}));
|
|
2242
|
+
function blockFeatures(c) {
|
|
2243
|
+
const queries = c.checks.flatMap((ch) => ch.queries);
|
|
2244
|
+
const scopes = c.scopes.length > 0 || c.rules.some((r) => r.scopes.length > 0) || queries.some((q) => q.scopes.length > 0);
|
|
2245
|
+
const checkAll = c.checks.some((ch) => ch.kind === "all");
|
|
2246
|
+
let v33 = c.checks.some((ch) => ch.kind === "reject");
|
|
2247
|
+
const v31 = c.rules.some((r) => hasV31Op(r.expressions)) || queries.some((q) => hasV31Op(q.expressions));
|
|
2248
|
+
if (!v33) v33 = c.rules.some((r) => hasV33Predicate(r.head) || r.body.some(hasV33Predicate) || hasV33Op(r.expressions)) || queries.some((q) => q.body.some(hasV33Predicate) || hasV33Op(q.expressions));
|
|
2249
|
+
if (!v33) v33 = c.facts.some((f) => hasV33Predicate(f.predicate));
|
|
2250
|
+
return {
|
|
2251
|
+
scopes,
|
|
2252
|
+
v31,
|
|
2253
|
+
checkAll,
|
|
2254
|
+
v33
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
/** the lowest version that can legally carry this block's content */
|
|
2258
|
+
function requiredVersion(c) {
|
|
2259
|
+
const f = blockFeatures(c);
|
|
2260
|
+
if (f.v33) return 6;
|
|
2261
|
+
if (f.scopes || f.v31 || f.checkAll) return 4;
|
|
2262
|
+
return 3;
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Rejects a block whose declared version is out of range, or which uses a
|
|
2266
|
+
* feature newer than that version. `rawChecks` is needed because the gate on
|
|
2267
|
+
* check kinds distinguishes an absent kind from an explicit `One`.
|
|
2268
|
+
*/
|
|
2269
|
+
function validateBlockVersion(declared, rawChecks, hasExternalKey, content) {
|
|
2270
|
+
const version = declared ?? 0;
|
|
2271
|
+
if (version < 3 || version > 6) throw new VersionError(`unsupported datalog version ${version}: supported versions are 3 to 6`);
|
|
2272
|
+
if (version < 6) for (const c of rawChecks) {
|
|
2273
|
+
if (version < 4 && c.kind !== void 0) throw new VersionError("check kinds are only supported on datalog v3.1+ blocks");
|
|
2274
|
+
if (version < 6 && c.kind === 2) throw new VersionError("reject if is only supported in datalog v3.3+");
|
|
2275
|
+
}
|
|
2276
|
+
if (version < 5 && hasExternalKey) throw new VersionError("third-party blocks are only supported in datalog v3.2+");
|
|
2277
|
+
const f = blockFeatures(content);
|
|
2278
|
+
if (version < 4) {
|
|
2279
|
+
if (f.scopes) throw new VersionError("scopes are only supported in datalog v3.1+");
|
|
2280
|
+
if (f.v31) throw new VersionError("bitwise operators and != are only supported in datalog v3.1+");
|
|
2281
|
+
if (f.checkAll) throw new VersionError("check all is only supported in datalog v3.1+");
|
|
2282
|
+
} else if (version < 6 && f.v33) throw new VersionError("maps, arrays, null, closures are only supported in datalog v3.3+");
|
|
2283
|
+
}
|
|
2284
|
+
//#endregion
|
|
2285
|
+
//#region src/authorizer.ts
|
|
2286
|
+
/**
|
|
2287
|
+
* Turns a serialized token plus authorizer code into an authorization result,
|
|
2288
|
+
* mirroring the evaluation order of the reference implementation.
|
|
2289
|
+
*/
|
|
2290
|
+
const DEFAULT_SYMBOLS = [
|
|
2291
|
+
"read",
|
|
2292
|
+
"write",
|
|
2293
|
+
"resource",
|
|
2294
|
+
"operation",
|
|
2295
|
+
"right",
|
|
2296
|
+
"time",
|
|
2297
|
+
"role",
|
|
2298
|
+
"owner",
|
|
2299
|
+
"tenant",
|
|
2300
|
+
"namespace",
|
|
2301
|
+
"user",
|
|
2302
|
+
"team",
|
|
2303
|
+
"service",
|
|
2304
|
+
"admin",
|
|
2305
|
+
"email",
|
|
2306
|
+
"group",
|
|
2307
|
+
"member",
|
|
2308
|
+
"ip_address",
|
|
2309
|
+
"client",
|
|
2310
|
+
"client_ip",
|
|
2311
|
+
"domain",
|
|
2312
|
+
"path",
|
|
2313
|
+
"version",
|
|
2314
|
+
"cluster",
|
|
2315
|
+
"node",
|
|
2316
|
+
"hostname",
|
|
2317
|
+
"nonce",
|
|
2318
|
+
"query"
|
|
2319
|
+
];
|
|
2320
|
+
const OFFSET$1 = 1024;
|
|
2321
|
+
var TokenError = class extends Error {
|
|
2322
|
+
kind;
|
|
2323
|
+
constructor(kind, message) {
|
|
2324
|
+
super(message);
|
|
2325
|
+
this.kind = kind;
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
2329
|
+
const keyString = (algorithm, key) => `${algorithm === 0 ? "ed25519" : "secp256r1"}/${hex(key)}`;
|
|
2330
|
+
var Symbols = class {
|
|
2331
|
+
table;
|
|
2332
|
+
keys;
|
|
2333
|
+
constructor(table, keys) {
|
|
2334
|
+
this.table = table;
|
|
2335
|
+
this.keys = keys;
|
|
2336
|
+
}
|
|
2337
|
+
get(index) {
|
|
2338
|
+
if (index < OFFSET$1) {
|
|
2339
|
+
const s = DEFAULT_SYMBOLS[index];
|
|
2340
|
+
if (s === void 0) throw new TokenError("Symbol", `unknown default symbol ${index}`);
|
|
2341
|
+
return s;
|
|
2342
|
+
}
|
|
2343
|
+
const s = this.table[index - OFFSET$1];
|
|
2344
|
+
if (s === void 0) throw new TokenError("Symbol", `unknown symbol ${index}`);
|
|
2345
|
+
return s;
|
|
2346
|
+
}
|
|
2347
|
+
publicKey(index) {
|
|
2348
|
+
const k = this.keys[index];
|
|
2349
|
+
if (k === void 0) throw new TokenError("Symbol", `unknown public key ${index}`);
|
|
2350
|
+
return k;
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
function convTerm(t, s) {
|
|
2354
|
+
switch (t.kind) {
|
|
2355
|
+
case "variable": return {
|
|
2356
|
+
t: "var",
|
|
2357
|
+
v: t.value
|
|
2358
|
+
};
|
|
2359
|
+
case "integer": return {
|
|
2360
|
+
t: "int",
|
|
2361
|
+
v: t.value
|
|
2362
|
+
};
|
|
2363
|
+
case "string": return {
|
|
2364
|
+
t: "str",
|
|
2365
|
+
v: s.get(t.value)
|
|
2366
|
+
};
|
|
2367
|
+
case "date": return {
|
|
2368
|
+
t: "date",
|
|
2369
|
+
v: t.value
|
|
2370
|
+
};
|
|
2371
|
+
case "bytes": return {
|
|
2372
|
+
t: "bytes",
|
|
2373
|
+
v: t.value
|
|
2374
|
+
};
|
|
2375
|
+
case "bool": return {
|
|
2376
|
+
t: "bool",
|
|
2377
|
+
v: t.value
|
|
2378
|
+
};
|
|
2379
|
+
case "null": return { t: "null" };
|
|
2380
|
+
case "set": return {
|
|
2381
|
+
t: "set",
|
|
2382
|
+
v: normalizeSet(t.value.map((x) => convTerm(x, s)))
|
|
2383
|
+
};
|
|
2384
|
+
case "array": return {
|
|
2385
|
+
t: "array",
|
|
2386
|
+
v: t.value.map((x) => convTerm(x, s))
|
|
2387
|
+
};
|
|
2388
|
+
case "map": return {
|
|
2389
|
+
t: "map",
|
|
2390
|
+
v: t.value.map((e) => [e.key.kind === "integer" ? {
|
|
2391
|
+
t: "int",
|
|
2392
|
+
v: e.key.value
|
|
2393
|
+
} : {
|
|
2394
|
+
t: "str",
|
|
2395
|
+
v: s.get(e.key.value)
|
|
2396
|
+
}, convTerm(e.value, s)])
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
const convPredicate = (p, s) => ({
|
|
2401
|
+
name: s.get(p.name),
|
|
2402
|
+
terms: p.terms.map((t) => convTerm(t, s))
|
|
2403
|
+
});
|
|
2404
|
+
function convOps(ops, s) {
|
|
2405
|
+
return ops.map((op) => {
|
|
2406
|
+
switch (op.kind) {
|
|
2407
|
+
case "value": return {
|
|
2408
|
+
kind: "value",
|
|
2409
|
+
value: convTerm(op.value, s)
|
|
2410
|
+
};
|
|
2411
|
+
case "unary": return {
|
|
2412
|
+
kind: "unary",
|
|
2413
|
+
op: op.op,
|
|
2414
|
+
ffi: op.ffiName !== void 0 ? s.get(op.ffiName) : void 0
|
|
2415
|
+
};
|
|
2416
|
+
case "binary": return {
|
|
2417
|
+
kind: "binary",
|
|
2418
|
+
op: op.op,
|
|
2419
|
+
ffi: op.ffiName !== void 0 ? s.get(op.ffiName) : void 0
|
|
2420
|
+
};
|
|
2421
|
+
case "closure": return {
|
|
2422
|
+
kind: "closure",
|
|
2423
|
+
params: op.params,
|
|
2424
|
+
ops: convOps(op.ops, s)
|
|
2425
|
+
};
|
|
2426
|
+
default: throw new ProtoError(`unknown expression op kind ${op.kind}`);
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
const convScope = (sc, s) => sc.kind === "type" ? sc.value === 0 ? { kind: "authority" } : { kind: "previous" } : {
|
|
2431
|
+
kind: "publicKey",
|
|
2432
|
+
key: s.publicKey(sc.value)
|
|
2433
|
+
};
|
|
2434
|
+
const convRule = (r, s) => ({
|
|
2435
|
+
head: convPredicate(r.head, s),
|
|
2436
|
+
body: r.body.map((p) => convPredicate(p, s)),
|
|
2437
|
+
expressions: r.expressions.map((e) => convOps(e, s)),
|
|
2438
|
+
scopes: r.scope.map((sc) => convScope(sc, s))
|
|
2439
|
+
});
|
|
2440
|
+
function convBlock(b, s, externalKey) {
|
|
2441
|
+
const varNames = /* @__PURE__ */ new Map();
|
|
2442
|
+
const collectTerm = (t) => {
|
|
2443
|
+
if (t.kind === "variable") varNames.set(t.value, s.get(t.value));
|
|
2444
|
+
else if (t.kind === "set" || t.kind === "array") t.value.forEach(collectTerm);
|
|
2445
|
+
else if (t.kind === "map") t.value.forEach((e) => {
|
|
2446
|
+
collectTerm(e.value);
|
|
2447
|
+
});
|
|
2448
|
+
};
|
|
2449
|
+
const collectOps = (ops) => {
|
|
2450
|
+
for (const op of ops) if (op.kind === "value") collectTerm(op.value);
|
|
2451
|
+
else if (op.kind === "closure") {
|
|
2452
|
+
for (const param of op.params) varNames.set(param, s.get(param));
|
|
2453
|
+
collectOps(op.ops);
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
const collect = (r) => {
|
|
2457
|
+
for (const p of [r.head, ...r.body]) p.terms.forEach(collectTerm);
|
|
2458
|
+
r.expressions.forEach(collectOps);
|
|
2459
|
+
};
|
|
2460
|
+
for (const r of b.rules) collect(r);
|
|
2461
|
+
for (const c of b.checks) for (const q of c.queries) collect(q);
|
|
2462
|
+
return {
|
|
2463
|
+
varNames,
|
|
2464
|
+
facts: b.facts.map((f) => ({ predicate: convPredicate(f, s) })),
|
|
2465
|
+
rules: b.rules.map((r) => convRule(r, s)),
|
|
2466
|
+
checks: b.checks.map((c) => ({
|
|
2467
|
+
queries: c.queries.map((q) => convRule(q, s)),
|
|
2468
|
+
kind: c.kind === 1 ? "all" : c.kind === 2 ? "reject" : "one"
|
|
2469
|
+
})),
|
|
2470
|
+
scopes: b.scope.map((sc) => convScope(sc, s)),
|
|
2471
|
+
externalKey
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
function loadToken(bytes, rootPublicKey, rootAlgorithm = 0) {
|
|
2475
|
+
const token = decodeBiscuit(bytes);
|
|
2476
|
+
verifyToken(token, rootPublicKey, rootAlgorithm);
|
|
2477
|
+
const signed = [token.authority, ...token.blocks];
|
|
2478
|
+
const decoded = signed.map((sb) => decodeBlock(sb.block));
|
|
2479
|
+
const globalSymbols = [];
|
|
2480
|
+
const globalKeys = [];
|
|
2481
|
+
const extend = (list, items) => {
|
|
2482
|
+
for (const i of items) if (!list.includes(i)) list.push(i);
|
|
2483
|
+
};
|
|
2484
|
+
const blocks = [];
|
|
2485
|
+
const publicKeyToBlockIds = /* @__PURE__ */ new Map();
|
|
2486
|
+
for (let id = 0; id < decoded.length; id++) {
|
|
2487
|
+
const raw = decoded[id];
|
|
2488
|
+
const ext = signed[id].externalSignature;
|
|
2489
|
+
const externalKey = ext ? keyString(ext.publicKey.algorithm, ext.publicKey.key) : void 0;
|
|
2490
|
+
const blockKeys = raw.publicKeys.map((k) => keyString(k.algorithm, k.key));
|
|
2491
|
+
if (externalKey === void 0) {
|
|
2492
|
+
for (const sym of raw.symbols) {
|
|
2493
|
+
if (globalSymbols.includes(sym)) throw new TokenError("Format", "symbol table overlap");
|
|
2494
|
+
globalSymbols.push(sym);
|
|
2495
|
+
}
|
|
2496
|
+
extend(globalKeys, blockKeys);
|
|
2497
|
+
const block = convBlock(raw, new Symbols(globalSymbols, globalKeys), void 0);
|
|
2498
|
+
validateBlockVersion(raw.version, raw.checks, false, block);
|
|
2499
|
+
blocks.push(block);
|
|
2500
|
+
} else {
|
|
2501
|
+
const block = convBlock(raw, new Symbols(raw.symbols, blockKeys), externalKey);
|
|
2502
|
+
validateBlockVersion(raw.version, raw.checks, true, block);
|
|
2503
|
+
blocks.push(block);
|
|
2504
|
+
extend(globalKeys, blockKeys);
|
|
2505
|
+
const ids = publicKeyToBlockIds.get(externalKey) ?? [];
|
|
2506
|
+
ids.push(id);
|
|
2507
|
+
publicKeyToBlockIds.set(externalKey, ids);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return {
|
|
2511
|
+
blocks,
|
|
2512
|
+
publicKeyToBlockIds,
|
|
2513
|
+
revocationIds: revocationIds(token).map(hex),
|
|
2514
|
+
rootKeyId: token.rootKeyId
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Reads the key identifier from a token **without verifying it**, so a caller
|
|
2519
|
+
* can choose which root key to verify against. The token is untrusted at this
|
|
2520
|
+
* point: the id is a hint, not a claim.
|
|
2521
|
+
*/
|
|
2522
|
+
function peekRootKeyId(bytes) {
|
|
2523
|
+
return decodeBiscuit(bytes).rootKeyId;
|
|
2524
|
+
}
|
|
2525
|
+
/** a block rule whose head uses a variable the body never binds is invalid */
|
|
2526
|
+
function headVariablesAreBound(rule) {
|
|
2527
|
+
const bound = /* @__PURE__ */ new Set();
|
|
2528
|
+
for (const p of rule.body) for (const t of p.terms) if (t.t === "var") bound.add(t.v);
|
|
2529
|
+
return rule.head.terms.every((t) => t.t !== "var" || bound.has(t.v));
|
|
2530
|
+
}
|
|
2531
|
+
function parseAuthorizer(src) {
|
|
2532
|
+
const out = {
|
|
2533
|
+
facts: [],
|
|
2534
|
+
rules: [],
|
|
2535
|
+
checks: [],
|
|
2536
|
+
policies: [],
|
|
2537
|
+
scopes: [],
|
|
2538
|
+
varNames: /* @__PURE__ */ new Map()
|
|
2539
|
+
};
|
|
2540
|
+
const parser = new Parser(src);
|
|
2541
|
+
const statements = parser.parse();
|
|
2542
|
+
out.varNames = parser.variableNames();
|
|
2543
|
+
for (const st of statements) if (st.k === "fact") out.facts.push(st.fact);
|
|
2544
|
+
else if (st.k === "rule") out.rules.push(st.rule);
|
|
2545
|
+
else if (st.k === "check") out.checks.push(st.check);
|
|
2546
|
+
else if (st.k === "policy") out.policies.push({
|
|
2547
|
+
kind: st.kind,
|
|
2548
|
+
queries: st.queries
|
|
2549
|
+
});
|
|
2550
|
+
else out.scopes.push(...st.scopes);
|
|
2551
|
+
return out;
|
|
2552
|
+
}
|
|
2553
|
+
function authorize(token, authorizerSrc, options = {}) {
|
|
2554
|
+
return authorizeDetailed(token, authorizerSrc, options).result;
|
|
2555
|
+
}
|
|
2556
|
+
const EMPTY_WORLD = {
|
|
2557
|
+
facts: [],
|
|
2558
|
+
rules: [],
|
|
2559
|
+
checks: [],
|
|
2560
|
+
policies: []
|
|
2561
|
+
};
|
|
2562
|
+
/** Same as `authorize`, and also returns the post-run world, in the shape the
|
|
2563
|
+
* official sample corpus records it. */
|
|
2564
|
+
function authorizeDetailed(token, authorizerSrc, options = {}) {
|
|
2565
|
+
const limits = options.limits ?? DEFAULT_LIMITS;
|
|
2566
|
+
let code;
|
|
2567
|
+
try {
|
|
2568
|
+
code = parseAuthorizer(authorizerSrc);
|
|
2569
|
+
} catch (e) {
|
|
2570
|
+
return {
|
|
2571
|
+
result: {
|
|
2572
|
+
kind: "format",
|
|
2573
|
+
error: e.message
|
|
2574
|
+
},
|
|
2575
|
+
world: EMPTY_WORLD
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
const world = new World();
|
|
2579
|
+
if (options.externs) world.externs = options.externs;
|
|
2580
|
+
const keys = token.publicKeyToBlockIds;
|
|
2581
|
+
const blockTrusted = [];
|
|
2582
|
+
for (let id = 0; id < token.blocks.length; id++) {
|
|
2583
|
+
const block = token.blocks[id];
|
|
2584
|
+
const trusted = trustedOriginsFromScopes(block.scopes, TrustedOrigins.default(), id, keys);
|
|
2585
|
+
blockTrusted.push(trusted);
|
|
2586
|
+
const origin = Origin.of(id);
|
|
2587
|
+
for (const f of block.facts) world.addFact(origin, f);
|
|
2588
|
+
for (const r of block.rules) {
|
|
2589
|
+
if (!headVariablesAreBound(r)) return {
|
|
2590
|
+
result: {
|
|
2591
|
+
kind: "invalidBlockRule",
|
|
2592
|
+
blockId: 0,
|
|
2593
|
+
rule: printRule(r, (v) => block.varNames.get(v) ?? String(v))
|
|
2594
|
+
},
|
|
2595
|
+
world: EMPTY_WORLD
|
|
2596
|
+
};
|
|
2597
|
+
world.addRule(id, trustedOriginsFromScopes(r.scopes, trusted, id, keys), r);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
const authorizerTrusted = trustedOriginsFromScopes(code.scopes, TrustedOrigins.default(), AUTHORIZER, keys);
|
|
2601
|
+
const authorizerOrigin = Origin.of(AUTHORIZER);
|
|
2602
|
+
for (const f of code.facts) world.addFact(authorizerOrigin, f);
|
|
2603
|
+
for (const r of code.rules) world.addRule(AUTHORIZER, trustedOriginsFromScopes(r.scopes, authorizerTrusted, AUTHORIZER, keys), r);
|
|
2604
|
+
const name = (id) => id === 4294967295 ? (v) => code.varNames.get(v) ?? String(v) : (v) => token.blocks[id]?.varNames.get(v) ?? String(v);
|
|
2605
|
+
const blockId = (id) => id === 4294967295 ? null : id;
|
|
2606
|
+
const AUTHORIZER_U64 = Number(18446744073709551615n);
|
|
2607
|
+
const ruleOrigin = (id) => id === 4294967295 ? AUTHORIZER_U64 : id;
|
|
2608
|
+
const snapshot = () => {
|
|
2609
|
+
const factGroups = [...world.facts.values()].filter((b) => b.items.size > 0).map((b) => ({
|
|
2610
|
+
origin: b.origin.ids.map(blockId).sort((x, y) => (x ?? -1) - (y ?? -1)),
|
|
2611
|
+
facts: [...b.items.values()].map((f) => printPredicate(f.predicate, () => "?")).sort()
|
|
2612
|
+
})).sort((a, x) => compareOrigins(a.origin, x.origin));
|
|
2613
|
+
const ruleGroups = /* @__PURE__ */ new Map();
|
|
2614
|
+
for (const { origin, rule } of world.rules) {
|
|
2615
|
+
const list = ruleGroups.get(origin) ?? [];
|
|
2616
|
+
list.push(printRule(rule, name(origin)));
|
|
2617
|
+
ruleGroups.set(origin, list);
|
|
2618
|
+
}
|
|
2619
|
+
const checkGroups = [];
|
|
2620
|
+
code.checks.forEach((c) => {
|
|
2621
|
+
const entry = checkGroups.find((g) => g.origin === AUTHORIZER_U64);
|
|
2622
|
+
const text = printCheck(c, name(AUTHORIZER));
|
|
2623
|
+
if (entry) entry.checks.push(text);
|
|
2624
|
+
else checkGroups.push({
|
|
2625
|
+
origin: AUTHORIZER_U64,
|
|
2626
|
+
checks: [text]
|
|
2627
|
+
});
|
|
2628
|
+
});
|
|
2629
|
+
token.blocks.forEach((b, id) => {
|
|
2630
|
+
if (b.checks.length === 0) return;
|
|
2631
|
+
checkGroups.push({
|
|
2632
|
+
origin: id,
|
|
2633
|
+
checks: b.checks.map((c) => printCheck(c, name(id)))
|
|
2634
|
+
});
|
|
2635
|
+
});
|
|
2636
|
+
return {
|
|
2637
|
+
facts: factGroups,
|
|
2638
|
+
rules: [...ruleGroups.entries()].map(([origin, rules]) => ({
|
|
2639
|
+
origin: ruleOrigin(origin),
|
|
2640
|
+
rules: rules.sort()
|
|
2641
|
+
})).sort((a, b) => (a.origin ?? -1) - (b.origin ?? -1)),
|
|
2642
|
+
checks: checkGroups.sort((a, b) => (a.origin ?? -1) - (b.origin ?? -1)),
|
|
2643
|
+
policies: code.policies.map((p) => printPolicy(p.kind, p.queries, name(AUTHORIZER)))
|
|
2644
|
+
};
|
|
2645
|
+
};
|
|
2646
|
+
try {
|
|
2647
|
+
world.run(limits);
|
|
2648
|
+
} catch (e) {
|
|
2649
|
+
if (e instanceof ExecutionError) return {
|
|
2650
|
+
result: {
|
|
2651
|
+
kind: "execution",
|
|
2652
|
+
error: e.kind
|
|
2653
|
+
},
|
|
2654
|
+
world: snapshot()
|
|
2655
|
+
};
|
|
2656
|
+
throw e;
|
|
2657
|
+
}
|
|
2658
|
+
const errors = [];
|
|
2659
|
+
const runCheck = (check, blockId, defaults) => {
|
|
2660
|
+
for (const query of check.queries) {
|
|
2661
|
+
const trusted = trustedOriginsFromScopes(query.scopes, defaults, blockId, keys);
|
|
2662
|
+
if (check.kind === "all" ? world.queryMatchAll(query, trusted) : check.kind === "reject" ? !world.queryMatch(query, blockId, trusted) : world.queryMatch(query, blockId, trusted)) return true;
|
|
2663
|
+
}
|
|
2664
|
+
return false;
|
|
2665
|
+
};
|
|
2666
|
+
try {
|
|
2667
|
+
code.checks.forEach((check, i) => {
|
|
2668
|
+
if (!runCheck(check, 4294967295, authorizerTrusted)) errors.push({
|
|
2669
|
+
source: "authorizer",
|
|
2670
|
+
checkId: i
|
|
2671
|
+
});
|
|
2672
|
+
});
|
|
2673
|
+
token.blocks[0]?.checks.forEach((check, j) => {
|
|
2674
|
+
if (!runCheck(check, 0, blockTrusted[0])) errors.push({
|
|
2675
|
+
source: "block",
|
|
2676
|
+
blockId: 0,
|
|
2677
|
+
checkId: j
|
|
2678
|
+
});
|
|
2679
|
+
});
|
|
2680
|
+
let policyResult = null;
|
|
2681
|
+
outer: for (const [i, policy] of code.policies.entries()) for (const query of policy.queries) {
|
|
2682
|
+
const trusted = trustedOriginsFromScopes(query.scopes, authorizerTrusted, AUTHORIZER, keys);
|
|
2683
|
+
if (world.queryMatch(query, 4294967295, trusted)) {
|
|
2684
|
+
policyResult = policy.kind === "allow" ? { allow: i } : { deny: i };
|
|
2685
|
+
break outer;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
for (let id = 1; id < token.blocks.length; id++) token.blocks[id].checks.forEach((check, j) => {
|
|
2689
|
+
if (!runCheck(check, id, blockTrusted[id])) errors.push({
|
|
2690
|
+
source: "block",
|
|
2691
|
+
blockId: id,
|
|
2692
|
+
checkId: j
|
|
2693
|
+
});
|
|
2694
|
+
});
|
|
2695
|
+
return {
|
|
2696
|
+
result: policyResult === null ? {
|
|
2697
|
+
kind: "noMatchingPolicy",
|
|
2698
|
+
checks: errors
|
|
2699
|
+
} : "allow" in policyResult && errors.length === 0 ? {
|
|
2700
|
+
kind: "ok",
|
|
2701
|
+
policy: policyResult.allow
|
|
2702
|
+
} : {
|
|
2703
|
+
kind: "unauthorized",
|
|
2704
|
+
policy: policyResult,
|
|
2705
|
+
checks: errors
|
|
2706
|
+
},
|
|
2707
|
+
world: snapshot()
|
|
2708
|
+
};
|
|
2709
|
+
} catch (e) {
|
|
2710
|
+
if (e instanceof ExecutionError) return {
|
|
2711
|
+
result: {
|
|
2712
|
+
kind: "execution",
|
|
2713
|
+
error: e.kind
|
|
2714
|
+
},
|
|
2715
|
+
world: snapshot()
|
|
2716
|
+
};
|
|
2717
|
+
throw e;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
function compareOrigins(a, b) {
|
|
2721
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
2722
|
+
const x = a[i] === void 0 ? -Infinity : a[i] ?? -1;
|
|
2723
|
+
const y = b[i] === void 0 ? -Infinity : b[i] ?? -1;
|
|
2724
|
+
if (x !== y) return x - y;
|
|
2725
|
+
}
|
|
2726
|
+
return 0;
|
|
2727
|
+
}
|
|
2728
|
+
//#endregion
|
|
2729
|
+
//#region src/base64.ts
|
|
2730
|
+
/** URL-safe base64 without padding, the wire form of a Biscuit token.
|
|
2731
|
+
* Implemented directly so the library stays runtime-agnostic (no Buffer,
|
|
2732
|
+
* no atob/btoa). */
|
|
2733
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
2734
|
+
const REVERSE = new Map([...ALPHABET].map((c, i) => [c, i]));
|
|
2735
|
+
REVERSE.set("+", 62);
|
|
2736
|
+
REVERSE.set("/", 63);
|
|
2737
|
+
function toBase64(bytes) {
|
|
2738
|
+
let out = "";
|
|
2739
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
2740
|
+
const b0 = bytes[i];
|
|
2741
|
+
const b1 = bytes[i + 1];
|
|
2742
|
+
const b2 = bytes[i + 2];
|
|
2743
|
+
out += ALPHABET[b0 >> 2];
|
|
2744
|
+
out += ALPHABET[(b0 & 3) << 4 | (b1 ?? 0) >> 4];
|
|
2745
|
+
if (b1 === void 0) break;
|
|
2746
|
+
out += ALPHABET[(b1 & 15) << 2 | (b2 ?? 0) >> 6];
|
|
2747
|
+
if (b2 === void 0) break;
|
|
2748
|
+
out += ALPHABET[b2 & 63];
|
|
2749
|
+
}
|
|
2750
|
+
return out;
|
|
2751
|
+
}
|
|
2752
|
+
function fromBase64(text) {
|
|
2753
|
+
const s = text.trim().replace(/=+$/, "");
|
|
2754
|
+
const out = new Uint8Array(Math.floor(s.length * 3 / 4));
|
|
2755
|
+
let o = 0;
|
|
2756
|
+
let acc = 0;
|
|
2757
|
+
let bits = 0;
|
|
2758
|
+
for (const c of s) {
|
|
2759
|
+
const v = REVERSE.get(c);
|
|
2760
|
+
if (v === void 0) throw new Error(`invalid base64 character ${JSON.stringify(c)}`);
|
|
2761
|
+
acc = acc << 6 | v;
|
|
2762
|
+
bits += 6;
|
|
2763
|
+
if (bits >= 8) {
|
|
2764
|
+
bits -= 8;
|
|
2765
|
+
out[o++] = acc >> bits & 255;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
return out.subarray(0, o);
|
|
2769
|
+
}
|
|
2770
|
+
//#endregion
|
|
2771
|
+
//#region src/builder.ts
|
|
2772
|
+
/**
|
|
2773
|
+
* The write path: minting tokens, attenuating them with extra blocks, and
|
|
2774
|
+
* sealing them. New blocks are signed with signature payload version 1, and
|
|
2775
|
+
* declare the lowest Datalog version their content legally requires.
|
|
2776
|
+
*/
|
|
2777
|
+
const SIGNATURE_VERSION = 1;
|
|
2778
|
+
const OFFSET = 1024;
|
|
2779
|
+
var BuilderError = class extends Error {};
|
|
2780
|
+
var SymbolWriter = class {
|
|
2781
|
+
known;
|
|
2782
|
+
knownKeys;
|
|
2783
|
+
/** symbols added by this block, in insertion order */
|
|
2784
|
+
added = [];
|
|
2785
|
+
addedKeys = [];
|
|
2786
|
+
constructor(known = [], knownKeys = []) {
|
|
2787
|
+
this.known = known;
|
|
2788
|
+
this.knownKeys = knownKeys;
|
|
2789
|
+
}
|
|
2790
|
+
insert(s) {
|
|
2791
|
+
const d = DEFAULT_SYMBOLS.indexOf(s);
|
|
2792
|
+
if (d >= 0) return d;
|
|
2793
|
+
const k = this.known.indexOf(s);
|
|
2794
|
+
if (k >= 0) return OFFSET + k;
|
|
2795
|
+
let i = this.added.indexOf(s);
|
|
2796
|
+
if (i < 0) i = this.added.push(s) - 1;
|
|
2797
|
+
return OFFSET + this.known.length + i;
|
|
2798
|
+
}
|
|
2799
|
+
insertKey(key) {
|
|
2800
|
+
const k = this.knownKeys.indexOf(key);
|
|
2801
|
+
if (k >= 0) return k;
|
|
2802
|
+
let i = this.addedKeys.indexOf(key);
|
|
2803
|
+
if (i < 0) i = this.addedKeys.push(key) - 1;
|
|
2804
|
+
return this.knownKeys.length + i;
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
const parseKeyString = (key) => {
|
|
2808
|
+
const [alg, h] = key.split("/");
|
|
2809
|
+
const bytes = new Uint8Array(h.length / 2);
|
|
2810
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(h.substr(i * 2, 2), 16);
|
|
2811
|
+
return {
|
|
2812
|
+
algorithm: alg === "secp256r1" ? 1 : 0,
|
|
2813
|
+
key: bytes
|
|
2814
|
+
};
|
|
2815
|
+
};
|
|
2816
|
+
function toTerm(t, w) {
|
|
2817
|
+
switch (t.t) {
|
|
2818
|
+
case "var": return {
|
|
2819
|
+
kind: "variable",
|
|
2820
|
+
value: t.v
|
|
2821
|
+
};
|
|
2822
|
+
case "int": return {
|
|
2823
|
+
kind: "integer",
|
|
2824
|
+
value: t.v
|
|
2825
|
+
};
|
|
2826
|
+
case "str": return {
|
|
2827
|
+
kind: "string",
|
|
2828
|
+
value: w.insert(t.v)
|
|
2829
|
+
};
|
|
2830
|
+
case "date": return {
|
|
2831
|
+
kind: "date",
|
|
2832
|
+
value: t.v
|
|
2833
|
+
};
|
|
2834
|
+
case "bytes": return {
|
|
2835
|
+
kind: "bytes",
|
|
2836
|
+
value: t.v
|
|
2837
|
+
};
|
|
2838
|
+
case "bool": return {
|
|
2839
|
+
kind: "bool",
|
|
2840
|
+
value: t.v
|
|
2841
|
+
};
|
|
2842
|
+
case "null": return { kind: "null" };
|
|
2843
|
+
case "set": return {
|
|
2844
|
+
kind: "set",
|
|
2845
|
+
value: t.v.map((x) => toTerm(x, w))
|
|
2846
|
+
};
|
|
2847
|
+
case "array": return {
|
|
2848
|
+
kind: "array",
|
|
2849
|
+
value: t.v.map((x) => toTerm(x, w))
|
|
2850
|
+
};
|
|
2851
|
+
case "map": return {
|
|
2852
|
+
kind: "map",
|
|
2853
|
+
value: t.v.map((e) => ({
|
|
2854
|
+
key: e[0].t === "int" ? {
|
|
2855
|
+
kind: "integer",
|
|
2856
|
+
value: e[0].v
|
|
2857
|
+
} : {
|
|
2858
|
+
kind: "string",
|
|
2859
|
+
value: w.insert(e[0].v)
|
|
2860
|
+
},
|
|
2861
|
+
value: toTerm(e[1], w)
|
|
2862
|
+
}))
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
const toPredicate = (p, w) => ({
|
|
2867
|
+
name: w.insert(p.name),
|
|
2868
|
+
terms: p.terms.map((t) => toTerm(t, w))
|
|
2869
|
+
});
|
|
2870
|
+
const toOps = (ops, w) => ops.map((op) => {
|
|
2871
|
+
switch (op.kind) {
|
|
2872
|
+
case "value": return {
|
|
2873
|
+
kind: "value",
|
|
2874
|
+
value: toTerm(op.value, w)
|
|
2875
|
+
};
|
|
2876
|
+
case "unary": return {
|
|
2877
|
+
kind: "unary",
|
|
2878
|
+
op: op.op,
|
|
2879
|
+
ffiName: op.ffi ? w.insert(op.ffi) : void 0
|
|
2880
|
+
};
|
|
2881
|
+
case "binary": return {
|
|
2882
|
+
kind: "binary",
|
|
2883
|
+
op: op.op,
|
|
2884
|
+
ffiName: op.ffi ? w.insert(op.ffi) : void 0
|
|
2885
|
+
};
|
|
2886
|
+
case "closure": return {
|
|
2887
|
+
kind: "closure",
|
|
2888
|
+
params: op.params,
|
|
2889
|
+
ops: toOps(op.ops, w)
|
|
2890
|
+
};
|
|
2891
|
+
default: throw new Error(`unknown expression op kind ${op.kind}`);
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
const toScope = (s, w) => s.kind === "authority" ? {
|
|
2895
|
+
kind: "type",
|
|
2896
|
+
value: 0
|
|
2897
|
+
} : s.kind === "previous" ? {
|
|
2898
|
+
kind: "type",
|
|
2899
|
+
value: 1
|
|
2900
|
+
} : {
|
|
2901
|
+
kind: "publicKey",
|
|
2902
|
+
value: w.insertKey(s.key)
|
|
2903
|
+
};
|
|
2904
|
+
const toRule = (r, w) => ({
|
|
2905
|
+
head: toPredicate(r.head, w),
|
|
2906
|
+
body: r.body.map((p) => toPredicate(p, w)),
|
|
2907
|
+
expressions: r.expressions.map((e) => toOps(e, w)),
|
|
2908
|
+
scope: r.scopes.map((s) => toScope(s, w))
|
|
2909
|
+
});
|
|
2910
|
+
function buildBlockMsg(content, knownSymbols = [], knownKeys = [], minVersion = 0) {
|
|
2911
|
+
const w = new SymbolWriter(knownSymbols, knownKeys);
|
|
2912
|
+
const facts = content.facts.map((f) => toPredicate(f.predicate, w));
|
|
2913
|
+
const rules = content.rules.map((r) => toRule(r, w));
|
|
2914
|
+
const checks = content.checks.map((c) => ({
|
|
2915
|
+
queries: c.queries.map((q) => toRule(q, w)),
|
|
2916
|
+
kind: c.kind === "all" ? 1 : c.kind === "reject" ? 2 : void 0
|
|
2917
|
+
}));
|
|
2918
|
+
const scope = content.scopes.map((s) => toScope(s, w));
|
|
2919
|
+
return {
|
|
2920
|
+
symbols: w.added,
|
|
2921
|
+
version: Math.max(requiredVersion(content), minVersion),
|
|
2922
|
+
facts,
|
|
2923
|
+
rules,
|
|
2924
|
+
checks,
|
|
2925
|
+
scope,
|
|
2926
|
+
publicKeys: w.addedKeys.map(parseKeyString)
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
const contentFromCode = (code) => {
|
|
2930
|
+
const parsed = parseAuthorizer(code);
|
|
2931
|
+
if (parsed.policies.length) throw new BuilderError("allow/deny policies belong to the authorizer, not to a block");
|
|
2932
|
+
return {
|
|
2933
|
+
facts: parsed.facts,
|
|
2934
|
+
rules: parsed.rules,
|
|
2935
|
+
checks: parsed.checks,
|
|
2936
|
+
scopes: parsed.scopes
|
|
2937
|
+
};
|
|
2938
|
+
};
|
|
2939
|
+
function knownTables(token) {
|
|
2940
|
+
const symbols = [];
|
|
2941
|
+
const keys = [];
|
|
2942
|
+
const signed = [token.authority, ...token.blocks];
|
|
2943
|
+
for (const sb of signed) {
|
|
2944
|
+
const b = decodeBlock(sb.block);
|
|
2945
|
+
if (!sb.externalSignature) symbols.push(...b.symbols);
|
|
2946
|
+
for (const k of b.publicKeys) {
|
|
2947
|
+
const s = `${k.algorithm === 1 ? "secp256r1" : "ed25519"}/${Array.from(k.key, (x) => x.toString(16).padStart(2, "0")).join("")}`;
|
|
2948
|
+
if (!keys.includes(s)) keys.push(s);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
return {
|
|
2952
|
+
symbols,
|
|
2953
|
+
keys
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
/** Mint a new token whose authority block holds `code`. */
|
|
2957
|
+
function buildToken(rootSecret, code, options = {}) {
|
|
2958
|
+
const algorithm = options.algorithm ?? 0;
|
|
2959
|
+
const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code) : code));
|
|
2960
|
+
const next = options.nextKeypair ?? generateKeypair(algorithm);
|
|
2961
|
+
const nextKey = {
|
|
2962
|
+
algorithm,
|
|
2963
|
+
key: next.publicKey
|
|
2964
|
+
};
|
|
2965
|
+
const signature = sign(authorityPayloadV1(blockBytes, nextKey, SIGNATURE_VERSION), rootSecret, algorithm);
|
|
2966
|
+
return encodeBiscuit({
|
|
2967
|
+
rootKeyId: options.rootKeyId,
|
|
2968
|
+
authority: {
|
|
2969
|
+
block: blockBytes,
|
|
2970
|
+
nextKey,
|
|
2971
|
+
signature,
|
|
2972
|
+
version: SIGNATURE_VERSION
|
|
2973
|
+
},
|
|
2974
|
+
blocks: [],
|
|
2975
|
+
proof: {
|
|
2976
|
+
kind: "nextSecret",
|
|
2977
|
+
value: next.secretKey
|
|
2978
|
+
}
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
/** Append an attenuation block. Uses the token's own proof secret to sign. */
|
|
2982
|
+
function attenuate(tokenBytes, code, options = {}) {
|
|
2983
|
+
const token = decodeBiscuit(tokenBytes);
|
|
2984
|
+
if (token.proof.kind !== "nextSecret") throw new BuilderError("the token is sealed and cannot be attenuated");
|
|
2985
|
+
const algorithm = options.algorithm ?? 0;
|
|
2986
|
+
const currentSecret = token.proof.value;
|
|
2987
|
+
const previous = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
|
|
2988
|
+
const currentAlgorithm = previous.nextKey.algorithm;
|
|
2989
|
+
const tables = knownTables(token);
|
|
2990
|
+
const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code) : code, tables.symbols, tables.keys));
|
|
2991
|
+
const next = options.nextKeypair ?? generateKeypair(algorithm);
|
|
2992
|
+
const nextKey = {
|
|
2993
|
+
algorithm,
|
|
2994
|
+
key: next.publicKey
|
|
2995
|
+
};
|
|
2996
|
+
const signature = sign(blockPayloadV1(blockBytes, nextKey, void 0, previous.signature, SIGNATURE_VERSION), currentSecret, currentAlgorithm);
|
|
2997
|
+
token.blocks.push({
|
|
2998
|
+
block: blockBytes,
|
|
2999
|
+
nextKey,
|
|
3000
|
+
signature,
|
|
3001
|
+
version: SIGNATURE_VERSION
|
|
3002
|
+
});
|
|
3003
|
+
token.proof = {
|
|
3004
|
+
kind: "nextSecret",
|
|
3005
|
+
value: next.secretKey
|
|
3006
|
+
};
|
|
3007
|
+
return encodeBiscuit(token);
|
|
3008
|
+
}
|
|
3009
|
+
/** Seal a token so no further block can be appended. */
|
|
3010
|
+
function sealToken(tokenBytes) {
|
|
3011
|
+
const token = decodeBiscuit(tokenBytes);
|
|
3012
|
+
if (token.proof.kind !== "nextSecret") return tokenBytes;
|
|
3013
|
+
const last = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
|
|
3014
|
+
token.proof = {
|
|
3015
|
+
kind: "finalSignature",
|
|
3016
|
+
value: sign(sealPayloadV0(last), token.proof.value, last.nextKey.algorithm)
|
|
3017
|
+
};
|
|
3018
|
+
return encodeBiscuit(token);
|
|
3019
|
+
}
|
|
3020
|
+
/** What a token holder sends to a third party that will sign a block. */
|
|
3021
|
+
function thirdPartyRequest(tokenBytes) {
|
|
3022
|
+
const token = decodeBiscuit(tokenBytes);
|
|
3023
|
+
return { previousSignature: (token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority).signature };
|
|
3024
|
+
}
|
|
3025
|
+
/** The third party builds and signs a block without holding the token. */
|
|
3026
|
+
function thirdPartyBlock(request, externalSecret, code, algorithm = 0) {
|
|
3027
|
+
const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code) : code, [], [], 5));
|
|
3028
|
+
return {
|
|
3029
|
+
block: blockBytes,
|
|
3030
|
+
signature: sign(externalPayloadV1(blockBytes, request.previousSignature, SIGNATURE_VERSION), externalSecret, algorithm),
|
|
3031
|
+
publicKey: {
|
|
3032
|
+
algorithm,
|
|
3033
|
+
key: publicKeyFromSecret(externalSecret, algorithm)
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
/** The token holder appends a block signed by a third party. */
|
|
3038
|
+
function appendThirdParty(tokenBytes, response, options = {}) {
|
|
3039
|
+
const token = decodeBiscuit(tokenBytes);
|
|
3040
|
+
if (token.proof.kind !== "nextSecret") throw new BuilderError("the token is sealed and cannot be attenuated");
|
|
3041
|
+
const previous = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
|
|
3042
|
+
const algorithm = options.algorithm ?? 0;
|
|
3043
|
+
const next = options.nextKeypair ?? generateKeypair(algorithm);
|
|
3044
|
+
const nextKey = {
|
|
3045
|
+
algorithm,
|
|
3046
|
+
key: next.publicKey
|
|
3047
|
+
};
|
|
3048
|
+
const signature = sign(blockPayloadV1(response.block, nextKey, response.signature, previous.signature, SIGNATURE_VERSION), token.proof.value, previous.nextKey.algorithm);
|
|
3049
|
+
token.blocks.push({
|
|
3050
|
+
block: response.block,
|
|
3051
|
+
nextKey,
|
|
3052
|
+
signature,
|
|
3053
|
+
externalSignature: {
|
|
3054
|
+
signature: response.signature,
|
|
3055
|
+
publicKey: response.publicKey
|
|
3056
|
+
},
|
|
3057
|
+
version: SIGNATURE_VERSION
|
|
3058
|
+
});
|
|
3059
|
+
token.proof = {
|
|
3060
|
+
kind: "nextSecret",
|
|
3061
|
+
value: next.secretKey
|
|
3062
|
+
};
|
|
3063
|
+
return encodeBiscuit(token);
|
|
3064
|
+
}
|
|
3065
|
+
//#endregion
|
|
3066
|
+
//#region src/index.ts
|
|
3067
|
+
/**
|
|
3068
|
+
* biscuit-ts — pure TypeScript Biscuit tokens.
|
|
3069
|
+
*
|
|
3070
|
+
* const root = generateKeypair();
|
|
3071
|
+
* const token = Biscuit.build(root.secretKey, 'user("alice");');
|
|
3072
|
+
* const text = token.toBase64();
|
|
3073
|
+
* const ok = Biscuit.fromBase64(text).verify(root.publicKey).authorize('allow if user("alice");');
|
|
3074
|
+
*/
|
|
3075
|
+
/** An unverified token: it can be attenuated and re-serialized. */
|
|
3076
|
+
var Biscuit = class Biscuit {
|
|
3077
|
+
bytes;
|
|
3078
|
+
constructor(bytes) {
|
|
3079
|
+
this.bytes = bytes;
|
|
3080
|
+
}
|
|
3081
|
+
static build(rootSecret, code, options) {
|
|
3082
|
+
return new Biscuit(buildToken(rootSecret, code, options));
|
|
3083
|
+
}
|
|
3084
|
+
static fromBytes(bytes) {
|
|
3085
|
+
return new Biscuit(bytes);
|
|
3086
|
+
}
|
|
3087
|
+
static fromBase64(text) {
|
|
3088
|
+
return new Biscuit(fromBase64(text));
|
|
3089
|
+
}
|
|
3090
|
+
attenuate(code, options) {
|
|
3091
|
+
return new Biscuit(attenuate(this.bytes, code, options));
|
|
3092
|
+
}
|
|
3093
|
+
appendThirdParty(response, options) {
|
|
3094
|
+
return new Biscuit(appendThirdParty(this.bytes, response, options));
|
|
3095
|
+
}
|
|
3096
|
+
thirdPartyRequest() {
|
|
3097
|
+
return thirdPartyRequest(this.bytes);
|
|
3098
|
+
}
|
|
3099
|
+
seal() {
|
|
3100
|
+
return new Biscuit(sealToken(this.bytes));
|
|
3101
|
+
}
|
|
3102
|
+
toBase64() {
|
|
3103
|
+
return toBase64(this.bytes);
|
|
3104
|
+
}
|
|
3105
|
+
/** Verify the signature chain. Throws if the token is not authentic. */
|
|
3106
|
+
verify(rootPublicKey, rootAlgorithm = 0) {
|
|
3107
|
+
return new VerifiedBiscuit(loadToken(this.bytes, rootPublicKey, rootAlgorithm), this.bytes);
|
|
3108
|
+
}
|
|
3109
|
+
};
|
|
3110
|
+
/** A token whose signature chain has been checked against a root key. */
|
|
3111
|
+
var VerifiedBiscuit = class {
|
|
3112
|
+
token;
|
|
3113
|
+
bytes;
|
|
3114
|
+
constructor(token, bytes) {
|
|
3115
|
+
this.token = token;
|
|
3116
|
+
this.bytes = bytes;
|
|
3117
|
+
}
|
|
3118
|
+
get revocationIds() {
|
|
3119
|
+
return this.token.revocationIds;
|
|
3120
|
+
}
|
|
3121
|
+
/** the issuer's key identifier, when the token carries one */
|
|
3122
|
+
get rootKeyId() {
|
|
3123
|
+
return this.token.rootKeyId;
|
|
3124
|
+
}
|
|
3125
|
+
authorize(authorizerCode, options) {
|
|
3126
|
+
return authorize(this.token, authorizerCode, options);
|
|
3127
|
+
}
|
|
3128
|
+
};
|
|
3129
|
+
//#endregion
|
|
3130
|
+
export { Biscuit, BuilderError, DATALOG_3_1, DATALOG_3_2, DATALOG_3_3, DEFAULT_SYMBOLS, ExecutionError, MAX_SCHEMA_VERSION, MIN_SCHEMA_VERSION, ParseError, ProtoError, SignatureError, TokenError, VerifiedBiscuit, VersionError, appendThirdParty, attenuate, authorize, authorizeDetailed, blockFeatures, buildBlockMsg, buildToken, fromBase64, generateKeypair, loadToken, parseAuthorizer, peekRootKeyId, requiredVersion, sealToken, thirdPartyBlock, thirdPartyRequest, toBase64, validateBlockVersion };
|