@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/src/index.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* biscuit-ts — pure TypeScript Biscuit tokens.
|
|
3
|
+
*
|
|
4
|
+
* const root = generateKeypair();
|
|
5
|
+
* const token = Biscuit.build(root.secretKey, 'user("alice");');
|
|
6
|
+
* const text = token.toBase64();
|
|
7
|
+
* const ok = Biscuit.fromBase64(text).verify(root.publicKey).authorize('allow if user("alice");');
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type AuthorizationResult,
|
|
12
|
+
type AuthorizeOptions,
|
|
13
|
+
authorize,
|
|
14
|
+
type LoadedToken,
|
|
15
|
+
loadToken,
|
|
16
|
+
} from "./authorizer.js";
|
|
17
|
+
import { fromBase64, toBase64 } from "./base64.js";
|
|
18
|
+
import {
|
|
19
|
+
appendThirdParty,
|
|
20
|
+
attenuate,
|
|
21
|
+
type BuildOptions,
|
|
22
|
+
buildToken,
|
|
23
|
+
generateKeypair,
|
|
24
|
+
sealToken,
|
|
25
|
+
type ThirdPartyResponse,
|
|
26
|
+
thirdPartyBlock,
|
|
27
|
+
thirdPartyRequest,
|
|
28
|
+
} from "./builder.js";
|
|
29
|
+
|
|
30
|
+
export * from "./authorizer.js";
|
|
31
|
+
export * from "./base64.js";
|
|
32
|
+
export * from "./builder.js";
|
|
33
|
+
export { SignatureError } from "./crypto.js";
|
|
34
|
+
export { ExecutionError, type ExternFn, type Term } from "./datalog.js";
|
|
35
|
+
export { ParseError } from "./parser.js";
|
|
36
|
+
export { ProtoError } from "./proto.js";
|
|
37
|
+
export * from "./version.js";
|
|
38
|
+
|
|
39
|
+
/** An unverified token: it can be attenuated and re-serialized. */
|
|
40
|
+
export class Biscuit {
|
|
41
|
+
private constructor(readonly bytes: Uint8Array) {}
|
|
42
|
+
|
|
43
|
+
static build(rootSecret: Uint8Array, code: string, options?: BuildOptions): Biscuit {
|
|
44
|
+
return new Biscuit(buildToken(rootSecret, code, options));
|
|
45
|
+
}
|
|
46
|
+
static fromBytes(bytes: Uint8Array): Biscuit {
|
|
47
|
+
return new Biscuit(bytes);
|
|
48
|
+
}
|
|
49
|
+
static fromBase64(text: string): Biscuit {
|
|
50
|
+
return new Biscuit(fromBase64(text));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
attenuate(code: string, options?: BuildOptions): Biscuit {
|
|
54
|
+
return new Biscuit(attenuate(this.bytes, code, options));
|
|
55
|
+
}
|
|
56
|
+
appendThirdParty(response: ThirdPartyResponse, options?: BuildOptions): Biscuit {
|
|
57
|
+
return new Biscuit(appendThirdParty(this.bytes, response, options));
|
|
58
|
+
}
|
|
59
|
+
thirdPartyRequest() {
|
|
60
|
+
return thirdPartyRequest(this.bytes);
|
|
61
|
+
}
|
|
62
|
+
seal(): Biscuit {
|
|
63
|
+
return new Biscuit(sealToken(this.bytes));
|
|
64
|
+
}
|
|
65
|
+
toBase64(): string {
|
|
66
|
+
return toBase64(this.bytes);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Verify the signature chain. Throws if the token is not authentic. */
|
|
70
|
+
verify(rootPublicKey: Uint8Array, rootAlgorithm: 0 | 1 = 0): VerifiedBiscuit {
|
|
71
|
+
return new VerifiedBiscuit(loadToken(this.bytes, rootPublicKey, rootAlgorithm), this.bytes);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A token whose signature chain has been checked against a root key. */
|
|
76
|
+
export class VerifiedBiscuit {
|
|
77
|
+
constructor(
|
|
78
|
+
readonly token: LoadedToken,
|
|
79
|
+
readonly bytes: Uint8Array,
|
|
80
|
+
) {}
|
|
81
|
+
get revocationIds(): string[] {
|
|
82
|
+
return this.token.revocationIds;
|
|
83
|
+
}
|
|
84
|
+
/** the issuer's key identifier, when the token carries one */
|
|
85
|
+
get rootKeyId(): number | undefined {
|
|
86
|
+
return this.token.rootKeyId;
|
|
87
|
+
}
|
|
88
|
+
authorize(authorizerCode: string, options?: AuthorizeOptions): AuthorizationResult {
|
|
89
|
+
return authorize(this.token, authorizerCode, options);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { generateKeypair, thirdPartyBlock };
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for the Biscuit Datalog text syntax (SPECIFICATIONS.md grammar).
|
|
3
|
+
* Hand-written recursive descent + precedence climbing; expressions are
|
|
4
|
+
* compiled straight to the opcode form the VM executes.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
BinaryOp as B,
|
|
8
|
+
type Check,
|
|
9
|
+
type CheckKind,
|
|
10
|
+
type Fact,
|
|
11
|
+
type MapKey,
|
|
12
|
+
normalizeSet,
|
|
13
|
+
type Op,
|
|
14
|
+
type Predicate,
|
|
15
|
+
type Rule,
|
|
16
|
+
type Scope,
|
|
17
|
+
type Term,
|
|
18
|
+
UnaryOp as U,
|
|
19
|
+
} from "./datalog.js";
|
|
20
|
+
|
|
21
|
+
export class ParseError extends Error {}
|
|
22
|
+
|
|
23
|
+
export type Statement =
|
|
24
|
+
| { k: "fact"; fact: Fact }
|
|
25
|
+
| { k: "rule"; rule: Rule }
|
|
26
|
+
| { k: "check"; check: Check }
|
|
27
|
+
| { k: "policy"; kind: "allow" | "deny"; queries: Rule[] }
|
|
28
|
+
| { k: "blockScope"; scopes: Scope[] };
|
|
29
|
+
|
|
30
|
+
const NAME_START = /[\p{L}]/u;
|
|
31
|
+
const NAME_CHAR = /[\p{L}\p{N}_:]/u;
|
|
32
|
+
|
|
33
|
+
export class Parser {
|
|
34
|
+
private i = 0;
|
|
35
|
+
private readonly vars: Map<string, number>;
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly src: string,
|
|
38
|
+
vars?: Map<string, number>,
|
|
39
|
+
) {
|
|
40
|
+
this.vars = vars ?? new Map();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** id -> name, for printing rules back out */
|
|
44
|
+
variableNames(): Map<number, string> {
|
|
45
|
+
const out = new Map<number, string>();
|
|
46
|
+
for (const [name, id] of this.vars) out.set(id, name);
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
varId(name: string): number {
|
|
51
|
+
let id = this.vars.get(name);
|
|
52
|
+
if (id === undefined) {
|
|
53
|
+
id = this.vars.size + 1;
|
|
54
|
+
this.vars.set(name, id);
|
|
55
|
+
}
|
|
56
|
+
return id;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ---------------------------------------------------------- lexing utils */
|
|
60
|
+
|
|
61
|
+
private ws(): void {
|
|
62
|
+
for (;;) {
|
|
63
|
+
while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
|
|
64
|
+
if (this.src.startsWith("//", this.i)) {
|
|
65
|
+
const nl = this.src.indexOf("\n", this.i);
|
|
66
|
+
this.i = nl === -1 ? this.src.length : nl + 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (this.src.startsWith("/*", this.i)) {
|
|
70
|
+
const end = this.src.indexOf("*/", this.i);
|
|
71
|
+
this.i = end === -1 ? this.src.length : end + 2;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
private get eof(): boolean {
|
|
78
|
+
this.ws();
|
|
79
|
+
return this.i >= this.src.length;
|
|
80
|
+
}
|
|
81
|
+
private peek(s: string): boolean {
|
|
82
|
+
this.ws();
|
|
83
|
+
return this.src.startsWith(s, this.i);
|
|
84
|
+
}
|
|
85
|
+
private eat(s: string): boolean {
|
|
86
|
+
if (!this.peek(s)) return false;
|
|
87
|
+
this.i += s.length;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
private expect(s: string): void {
|
|
91
|
+
if (!this.eat(s)) throw new ParseError(`expected "${s}" at offset ${this.i}`);
|
|
92
|
+
}
|
|
93
|
+
/** keyword: matches only if not followed by a name character */
|
|
94
|
+
private keyword(k: string): boolean {
|
|
95
|
+
this.ws();
|
|
96
|
+
if (!this.src.startsWith(k, this.i)) return false;
|
|
97
|
+
const after = this.src[this.i + k.length];
|
|
98
|
+
if (after !== undefined && NAME_CHAR.test(after)) return false;
|
|
99
|
+
this.i += k.length;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/** variable names, unlike predicate names, may start with a digit ($0) */
|
|
103
|
+
private variableName(): string {
|
|
104
|
+
this.ws();
|
|
105
|
+
const start = this.i;
|
|
106
|
+
while (this.i < this.src.length && NAME_CHAR.test(this.src[this.i])) this.i++;
|
|
107
|
+
if (this.i === start) throw new ParseError(`expected a variable name at offset ${this.i}`);
|
|
108
|
+
return this.src.slice(start, this.i);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private name(): string {
|
|
112
|
+
this.ws();
|
|
113
|
+
const start = this.i;
|
|
114
|
+
if (this.i >= this.src.length || !NAME_START.test(this.src[this.i]))
|
|
115
|
+
throw new ParseError(`expected a name at offset ${this.i}`);
|
|
116
|
+
this.i++;
|
|
117
|
+
while (this.i < this.src.length && NAME_CHAR.test(this.src[this.i])) this.i++;
|
|
118
|
+
return this.src.slice(start, this.i);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ----------------------------------------------------------------- terms */
|
|
122
|
+
|
|
123
|
+
private string(): string {
|
|
124
|
+
this.expect('"');
|
|
125
|
+
let out = "";
|
|
126
|
+
for (;;) {
|
|
127
|
+
if (this.i >= this.src.length) throw new ParseError("unterminated string");
|
|
128
|
+
const c = this.src[this.i++];
|
|
129
|
+
if (c === '"') return out;
|
|
130
|
+
if (c !== "\\") {
|
|
131
|
+
out += c;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const e = this.src[this.i++];
|
|
135
|
+
if (e === "n") out += "\n";
|
|
136
|
+
else if (e === "t") out += "\t";
|
|
137
|
+
else if (e === "r") out += "\r";
|
|
138
|
+
else if (e === "0") out += "\0";
|
|
139
|
+
else if (e === '"') out += '"';
|
|
140
|
+
else if (e === "\\") out += "\\";
|
|
141
|
+
else if (e === "u") {
|
|
142
|
+
const m = /^\{([0-9a-fA-F]+)\}/.exec(this.src.slice(this.i));
|
|
143
|
+
if (m) {
|
|
144
|
+
out += String.fromCodePoint(parseInt(m[1], 16));
|
|
145
|
+
this.i += m[0].length;
|
|
146
|
+
} else {
|
|
147
|
+
out += String.fromCharCode(parseInt(this.src.substr(this.i, 4), 16));
|
|
148
|
+
this.i += 4;
|
|
149
|
+
}
|
|
150
|
+
} else out += e;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private tryDate(): bigint | null {
|
|
155
|
+
this.ws();
|
|
156
|
+
const m = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})/.exec(
|
|
157
|
+
this.src.slice(this.i),
|
|
158
|
+
);
|
|
159
|
+
if (!m) return null;
|
|
160
|
+
const ms = Date.parse(m[0]);
|
|
161
|
+
if (Number.isNaN(ms)) throw new ParseError(`invalid date ${m[0]}`);
|
|
162
|
+
this.i += m[0].length;
|
|
163
|
+
return BigInt(Math.floor(ms / 1000));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** a term that may be a variable (rule/check context) */
|
|
167
|
+
term(allowVariables = true): Term {
|
|
168
|
+
this.ws();
|
|
169
|
+
const date = this.tryDate();
|
|
170
|
+
if (date !== null) return { t: "date", v: date };
|
|
171
|
+
|
|
172
|
+
if (this.peek('"')) return { t: "str", v: this.string() };
|
|
173
|
+
if (this.eat("$")) {
|
|
174
|
+
if (!allowVariables) throw new ParseError("variables are not allowed in facts");
|
|
175
|
+
return { t: "var", v: this.varId(this.variableName()) };
|
|
176
|
+
}
|
|
177
|
+
if (this.keyword("true")) return { t: "bool", v: true };
|
|
178
|
+
if (this.keyword("false")) return { t: "bool", v: false };
|
|
179
|
+
if (this.keyword("null")) return { t: "null" };
|
|
180
|
+
if (this.peek("hex:")) {
|
|
181
|
+
this.i += 4;
|
|
182
|
+
const start = this.i;
|
|
183
|
+
while (this.i < this.src.length && /[0-9a-fA-F]/.test(this.src[this.i])) this.i++;
|
|
184
|
+
const h = this.src.slice(start, this.i);
|
|
185
|
+
if (h.length % 2) throw new ParseError("odd-length hex literal");
|
|
186
|
+
const bytes = new Uint8Array(h.length / 2);
|
|
187
|
+
for (let j = 0; j < bytes.length; j++) bytes[j] = parseInt(h.substr(j * 2, 2), 16);
|
|
188
|
+
return { t: "bytes", v: bytes };
|
|
189
|
+
}
|
|
190
|
+
if (this.peek("[")) return this.array(allowVariables);
|
|
191
|
+
if (this.peek("{")) return this.setOrMap(allowVariables);
|
|
192
|
+
|
|
193
|
+
const m = /^-?\d+/.exec(this.src.slice(this.i));
|
|
194
|
+
if (m) {
|
|
195
|
+
this.i += m[0].length;
|
|
196
|
+
return { t: "int", v: BigInt(m[0]) };
|
|
197
|
+
}
|
|
198
|
+
throw new ParseError(
|
|
199
|
+
`unexpected term at offset ${this.i}: ${this.src.slice(this.i, this.i + 20)}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private array(allowVariables: boolean): Term {
|
|
204
|
+
this.expect("[");
|
|
205
|
+
const items: Term[] = [];
|
|
206
|
+
if (this.eat("]")) return { t: "array", v: items };
|
|
207
|
+
do {
|
|
208
|
+
items.push(this.term(allowVariables));
|
|
209
|
+
} while (this.eat(","));
|
|
210
|
+
this.expect("]");
|
|
211
|
+
return { t: "array", v: items };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private setOrMap(allowVariables: boolean): Term {
|
|
215
|
+
this.expect("{");
|
|
216
|
+
if (this.eat(",")) {
|
|
217
|
+
this.expect("}");
|
|
218
|
+
return { t: "set", v: [] }; // {,} is the empty set
|
|
219
|
+
}
|
|
220
|
+
if (this.eat("}")) return { t: "map", v: [] }; // {} is the empty map
|
|
221
|
+
const first = this.term(allowVariables);
|
|
222
|
+
if (this.eat(":")) {
|
|
223
|
+
const entries: [MapKey, Term][] = [[this.asMapKey(first), this.term(allowVariables)]];
|
|
224
|
+
while (this.eat(",")) {
|
|
225
|
+
if (this.peek("}")) break;
|
|
226
|
+
const k = this.asMapKey(this.term(allowVariables));
|
|
227
|
+
this.expect(":");
|
|
228
|
+
entries.push([k, this.term(allowVariables)]);
|
|
229
|
+
}
|
|
230
|
+
this.expect("}");
|
|
231
|
+
return { t: "map", v: entries };
|
|
232
|
+
}
|
|
233
|
+
const items = [first];
|
|
234
|
+
while (this.eat(",")) {
|
|
235
|
+
if (this.peek("}")) break;
|
|
236
|
+
items.push(this.term(allowVariables));
|
|
237
|
+
}
|
|
238
|
+
this.expect("}");
|
|
239
|
+
return { t: "set", v: normalizeSet(items) };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private asMapKey(t: Term): MapKey {
|
|
243
|
+
if (t.t === "int") return { t: "int", v: t.v };
|
|
244
|
+
if (t.t === "str") return { t: "str", v: t.v };
|
|
245
|
+
throw new ParseError("map keys must be integers or strings");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ------------------------------------------------------------ predicates */
|
|
249
|
+
|
|
250
|
+
predicate(allowVariables = true): Predicate {
|
|
251
|
+
const name = this.name();
|
|
252
|
+
this.expect("(");
|
|
253
|
+
const terms: Term[] = [];
|
|
254
|
+
if (!this.peek(")")) {
|
|
255
|
+
do {
|
|
256
|
+
terms.push(this.term(allowVariables));
|
|
257
|
+
} while (this.eat(","));
|
|
258
|
+
}
|
|
259
|
+
this.expect(")");
|
|
260
|
+
return { name, terms };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ----------------------------------------------------------- expressions */
|
|
264
|
+
|
|
265
|
+
/** precedence climbing; each level returns opcodes in postfix order */
|
|
266
|
+
expression(): Op[] {
|
|
267
|
+
return this.orExpr();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private orExpr(): Op[] {
|
|
271
|
+
let left = this.andExpr();
|
|
272
|
+
while (this.peek("||")) {
|
|
273
|
+
this.i += 2;
|
|
274
|
+
const right = this.andExpr();
|
|
275
|
+
left = [
|
|
276
|
+
...left,
|
|
277
|
+
{ kind: "closure", params: [], ops: right },
|
|
278
|
+
{ kind: "binary", op: B.LazyOr },
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
return left;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private andExpr(): Op[] {
|
|
285
|
+
let left = this.comparison();
|
|
286
|
+
while (this.peek("&&")) {
|
|
287
|
+
this.i += 2;
|
|
288
|
+
const right = this.comparison();
|
|
289
|
+
left = [
|
|
290
|
+
...left,
|
|
291
|
+
{ kind: "closure", params: [], ops: right },
|
|
292
|
+
{ kind: "binary", op: B.LazyAnd },
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
return left;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private comparison(): Op[] {
|
|
299
|
+
const left = this.bitXor();
|
|
300
|
+
// comparison operators are non-associative
|
|
301
|
+
for (const [tok, op] of [
|
|
302
|
+
["<=", B.LessOrEqual],
|
|
303
|
+
[">=", B.GreaterOrEqual],
|
|
304
|
+
["===", B.Equal],
|
|
305
|
+
["!==", B.NotEqual],
|
|
306
|
+
["==", B.HeterogeneousEqual],
|
|
307
|
+
["!=", B.HeterogeneousNotEqual],
|
|
308
|
+
["<", B.LessThan],
|
|
309
|
+
[">", B.GreaterThan],
|
|
310
|
+
] as [string, number][]) {
|
|
311
|
+
if (this.peekOperator(tok)) {
|
|
312
|
+
this.i += tok.length;
|
|
313
|
+
const right = this.bitXor();
|
|
314
|
+
return [...left, ...right, { kind: "binary", op }];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return left;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** avoids matching "==" when the source really has "===" */
|
|
321
|
+
private peekOperator(tok: string): boolean {
|
|
322
|
+
this.ws();
|
|
323
|
+
if (!this.src.startsWith(tok, this.i)) return false;
|
|
324
|
+
const next = this.src[this.i + tok.length];
|
|
325
|
+
if ((tok === "==" || tok === "!=") && next === "=") return false;
|
|
326
|
+
if ((tok === "<" || tok === ">") && next === "=") return false;
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private bitXor(): Op[] {
|
|
331
|
+
let left = this.bitOr();
|
|
332
|
+
while (this.peekOperator("^")) {
|
|
333
|
+
this.i += 1;
|
|
334
|
+
left = [...left, ...this.bitOr(), { kind: "binary", op: B.BitwiseXor }];
|
|
335
|
+
}
|
|
336
|
+
return left;
|
|
337
|
+
}
|
|
338
|
+
private bitOr(): Op[] {
|
|
339
|
+
let left = this.bitAnd();
|
|
340
|
+
while (this.peek("|") && !this.peek("||")) {
|
|
341
|
+
this.i += 1;
|
|
342
|
+
left = [...left, ...this.bitAnd(), { kind: "binary", op: B.BitwiseOr }];
|
|
343
|
+
}
|
|
344
|
+
return left;
|
|
345
|
+
}
|
|
346
|
+
private bitAnd(): Op[] {
|
|
347
|
+
let left = this.additive();
|
|
348
|
+
while (this.peek("&") && !this.peek("&&")) {
|
|
349
|
+
this.i += 1;
|
|
350
|
+
left = [...left, ...this.additive(), { kind: "binary", op: B.BitwiseAnd }];
|
|
351
|
+
}
|
|
352
|
+
return left;
|
|
353
|
+
}
|
|
354
|
+
private additive(): Op[] {
|
|
355
|
+
let left = this.multiplicative();
|
|
356
|
+
for (;;) {
|
|
357
|
+
if (this.peekSign("+")) {
|
|
358
|
+
this.i += 1;
|
|
359
|
+
left = [...left, ...this.multiplicative(), { kind: "binary", op: B.Add }];
|
|
360
|
+
} else if (this.peekSign("-")) {
|
|
361
|
+
this.i += 1;
|
|
362
|
+
left = [...left, ...this.multiplicative(), { kind: "binary", op: B.Sub }];
|
|
363
|
+
} else return left;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/** `-` starts a negative literal only when it is not an infix position */
|
|
367
|
+
private peekSign(tok: string): boolean {
|
|
368
|
+
this.ws();
|
|
369
|
+
return this.src.startsWith(tok, this.i);
|
|
370
|
+
}
|
|
371
|
+
private multiplicative(): Op[] {
|
|
372
|
+
let left = this.unary();
|
|
373
|
+
for (;;) {
|
|
374
|
+
if (this.peek("*")) {
|
|
375
|
+
this.i += 1;
|
|
376
|
+
left = [...left, ...this.unary(), { kind: "binary", op: B.Mul }];
|
|
377
|
+
} else if (this.peek("/")) {
|
|
378
|
+
this.i += 1;
|
|
379
|
+
left = [...left, ...this.unary(), { kind: "binary", op: B.Div }];
|
|
380
|
+
} else return left;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private unary(): Op[] {
|
|
385
|
+
if (this.eat("!")) {
|
|
386
|
+
const inner = this.unary();
|
|
387
|
+
return [...inner, { kind: "unary", op: U.Negate }];
|
|
388
|
+
}
|
|
389
|
+
return this.methods(this.primary());
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private primary(): Op[] {
|
|
393
|
+
this.ws();
|
|
394
|
+
if (this.peek("(")) {
|
|
395
|
+
this.i += 1;
|
|
396
|
+
const inner = this.expression();
|
|
397
|
+
this.expect(")");
|
|
398
|
+
return inner;
|
|
399
|
+
}
|
|
400
|
+
return [{ kind: "value", value: this.term() }];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private methods(target: Op[]): Op[] {
|
|
404
|
+
let out = target;
|
|
405
|
+
while (this.peek(".")) {
|
|
406
|
+
this.i += 1;
|
|
407
|
+
if (this.peek("extern::")) {
|
|
408
|
+
this.i += 8;
|
|
409
|
+
const fn = this.name();
|
|
410
|
+
this.expect("(");
|
|
411
|
+
if (this.eat(")")) out = [...out, { kind: "unary", op: U.Ffi, ffi: fn }];
|
|
412
|
+
else {
|
|
413
|
+
const arg = this.expression();
|
|
414
|
+
this.expect(")");
|
|
415
|
+
out = [...out, ...arg, { kind: "binary", op: B.Ffi, ffi: fn }];
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const method = this.name();
|
|
420
|
+
this.expect("(");
|
|
421
|
+
switch (method) {
|
|
422
|
+
case "length":
|
|
423
|
+
this.expect(")");
|
|
424
|
+
out = [...out, { kind: "unary", op: U.Length }];
|
|
425
|
+
break;
|
|
426
|
+
case "type":
|
|
427
|
+
this.expect(")");
|
|
428
|
+
out = [...out, { kind: "unary", op: U.TypeOf }];
|
|
429
|
+
break;
|
|
430
|
+
case "any":
|
|
431
|
+
case "all": {
|
|
432
|
+
const closure = this.closure();
|
|
433
|
+
this.expect(")");
|
|
434
|
+
out = [...out, closure, { kind: "binary", op: method === "any" ? B.Any : B.All }];
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
case "try_or": {
|
|
438
|
+
const fallback = this.expression();
|
|
439
|
+
this.expect(")");
|
|
440
|
+
// X.try_or(Y) => [closure(X), Y, TryOr] : the closure is what gets tried
|
|
441
|
+
out = [
|
|
442
|
+
{ kind: "closure", params: [], ops: out },
|
|
443
|
+
...fallback,
|
|
444
|
+
{ kind: "binary", op: B.TryOr },
|
|
445
|
+
];
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
default: {
|
|
449
|
+
const arg = this.expression();
|
|
450
|
+
this.expect(")");
|
|
451
|
+
const op = {
|
|
452
|
+
contains: B.Contains,
|
|
453
|
+
starts_with: B.Prefix,
|
|
454
|
+
ends_with: B.Suffix,
|
|
455
|
+
matches: B.Regex,
|
|
456
|
+
intersection: B.Intersection,
|
|
457
|
+
union: B.Union,
|
|
458
|
+
get: B.Get,
|
|
459
|
+
}[method];
|
|
460
|
+
if (op === undefined) throw new ParseError(`unknown method ${method}`);
|
|
461
|
+
out = [...out, ...arg, { kind: "binary", op }];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private closure(): Op {
|
|
469
|
+
const params: number[] = [];
|
|
470
|
+
this.ws();
|
|
471
|
+
if (this.eat("(")) {
|
|
472
|
+
this.expect(")");
|
|
473
|
+
} else {
|
|
474
|
+
do {
|
|
475
|
+
this.expect("$");
|
|
476
|
+
params.push(this.varId(this.variableName()));
|
|
477
|
+
} while (this.eat(","));
|
|
478
|
+
}
|
|
479
|
+
this.expect("->");
|
|
480
|
+
return { kind: "closure", params, ops: this.expression() };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* ---------------------------------------------------------------- scopes */
|
|
484
|
+
|
|
485
|
+
private scopes(): Scope[] {
|
|
486
|
+
const out: Scope[] = [];
|
|
487
|
+
do {
|
|
488
|
+
this.ws();
|
|
489
|
+
if (this.keyword("authority")) out.push({ kind: "authority" });
|
|
490
|
+
else if (this.keyword("previous")) out.push({ kind: "previous" });
|
|
491
|
+
else {
|
|
492
|
+
const alg = this.name();
|
|
493
|
+
this.expect("/");
|
|
494
|
+
const start = this.i;
|
|
495
|
+
while (this.i < this.src.length && /[0-9a-fA-F]/.test(this.src[this.i])) this.i++;
|
|
496
|
+
out.push({
|
|
497
|
+
kind: "publicKey",
|
|
498
|
+
key: `${alg}/${this.src.slice(start, this.i).toLowerCase()}`,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
} while (this.eat(","));
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* ------------------------------------------------------------ statements */
|
|
506
|
+
|
|
507
|
+
/** body of a rule / check / policy: predicates, expressions, trusting */
|
|
508
|
+
private ruleBody(): { body: Predicate[]; expressions: Op[][]; scopes: Scope[] } {
|
|
509
|
+
const body: Predicate[] = [];
|
|
510
|
+
const expressions: Op[][] = [];
|
|
511
|
+
let scopes: Scope[] = [];
|
|
512
|
+
do {
|
|
513
|
+
this.ws();
|
|
514
|
+
if (this.keyword("trusting")) {
|
|
515
|
+
scopes = this.scopes();
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
const save = this.i;
|
|
519
|
+
const asPredicate = this.tryPredicate();
|
|
520
|
+
if (asPredicate) body.push(asPredicate);
|
|
521
|
+
else {
|
|
522
|
+
this.i = save;
|
|
523
|
+
expressions.push(this.expression());
|
|
524
|
+
}
|
|
525
|
+
this.ws();
|
|
526
|
+
if (this.keyword("trusting")) {
|
|
527
|
+
scopes = this.scopes();
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
} while (this.eat(","));
|
|
531
|
+
return { body, expressions, scopes };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** a predicate is only a predicate if nothing operator-like follows it */
|
|
535
|
+
private tryPredicate(): Predicate | null {
|
|
536
|
+
const save = this.i;
|
|
537
|
+
try {
|
|
538
|
+
this.ws();
|
|
539
|
+
if (!NAME_START.test(this.src[this.i] ?? "")) return null;
|
|
540
|
+
const p = this.predicate();
|
|
541
|
+
this.ws();
|
|
542
|
+
const rest = this.src.slice(this.i);
|
|
543
|
+
if (rest === "" || /^[,;)]/.test(rest) || /^(or|trusting)\b/.test(rest)) return p;
|
|
544
|
+
this.i = save;
|
|
545
|
+
return null;
|
|
546
|
+
} catch {
|
|
547
|
+
this.i = save;
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private queries(): Rule[] {
|
|
553
|
+
const out: Rule[] = [];
|
|
554
|
+
do {
|
|
555
|
+
const { body, expressions, scopes } = this.ruleBody();
|
|
556
|
+
out.push({ head: { name: "query", terms: [] }, body, expressions, scopes });
|
|
557
|
+
} while (this.keyword("or"));
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
statement(): Statement {
|
|
562
|
+
this.ws();
|
|
563
|
+
if (this.keyword("check")) {
|
|
564
|
+
let kind: CheckKind;
|
|
565
|
+
if (this.keyword("if")) kind = "one";
|
|
566
|
+
else if (this.keyword("all")) kind = "all";
|
|
567
|
+
else throw new ParseError('expected "if" or "all" after "check"');
|
|
568
|
+
return { k: "check", check: { queries: this.queries(), kind } };
|
|
569
|
+
}
|
|
570
|
+
if (this.keyword("reject")) {
|
|
571
|
+
if (!this.keyword("if")) throw new ParseError('expected "if" after "reject"');
|
|
572
|
+
return { k: "check", check: { queries: this.queries(), kind: "reject" } };
|
|
573
|
+
}
|
|
574
|
+
if (this.keyword("allow")) {
|
|
575
|
+
if (!this.keyword("if")) throw new ParseError('expected "if" after "allow"');
|
|
576
|
+
return { k: "policy", kind: "allow", queries: this.queries() };
|
|
577
|
+
}
|
|
578
|
+
if (this.keyword("deny")) {
|
|
579
|
+
if (!this.keyword("if")) throw new ParseError('expected "if" after "deny"');
|
|
580
|
+
return { k: "policy", kind: "deny", queries: this.queries() };
|
|
581
|
+
}
|
|
582
|
+
if (this.keyword("trusting")) return { k: "blockScope", scopes: this.scopes() };
|
|
583
|
+
|
|
584
|
+
const head = this.predicate();
|
|
585
|
+
this.ws();
|
|
586
|
+
if (this.eat("<-")) {
|
|
587
|
+
const { body, expressions, scopes } = this.ruleBody();
|
|
588
|
+
return { k: "rule", rule: { head, body, expressions, scopes } };
|
|
589
|
+
}
|
|
590
|
+
if (head.terms.some((t) => t.t === "var"))
|
|
591
|
+
throw new ParseError("a fact cannot contain variables");
|
|
592
|
+
return { k: "fact", fact: { predicate: head } };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
parse(): Statement[] {
|
|
596
|
+
const out: Statement[] = [];
|
|
597
|
+
while (!this.eof) {
|
|
598
|
+
out.push(this.statement());
|
|
599
|
+
this.ws();
|
|
600
|
+
this.expect(";");
|
|
601
|
+
}
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export const parse = (src: string): Statement[] => new Parser(src).parse();
|