@statewalker/webrun-biscuit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,47 @@
1
1
  import { ed25519 } from "@noble/curves/ed25519.js";
2
2
  import { p256 } from "@noble/curves/nist.js";
3
+ //#region src/base64.ts
4
+ /** URL-safe base64 without padding, the wire form of a Biscuit token.
5
+ * Implemented directly so the library stays runtime-agnostic (no Buffer,
6
+ * no atob/btoa). */
7
+ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
8
+ const REVERSE = new Map([...ALPHABET].map((c, i) => [c, i]));
9
+ REVERSE.set("+", 62);
10
+ REVERSE.set("/", 63);
11
+ function toBase64(bytes) {
12
+ let out = "";
13
+ for (let i = 0; i < bytes.length; i += 3) {
14
+ const b0 = bytes[i];
15
+ const b1 = bytes[i + 1];
16
+ const b2 = bytes[i + 2];
17
+ out += ALPHABET[b0 >> 2];
18
+ out += ALPHABET[(b0 & 3) << 4 | (b1 ?? 0) >> 4];
19
+ if (b1 === void 0) break;
20
+ out += ALPHABET[(b1 & 15) << 2 | (b2 ?? 0) >> 6];
21
+ if (b2 === void 0) break;
22
+ out += ALPHABET[b2 & 63];
23
+ }
24
+ return out;
25
+ }
26
+ function fromBase64(text) {
27
+ const s = text.trim().replace(/=+$/, "");
28
+ const out = new Uint8Array(Math.floor(s.length * 3 / 4));
29
+ let o = 0;
30
+ let acc = 0;
31
+ let bits = 0;
32
+ for (const c of s) {
33
+ const v = REVERSE.get(c);
34
+ if (v === void 0) throw new Error(`invalid base64 character ${JSON.stringify(c)}`);
35
+ acc = acc << 6 | v;
36
+ bits += 6;
37
+ if (bits >= 8) {
38
+ bits -= 8;
39
+ out[o++] = acc >> bits & 255;
40
+ }
41
+ }
42
+ return out.subarray(0, o);
43
+ }
44
+ //#endregion
3
45
  //#region src/crypto.ts
4
46
  /**
5
47
  * Biscuit cryptography: chained block signatures, sealing, third-party
@@ -75,17 +117,30 @@ function generateKeypair(algorithm = 0) {
75
117
  publicKey: publicKeyFromSecret(secretKey, algorithm)
76
118
  };
77
119
  }
78
- /** Verifies the full signature chain and the proof. Throws on failure. */
79
- function verifyToken(token, rootPublicKey, rootAlgorithm = 0) {
120
+ /**
121
+ * Everything verifying a token requires, as data: the signatures to check, in
122
+ * chain order, and — for an unsealed token — the proof secret whose public key
123
+ * must equal the last block's next key. Structural problems throw here, before
124
+ * any signature is looked at. `verifyToken` and `verifyTokenAsync` both consume
125
+ * this, so neither can skip a check the other makes.
126
+ */
127
+ function chainChecks(token, rootPublicKey, rootAlgorithm) {
80
128
  const root = {
81
129
  algorithm: rootAlgorithm,
82
130
  key: rootPublicKey
83
131
  };
132
+ const checks = [];
84
133
  if (token.authority.externalSignature) throw new SignatureError("the authority block must not carry an external signature");
85
134
  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) : (() => {
135
+ const authPayload = authVersion === 0 ? blockPayloadV0(token.authority.block, token.authority.nextKey) : authVersion === 1 ? authorityPayloadV1(token.authority.block, token.authority.nextKey, authVersion) : (() => {
87
136
  throw new SignatureError(`unsupported block version ${authVersion}`);
88
- })(), token.authority.signature)) throw new SignatureError("invalid authority block signature");
137
+ })();
138
+ checks.push({
139
+ key: root,
140
+ payload: authPayload,
141
+ signature: token.authority.signature,
142
+ error: "invalid authority block signature"
143
+ });
89
144
  let currentKey = token.authority.nextKey;
90
145
  let previousSignature = token.authority.signature;
91
146
  for (const block of token.blocks) {
@@ -95,21 +150,121 @@ function verifyToken(token, rootPublicKey, rootAlgorithm = 0) {
95
150
  if (version === 0) payload = blockPayloadV0(block.block, block.nextKey, externalSig);
96
151
  else if (version === 1) payload = blockPayloadV1(block.block, block.nextKey, externalSig, previousSignature, version);
97
152
  else throw new SignatureError(`unsupported block version ${version}`);
98
- if (!verifySignature(currentKey, payload, block.signature)) throw new SignatureError("invalid block signature");
153
+ checks.push({
154
+ key: currentKey,
155
+ payload,
156
+ signature: block.signature,
157
+ error: "invalid block signature"
158
+ });
99
159
  if (block.externalSignature) {
100
160
  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");
161
+ checks.push({
162
+ key: block.externalSignature.publicKey,
163
+ payload: externalPayloadV1(block.block, previousSignature, version),
164
+ signature: block.externalSignature.signature,
165
+ error: "invalid external signature"
166
+ });
103
167
  }
104
168
  currentKey = block.nextKey;
105
169
  previousSignature = block.signature;
106
170
  }
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");
171
+ if (token.proof.kind === "nextSecret") return {
172
+ checks,
173
+ proof: {
174
+ secret: token.proof.value,
175
+ key: currentKey
176
+ }
177
+ };
178
+ const last = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
179
+ checks.push({
180
+ key: currentKey,
181
+ payload: sealPayloadV0(last),
182
+ signature: token.proof.value,
183
+ error: "invalid seal signature"
184
+ });
185
+ return { checks };
186
+ }
187
+ /** Verifies the full signature chain and the proof. Throws on failure. */
188
+ function verifyToken(token, rootPublicKey, rootAlgorithm = 0) {
189
+ const { checks, proof } = chainChecks(token, rootPublicKey, rootAlgorithm);
190
+ for (const check of checks) if (!verifySignature(check.key, check.payload, check.signature)) throw new SignatureError(check.error);
191
+ if (proof && !bytesEqual(publicKeyFromSecret(proof.secret, proof.key.algorithm), proof.key.key)) throw new SignatureError("the last public key does not match the private key");
192
+ }
193
+ /**
194
+ * `verifyToken`, using the platform's WebCrypto for Ed25519 where it offers it
195
+ * — about ten times faster than the pure-JS path in Node and current browsers.
196
+ * secp256r1, and any runtime without WebCrypto Ed25519, fall back to
197
+ * `@noble/curves`, so the result never depends on where it runs.
198
+ */
199
+ async function verifyTokenAsync(token, rootPublicKey, rootAlgorithm = 0) {
200
+ const { checks, proof } = chainChecks(token, rootPublicKey, rootAlgorithm);
201
+ const [verdicts, derived] = await Promise.all([Promise.all(checks.map((c) => verifySignatureAsync(c.key, c.payload, c.signature))), proof ? publicKeyFromSecretAsync(proof.secret, proof.key.algorithm) : void 0]);
202
+ verdicts.forEach((ok, i) => {
203
+ if (!ok) throw new SignatureError(checks[i].error);
204
+ });
205
+ if (proof && !bytesEqual(derived, proof.key.key)) throw new SignatureError("the last public key does not match the private key");
206
+ }
207
+ const subtle = globalThis.crypto?.subtle;
208
+ /** PKCS#8 wrapping of a raw 32-byte Ed25519 seed (RFC 8410) */
209
+ const PKCS8_ED25519 = Uint8Array.from([
210
+ 48,
211
+ 46,
212
+ 2,
213
+ 1,
214
+ 0,
215
+ 48,
216
+ 5,
217
+ 6,
218
+ 3,
219
+ 43,
220
+ 101,
221
+ 112,
222
+ 4,
223
+ 34,
224
+ 4,
225
+ 32
226
+ ]);
227
+ let ed25519Native;
228
+ /** Whether this runtime's WebCrypto verifies Ed25519, and does so correctly. Probed once. */
229
+ function nativeEd25519() {
230
+ ed25519Native ??= (async () => {
231
+ if (!subtle) return false;
232
+ try {
233
+ const secret = (/* @__PURE__ */ new Uint8Array(32)).fill(7);
234
+ const message = ascii("webrun-biscuit");
235
+ const signature = ed25519.sign(message, secret);
236
+ const key = await subtle.importKey("raw", ed25519.getPublicKey(secret), { name: "Ed25519" }, false, ["verify"]);
237
+ const forged = signature.slice();
238
+ forged[0] ^= 1;
239
+ return await subtle.verify("Ed25519", key, signature, message) && !await subtle.verify("Ed25519", key, forged, message);
240
+ } catch {
241
+ return false;
242
+ }
243
+ })();
244
+ return ed25519Native;
245
+ }
246
+ async function verifySignatureAsync(key, payload, sig) {
247
+ if (key.algorithm !== ALG_ED25519 || !subtle || !await nativeEd25519()) return verifySignature(key, payload, sig);
248
+ if (key.key.length !== 32 || sig.length !== 64) return false;
249
+ try {
250
+ const imported = await subtle.importKey("raw", key.key, { name: "Ed25519" }, false, ["verify"]);
251
+ return await subtle.verify("Ed25519", imported, sig, payload);
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
256
+ async function publicKeyFromSecretAsync(secret, algorithm) {
257
+ if (algorithm !== ALG_ED25519 || secret.length !== 32 || !subtle || !await nativeEd25519()) return publicKeyFromSecret(secret, algorithm);
258
+ try {
259
+ const pkcs8 = new Uint8Array(PKCS8_ED25519.length + 32);
260
+ pkcs8.set(PKCS8_ED25519);
261
+ pkcs8.set(secret, PKCS8_ED25519.length);
262
+ const key = await subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, true, ["sign"]);
263
+ const { x } = await subtle.exportKey("jwk", key);
264
+ if (x === void 0) return publicKeyFromSecret(secret, algorithm);
265
+ return fromBase64(x);
266
+ } catch {
267
+ return publicKeyFromSecret(secret, algorithm);
113
268
  }
114
269
  }
115
270
  /** Revocation identifier of each block: its signature bytes. */
@@ -250,10 +405,10 @@ const B = {
250
405
  };
251
406
  const BinaryOp = B;
252
407
  const UnaryOp = U;
253
- const I64_MIN = -(2n ** 63n);
254
- const I64_MAX = 2n ** 63n - 1n;
408
+ const I64_MIN$1 = -(2n ** 63n);
409
+ const I64_MAX$1 = 2n ** 63n - 1n;
255
410
  function checkedI64(v) {
256
- if (v < I64_MIN || v > I64_MAX) throw new ExecutionError("Overflow");
411
+ if (v < I64_MIN$1 || v > I64_MAX$1) throw new ExecutionError("Overflow");
257
412
  return {
258
413
  t: "int",
259
414
  v
@@ -700,6 +855,15 @@ var World = class {
700
855
  } }];
701
856
  }
702
857
  }
858
+ /** every distinct fact `rule` derives from the facts `trusted` can see */
859
+ query(rule, trusted) {
860
+ const seen = /* @__PURE__ */ new Map();
861
+ for (const [, fact] of this.apply(rule, this.visible(trusted), AUTHORIZER)) {
862
+ const key = factKey(fact);
863
+ if (!seen.has(key)) seen.set(key, fact.predicate);
864
+ }
865
+ return [...seen.values()];
866
+ }
703
867
  /** `check if` / policies: does at least one combination match? */
704
868
  queryMatch(rule, origin, trusted) {
705
869
  for (const _ of this.apply(rule, this.visible(trusted), origin)) return true;
@@ -728,16 +892,73 @@ var World = class {
728
892
  * compiled straight to the opcode form the VM executes.
729
893
  */
730
894
  var ParseError = class extends Error {};
895
+ const I64_MIN = -(2n ** 63n);
896
+ const I64_MAX = 2n ** 63n - 1n;
897
+ function paramTerm(name, value) {
898
+ if (typeof value === "string") return {
899
+ t: "str",
900
+ v: value
901
+ };
902
+ if (typeof value === "boolean") return {
903
+ t: "bool",
904
+ v: value
905
+ };
906
+ if (value === null) return { t: "null" };
907
+ if (typeof value === "number") {
908
+ if (!Number.isSafeInteger(value)) throw new ParseError(`parameter {${name}}: ${value} is not a safe integer`);
909
+ return {
910
+ t: "int",
911
+ v: BigInt(value)
912
+ };
913
+ }
914
+ if (typeof value === "bigint") {
915
+ if (value < I64_MIN || value > I64_MAX) throw new ParseError(`parameter {${name}}: ${value} does not fit in i64`);
916
+ return {
917
+ t: "int",
918
+ v: value
919
+ };
920
+ }
921
+ if (value instanceof Date) {
922
+ const ms = value.getTime();
923
+ if (Number.isNaN(ms)) throw new ParseError(`parameter {${name}}: invalid date`);
924
+ return {
925
+ t: "date",
926
+ v: BigInt(Math.floor(ms / 1e3))
927
+ };
928
+ }
929
+ if (value instanceof Uint8Array) return {
930
+ t: "bytes",
931
+ v: value
932
+ };
933
+ if (Array.isArray(value)) return {
934
+ t: "array",
935
+ v: value.map((x) => paramTerm(name, x))
936
+ };
937
+ if (value instanceof Set) return {
938
+ t: "set",
939
+ v: normalizeSet([...value].map((x) => paramTerm(name, x)))
940
+ };
941
+ throw new ParseError(`parameter {${name}}: no Datalog term for this value`);
942
+ }
943
+ /** `{name}` in term position; `{true}`, `{false}` and `{null}` stay one-element sets */
944
+ const PARAMETER = /^\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}/;
731
945
  const NAME_START = /[\p{L}]/u;
732
946
  const NAME_CHAR = /[\p{L}\p{N}_:]/u;
733
947
  var Parser = class {
734
948
  src;
949
+ params;
735
950
  i = 0;
736
951
  vars;
737
- constructor(src, vars) {
952
+ usedParams = /* @__PURE__ */ new Set();
953
+ constructor(src, vars, params = {}) {
738
954
  this.src = src;
955
+ this.params = params;
739
956
  this.vars = vars ?? /* @__PURE__ */ new Map();
740
957
  }
958
+ /** names in `params` that the source never referred to */
959
+ unusedParameters() {
960
+ return Object.keys(this.params).filter((name) => !this.usedParams.has(name));
961
+ }
741
962
  /** id -> name, for printing rules back out */
742
963
  variableNames() {
743
964
  const out = /* @__PURE__ */ new Map();
@@ -890,7 +1111,7 @@ var Parser = class {
890
1111
  };
891
1112
  }
892
1113
  if (this.peek("[")) return this.array(allowVariables);
893
- if (this.peek("{")) return this.setOrMap(allowVariables);
1114
+ if (this.peek("{")) return this.parameter() ?? this.setOrMap(allowVariables);
894
1115
  const m = /^-?\d+/.exec(this.src.slice(this.i));
895
1116
  if (m) {
896
1117
  this.i += m[0].length;
@@ -901,6 +1122,15 @@ var Parser = class {
901
1122
  }
902
1123
  throw new ParseError(`unexpected term at offset ${this.i}: ${this.src.slice(this.i, this.i + 20)}`);
903
1124
  }
1125
+ parameter() {
1126
+ const m = PARAMETER.exec(this.src.slice(this.i));
1127
+ if (!m || m[1] === "true" || m[1] === "false" || m[1] === "null") return null;
1128
+ const name = m[1];
1129
+ if (!Object.hasOwn(this.params, name)) throw new ParseError(`unbound parameter {${name}}`);
1130
+ this.i += m[0].length;
1131
+ this.usedParams.add(name);
1132
+ return paramTerm(name, this.params[name]);
1133
+ }
904
1134
  array(allowVariables) {
905
1135
  this.expect("[");
906
1136
  const items = [];
@@ -2474,6 +2704,18 @@ function convBlock(b, s, externalKey) {
2474
2704
  function loadToken(bytes, rootPublicKey, rootAlgorithm = 0) {
2475
2705
  const token = decodeBiscuit(bytes);
2476
2706
  verifyToken(token, rootPublicKey, rootAlgorithm);
2707
+ return toLoadedToken(token);
2708
+ }
2709
+ /**
2710
+ * `loadToken`, verifying signatures with the platform's WebCrypto Ed25519 where
2711
+ * available — see `verifyTokenAsync`. Same result, same errors.
2712
+ */
2713
+ async function loadTokenAsync(bytes, rootPublicKey, rootAlgorithm = 0) {
2714
+ const token = decodeBiscuit(bytes);
2715
+ await verifyTokenAsync(token, rootPublicKey, rootAlgorithm);
2716
+ return toLoadedToken(token);
2717
+ }
2718
+ function toLoadedToken(token) {
2477
2719
  const signed = [token.authority, ...token.blocks];
2478
2720
  const decoded = signed.map((sb) => decodeBlock(sb.block));
2479
2721
  const globalSymbols = [];
@@ -2528,7 +2770,12 @@ function headVariablesAreBound(rule) {
2528
2770
  for (const p of rule.body) for (const t of p.terms) if (t.t === "var") bound.add(t.v);
2529
2771
  return rule.head.terms.every((t) => t.t !== "var" || bound.has(t.v));
2530
2772
  }
2531
- function parseAuthorizer(src) {
2773
+ /**
2774
+ * Parse authorizer (or block) source. `{name}` parameters are bound from
2775
+ * `params` as terms; an unbound or an unused parameter is a `ParseError`, as it
2776
+ * is in the reference.
2777
+ */
2778
+ function parseAuthorizer(src, params) {
2532
2779
  const out = {
2533
2780
  facts: [],
2534
2781
  rules: [],
@@ -2537,8 +2784,10 @@ function parseAuthorizer(src) {
2537
2784
  scopes: [],
2538
2785
  varNames: /* @__PURE__ */ new Map()
2539
2786
  };
2540
- const parser = new Parser(src);
2787
+ const parser = new Parser(src, void 0, params);
2541
2788
  const statements = parser.parse();
2789
+ const unused = parser.unusedParameters();
2790
+ if (unused.length > 0) throw new ParseError(`unused parameter${unused.length > 1 ? "s" : ""} {${unused.join("}, {")}}`);
2542
2791
  out.varNames = parser.variableNames();
2543
2792
  for (const st of statements) if (st.k === "fact") out.facts.push(st.fact);
2544
2793
  else if (st.k === "rule") out.rules.push(st.rule);
@@ -2551,7 +2800,7 @@ function parseAuthorizer(src) {
2551
2800
  return out;
2552
2801
  }
2553
2802
  function authorize(token, authorizerSrc, options = {}) {
2554
- return authorizeDetailed(token, authorizerSrc, options).result;
2803
+ return evaluate(token, authorizerSrc, options).result;
2555
2804
  }
2556
2805
  const EMPTY_WORLD = {
2557
2806
  facts: [],
@@ -2562,18 +2811,32 @@ const EMPTY_WORLD = {
2562
2811
  /** Same as `authorize`, and also returns the post-run world, in the shape the
2563
2812
  * official sample corpus records it. */
2564
2813
  function authorizeDetailed(token, authorizerSrc, options = {}) {
2814
+ const evaluation = evaluate(token, authorizerSrc, options);
2815
+ return {
2816
+ result: evaluation.result,
2817
+ world: evaluation.snapshot()
2818
+ };
2819
+ }
2820
+ const NO_TOKEN = {
2821
+ blocks: [],
2822
+ publicKeyToBlockIds: /* @__PURE__ */ new Map(),
2823
+ revocationIds: []
2824
+ };
2825
+ /**
2826
+ * Run the authorizer against a verified token — or against no token at all,
2827
+ * for a decision made from the authorizer's own facts and rules.
2828
+ */
2829
+ function evaluate(loadedToken, authorizerSrc, options = {}) {
2830
+ const token = loadedToken ?? NO_TOKEN;
2565
2831
  const limits = options.limits ?? DEFAULT_LIMITS;
2566
2832
  let code;
2567
2833
  try {
2568
- code = parseAuthorizer(authorizerSrc);
2834
+ code = parseAuthorizer(authorizerSrc, options.params);
2569
2835
  } catch (e) {
2570
- return {
2571
- result: {
2572
- kind: "format",
2573
- error: e.message
2574
- },
2575
- world: EMPTY_WORLD
2576
- };
2836
+ return unusable({
2837
+ kind: "format",
2838
+ error: e.message
2839
+ });
2577
2840
  }
2578
2841
  const world = new World();
2579
2842
  if (options.externs) world.externs = options.externs;
@@ -2586,14 +2849,11 @@ function authorizeDetailed(token, authorizerSrc, options = {}) {
2586
2849
  const origin = Origin.of(id);
2587
2850
  for (const f of block.facts) world.addFact(origin, f);
2588
2851
  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
- };
2852
+ if (!headVariablesAreBound(r)) return unusable({
2853
+ kind: "invalidBlockRule",
2854
+ blockId: 0,
2855
+ rule: printRule(r, (v) => block.varNames.get(v) ?? String(v))
2856
+ });
2597
2857
  world.addRule(id, trustedOriginsFromScopes(r.scopes, trusted, id, keys), r);
2598
2858
  }
2599
2859
  }
@@ -2643,16 +2903,29 @@ function authorizeDetailed(token, authorizerSrc, options = {}) {
2643
2903
  policies: code.policies.map((p) => printPolicy(p.kind, p.queries, name(AUTHORIZER)))
2644
2904
  };
2645
2905
  };
2906
+ let failure;
2907
+ const settled = (result) => ({
2908
+ result,
2909
+ snapshot,
2910
+ query(src, queryOptions = {}) {
2911
+ if (failure) throw failure;
2912
+ const parsed = parseAuthorizer(`${src};`, queryOptions.params);
2913
+ if (parsed.facts.length + parsed.rules.length + parsed.checks.length + parsed.policies.length !== 1 || parsed.rules.length !== 1) throw new ParseError("a query must be exactly one rule");
2914
+ const rule = parsed.rules[0];
2915
+ const trusted = trustedOriginsFromScopes(rule.scopes, authorizerTrusted, AUTHORIZER, keys);
2916
+ return world.query(rule, trusted);
2917
+ }
2918
+ });
2646
2919
  try {
2647
2920
  world.run(limits);
2648
2921
  } catch (e) {
2649
- if (e instanceof ExecutionError) return {
2650
- result: {
2922
+ if (e instanceof ExecutionError) {
2923
+ failure = e;
2924
+ return settled({
2651
2925
  kind: "execution",
2652
2926
  error: e.kind
2653
- },
2654
- world: snapshot()
2655
- };
2927
+ });
2928
+ }
2656
2929
  throw e;
2657
2930
  }
2658
2931
  const errors = [];
@@ -2667,14 +2940,16 @@ function authorizeDetailed(token, authorizerSrc, options = {}) {
2667
2940
  code.checks.forEach((check, i) => {
2668
2941
  if (!runCheck(check, 4294967295, authorizerTrusted)) errors.push({
2669
2942
  source: "authorizer",
2670
- checkId: i
2943
+ checkId: i,
2944
+ rule: printCheck(check, name(AUTHORIZER))
2671
2945
  });
2672
2946
  });
2673
2947
  token.blocks[0]?.checks.forEach((check, j) => {
2674
2948
  if (!runCheck(check, 0, blockTrusted[0])) errors.push({
2675
2949
  source: "block",
2676
2950
  blockId: 0,
2677
- checkId: j
2951
+ checkId: j,
2952
+ rule: printCheck(check, name(0))
2678
2953
  });
2679
2954
  });
2680
2955
  let policyResult = null;
@@ -2689,34 +2964,42 @@ function authorizeDetailed(token, authorizerSrc, options = {}) {
2689
2964
  if (!runCheck(check, id, blockTrusted[id])) errors.push({
2690
2965
  source: "block",
2691
2966
  blockId: id,
2692
- checkId: j
2967
+ checkId: j,
2968
+ rule: printCheck(check, name(id))
2693
2969
  });
2694
2970
  });
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
- };
2971
+ return settled(policyResult === null ? {
2972
+ kind: "noMatchingPolicy",
2973
+ checks: errors
2974
+ } : "allow" in policyResult && errors.length === 0 ? {
2975
+ kind: "ok",
2976
+ policy: policyResult.allow
2977
+ } : {
2978
+ kind: "unauthorized",
2979
+ policy: policyResult,
2980
+ checks: errors
2981
+ });
2709
2982
  } catch (e) {
2710
- if (e instanceof ExecutionError) return {
2711
- result: {
2983
+ if (e instanceof ExecutionError) {
2984
+ failure = e;
2985
+ return settled({
2712
2986
  kind: "execution",
2713
2987
  error: e.kind
2714
- },
2715
- world: snapshot()
2716
- };
2988
+ });
2989
+ }
2717
2990
  throw e;
2718
2991
  }
2719
2992
  }
2993
+ /** An evaluation that never produced a world: the result says why, and queries refuse. */
2994
+ function unusable(result) {
2995
+ return {
2996
+ result,
2997
+ snapshot: () => EMPTY_WORLD,
2998
+ query() {
2999
+ throw new Error(`cannot query an evaluation that ended in ${result.kind}`);
3000
+ }
3001
+ };
3002
+ }
2720
3003
  function compareOrigins(a, b) {
2721
3004
  for (let i = 0; i < Math.max(a.length, b.length); i++) {
2722
3005
  const x = a[i] === void 0 ? -Infinity : a[i] ?? -1;
@@ -2726,48 +3009,6 @@ function compareOrigins(a, b) {
2726
3009
  return 0;
2727
3010
  }
2728
3011
  //#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
3012
  //#region src/builder.ts
2772
3013
  /**
2773
3014
  * The write path: minting tokens, attenuating them with extra blocks, and
@@ -2780,12 +3021,18 @@ var BuilderError = class extends Error {};
2780
3021
  var SymbolWriter = class {
2781
3022
  known;
2782
3023
  knownKeys;
3024
+ varNames;
2783
3025
  /** symbols added by this block, in insertion order */
2784
3026
  added = [];
2785
3027
  addedKeys = [];
2786
- constructor(known = [], knownKeys = []) {
3028
+ constructor(known = [], knownKeys = [], varNames = /* @__PURE__ */ new Map()) {
2787
3029
  this.known = known;
2788
3030
  this.knownKeys = knownKeys;
3031
+ this.varNames = varNames;
3032
+ }
3033
+ /** A variable's wire id is the symbol index of its NAME, not the parser's local id. */
3034
+ variable(id) {
3035
+ return this.insert(this.varNames.get(id) ?? String(id));
2789
3036
  }
2790
3037
  insert(s) {
2791
3038
  const d = DEFAULT_SYMBOLS.indexOf(s);
@@ -2817,7 +3064,7 @@ function toTerm(t, w) {
2817
3064
  switch (t.t) {
2818
3065
  case "var": return {
2819
3066
  kind: "variable",
2820
- value: t.v
3067
+ value: w.variable(t.v)
2821
3068
  };
2822
3069
  case "int": return {
2823
3070
  kind: "integer",
@@ -2885,7 +3132,7 @@ const toOps = (ops, w) => ops.map((op) => {
2885
3132
  };
2886
3133
  case "closure": return {
2887
3134
  kind: "closure",
2888
- params: op.params,
3135
+ params: op.params.map((p) => w.variable(p)),
2889
3136
  ops: toOps(op.ops, w)
2890
3137
  };
2891
3138
  default: throw new Error(`unknown expression op kind ${op.kind}`);
@@ -2908,7 +3155,7 @@ const toRule = (r, w) => ({
2908
3155
  scope: r.scopes.map((s) => toScope(s, w))
2909
3156
  });
2910
3157
  function buildBlockMsg(content, knownSymbols = [], knownKeys = [], minVersion = 0) {
2911
- const w = new SymbolWriter(knownSymbols, knownKeys);
3158
+ const w = new SymbolWriter(knownSymbols, knownKeys, content.varNames);
2912
3159
  const facts = content.facts.map((f) => toPredicate(f.predicate, w));
2913
3160
  const rules = content.rules.map((r) => toRule(r, w));
2914
3161
  const checks = content.checks.map((c) => ({
@@ -2926,14 +3173,15 @@ function buildBlockMsg(content, knownSymbols = [], knownKeys = [], minVersion =
2926
3173
  publicKeys: w.addedKeys.map(parseKeyString)
2927
3174
  };
2928
3175
  }
2929
- const contentFromCode = (code) => {
2930
- const parsed = parseAuthorizer(code);
3176
+ const contentFromCode = (code, params) => {
3177
+ const parsed = parseAuthorizer(code, params);
2931
3178
  if (parsed.policies.length) throw new BuilderError("allow/deny policies belong to the authorizer, not to a block");
2932
3179
  return {
2933
3180
  facts: parsed.facts,
2934
3181
  rules: parsed.rules,
2935
3182
  checks: parsed.checks,
2936
- scopes: parsed.scopes
3183
+ scopes: parsed.scopes,
3184
+ varNames: parsed.varNames
2937
3185
  };
2938
3186
  };
2939
3187
  function knownTables(token) {
@@ -2956,7 +3204,7 @@ function knownTables(token) {
2956
3204
  /** Mint a new token whose authority block holds `code`. */
2957
3205
  function buildToken(rootSecret, code, options = {}) {
2958
3206
  const algorithm = options.algorithm ?? 0;
2959
- const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code) : code));
3207
+ const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code, options.params) : code));
2960
3208
  const next = options.nextKeypair ?? generateKeypair(algorithm);
2961
3209
  const nextKey = {
2962
3210
  algorithm,
@@ -2987,7 +3235,7 @@ function attenuate(tokenBytes, code, options = {}) {
2987
3235
  const previous = token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority;
2988
3236
  const currentAlgorithm = previous.nextKey.algorithm;
2989
3237
  const tables = knownTables(token);
2990
- const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code) : code, tables.symbols, tables.keys));
3238
+ const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code, options.params) : code, tables.symbols, tables.keys));
2991
3239
  const next = options.nextKeypair ?? generateKeypair(algorithm);
2992
3240
  const nextKey = {
2993
3241
  algorithm,
@@ -3023,8 +3271,8 @@ function thirdPartyRequest(tokenBytes) {
3023
3271
  return { previousSignature: (token.blocks.length ? token.blocks[token.blocks.length - 1] : token.authority).signature };
3024
3272
  }
3025
3273
  /** 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));
3274
+ function thirdPartyBlock(request, externalSecret, code, algorithm = 0, params) {
3275
+ const blockBytes = encodeBlock(buildBlockMsg(typeof code === "string" ? contentFromCode(code, params) : code, [], [], 5));
3028
3276
  return {
3029
3277
  block: blockBytes,
3030
3278
  signature: sign(externalPayloadV1(blockBytes, request.previousSignature, SIGNATURE_VERSION), externalSecret, algorithm),
@@ -3106,6 +3354,13 @@ var Biscuit = class Biscuit {
3106
3354
  verify(rootPublicKey, rootAlgorithm = 0) {
3107
3355
  return new VerifiedBiscuit(loadToken(this.bytes, rootPublicKey, rootAlgorithm), this.bytes);
3108
3356
  }
3357
+ /**
3358
+ * `verify`, using WebCrypto Ed25519 where the platform has it — roughly ten
3359
+ * times faster than the pure-JS path. Rejects exactly where `verify` throws.
3360
+ */
3361
+ async verifyAsync(rootPublicKey, rootAlgorithm = 0) {
3362
+ return new VerifiedBiscuit(await loadTokenAsync(this.bytes, rootPublicKey, rootAlgorithm), this.bytes);
3363
+ }
3109
3364
  };
3110
3365
  /** A token whose signature chain has been checked against a root key. */
3111
3366
  var VerifiedBiscuit = class {
@@ -3125,6 +3380,10 @@ var VerifiedBiscuit = class {
3125
3380
  authorize(authorizerCode, options) {
3126
3381
  return authorize(this.token, authorizerCode, options);
3127
3382
  }
3383
+ /** Authorize, keeping the evaluated world available to `query`. */
3384
+ evaluate(authorizerCode, options) {
3385
+ return evaluate(this.token, authorizerCode, options);
3386
+ }
3128
3387
  };
3129
3388
  //#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 };
3389
+ 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, evaluate, fromBase64, generateKeypair, loadToken, loadTokenAsync, parseAuthorizer, peekRootKeyId, requiredVersion, sealToken, thirdPartyBlock, thirdPartyRequest, toBase64, validateBlockVersion };