attenu-guard 0.12.0 → 0.13.1
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/CHANGELOG.md +72 -0
- package/README.md +27 -0
- package/dist/cjs/audit.d.ts.map +1 -1
- package/dist/cjs/audit.js +45 -11
- package/dist/cjs/audit.js.map +1 -1
- package/dist/cjs/authority.d.ts +12 -6
- package/dist/cjs/authority.d.ts.map +1 -1
- package/dist/cjs/authority.js +66 -24
- package/dist/cjs/authority.js.map +1 -1
- package/dist/cjs/canonical.d.ts +24 -0
- package/dist/cjs/canonical.d.ts.map +1 -1
- package/dist/cjs/canonical.js +49 -0
- package/dist/cjs/canonical.js.map +1 -1
- package/dist/cjs/ceilings.d.ts +71 -9
- package/dist/cjs/ceilings.d.ts.map +1 -1
- package/dist/cjs/ceilings.js +432 -62
- package/dist/cjs/ceilings.js.map +1 -1
- package/dist/cjs/cli.d.ts +12 -3
- package/dist/cjs/cli.d.ts.map +1 -1
- package/dist/cjs/cli.js +272 -20
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/display.d.ts +77 -0
- package/dist/cjs/display.d.ts.map +1 -0
- package/dist/cjs/display.js +239 -0
- package/dist/cjs/display.js.map +1 -0
- package/dist/cjs/evidence.d.ts +74 -5
- package/dist/cjs/evidence.d.ts.map +1 -1
- package/dist/cjs/evidence.js +656 -244
- package/dist/cjs/evidence.js.map +1 -1
- package/dist/cjs/guard.d.ts.map +1 -1
- package/dist/cjs/guard.js +7 -3
- package/dist/cjs/guard.js.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/wire.d.ts.map +1 -1
- package/dist/cjs/wire.js +3 -2
- package/dist/cjs/wire.js.map +1 -1
- package/dist/esm/audit.d.ts.map +1 -1
- package/dist/esm/audit.js +46 -12
- package/dist/esm/audit.js.map +1 -1
- package/dist/esm/authority.d.ts +12 -6
- package/dist/esm/authority.d.ts.map +1 -1
- package/dist/esm/authority.js +66 -24
- package/dist/esm/authority.js.map +1 -1
- package/dist/esm/canonical.d.ts +24 -0
- package/dist/esm/canonical.d.ts.map +1 -1
- package/dist/esm/canonical.js +46 -0
- package/dist/esm/canonical.js.map +1 -1
- package/dist/esm/ceilings.d.ts +71 -9
- package/dist/esm/ceilings.d.ts.map +1 -1
- package/dist/esm/ceilings.js +428 -62
- package/dist/esm/ceilings.js.map +1 -1
- package/dist/esm/cli.d.ts +12 -3
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +273 -21
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/display.d.ts +77 -0
- package/dist/esm/display.d.ts.map +1 -0
- package/dist/esm/display.js +227 -0
- package/dist/esm/display.js.map +1 -0
- package/dist/esm/evidence.d.ts +74 -5
- package/dist/esm/evidence.d.ts.map +1 -1
- package/dist/esm/evidence.js +639 -230
- package/dist/esm/evidence.js.map +1 -1
- package/dist/esm/guard.d.ts.map +1 -1
- package/dist/esm/guard.js +8 -4
- package/dist/esm/guard.js.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/wire.d.ts.map +1 -1
- package/dist/esm/wire.js +3 -2
- package/dist/esm/wire.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/ceilings.js
CHANGED
|
@@ -16,13 +16,18 @@
|
|
|
16
16
|
* silently dropped.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.UnknownCeiling = exports.Prefix = exports.Deny = exports.Allow = exports.EgressRank = exports.CallLimit = exports.SpendCap = exports.RowLimit = void 0;
|
|
19
|
+
exports.UnknownCeiling = exports.Prefix = exports.Deny = exports.Allow = exports.EgressRank = exports.CallLimit = exports.SpendCap = exports.RowLimit = exports.SCOPE_RE = void 0;
|
|
20
20
|
exports.ctxFieldOf = ctxFieldOf;
|
|
21
|
+
exports.sameType = sameType;
|
|
21
22
|
exports.describe = describe;
|
|
23
|
+
exports.wireKeyOf = wireKeyOf;
|
|
24
|
+
exports.describeInFinding = describeInFinding;
|
|
22
25
|
exports.isMetered = isMetered;
|
|
26
|
+
exports.ownValue = ownValue;
|
|
23
27
|
exports.registerCeiling = registerCeiling;
|
|
24
28
|
exports.ceilingFromWire = ceilingFromWire;
|
|
25
29
|
const canonical_js_1 = require("./canonical.js");
|
|
30
|
+
const display_js_1 = require("./display.js");
|
|
26
31
|
const reasons_js_1 = require("./reasons.js");
|
|
27
32
|
/**
|
|
28
33
|
* Reject a ceiling bound whose magnitude can't survive the signing surface
|
|
@@ -39,6 +44,12 @@ function validateSafeNumber(key, value) {
|
|
|
39
44
|
"binary64 signing surface (RFC 8785)");
|
|
40
45
|
}
|
|
41
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The scope grammar the draft defines: lowercase dot-separated segments, `*` only as the whole last
|
|
49
|
+
* segment. `Authority` validates its scopes against it, and a scoped `CallLimit`'s `appliesTo`
|
|
50
|
+
* follows it too.
|
|
51
|
+
*/
|
|
52
|
+
exports.SCOPE_RE = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*\.(?:[a-z][a-z0-9_-]*|\*)$/;
|
|
42
53
|
// Ordered enum for egress: index 0 is the strictest. A value outside this
|
|
43
54
|
// vocabulary is treated as maximally permissive-requested (worst case), so a
|
|
44
55
|
// garbage or unknown egress value fails closed rather than silently passing.
|
|
@@ -47,6 +58,93 @@ function egressRankOf(value) {
|
|
|
47
58
|
const i = EGRESS_ORDER.indexOf(value);
|
|
48
59
|
return i === -1 ? EGRESS_ORDER.length : i;
|
|
49
60
|
}
|
|
61
|
+
/** The JSON kinds a ceiling compares, as `jsonKind` names them. */
|
|
62
|
+
const NUMBER = ["a number"];
|
|
63
|
+
const STRING = ["a string"];
|
|
64
|
+
const SCALAR = ["a string", "a number", "a boolean"];
|
|
65
|
+
/**
|
|
66
|
+
* `value`'s JSON type as a refusal names it: null, a boolean, a number, a string, an array or an
|
|
67
|
+
* object, or "a value that is not JSON" for anything else passed in-process (a bigint). A number
|
|
68
|
+
* this library's `parseJson` read, a `RawNumber`, is a number. The Python implementation names the
|
|
69
|
+
* same values the same way.
|
|
70
|
+
*/
|
|
71
|
+
function jsonKind(value) {
|
|
72
|
+
if (value === null)
|
|
73
|
+
return "null";
|
|
74
|
+
if (typeof value === "boolean")
|
|
75
|
+
return "a boolean";
|
|
76
|
+
if (typeof value === "number" || value instanceof canonical_js_1.RawNumber)
|
|
77
|
+
return "a number";
|
|
78
|
+
if (typeof value === "string")
|
|
79
|
+
return "a string";
|
|
80
|
+
if (Array.isArray(value))
|
|
81
|
+
return "an array";
|
|
82
|
+
if (typeof value === "object")
|
|
83
|
+
return "an object";
|
|
84
|
+
return "a value that is not JSON";
|
|
85
|
+
}
|
|
86
|
+
/** `value`, or the number it holds when `parseJson` read it as a `RawNumber`. */
|
|
87
|
+
function plain(value) {
|
|
88
|
+
return value instanceof canonical_js_1.RawNumber ? value.value : value;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `jsonKind(value)` when a ceiling that compares `accepted` cannot compare `value`, or `null`.
|
|
92
|
+
*
|
|
93
|
+
* Every built-in ceiling refuses such a request value rather than coercing it (attenu-ops#110):
|
|
94
|
+
* `"50"`, `[50]` and `true` passed a row cap through `<=`, `["/tmp/x"]` passed the prefix "/tmp/" as
|
|
95
|
+
* the text "/tmp/x", and `true` passed the prefix "t" as "true".
|
|
96
|
+
*/
|
|
97
|
+
function wrongKind(value, accepted) {
|
|
98
|
+
const kind = jsonKind(value);
|
|
99
|
+
return accepted.includes(kind) ? null : kind;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A denial's message: none when the value had the right type, as before; for one of the wrong type,
|
|
103
|
+
* `<kind> cannot be compared with <against>; refused`, one wording per ceiling kind, and the Python
|
|
104
|
+
* implementation's too.
|
|
105
|
+
*/
|
|
106
|
+
function refusal(kind, against) {
|
|
107
|
+
return kind === null ? "" : `${kind} cannot be compared with ${against}; refused`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The error for a constraint member of the wrong type, `<member> of constraint <key> is <kind>, not
|
|
111
|
+
* <expected>`, with the key as Python's repr prints it: the Python implementation's text, where it
|
|
112
|
+
* raises ValueError. A member the constraint does not carry is `absent`.
|
|
113
|
+
*
|
|
114
|
+
* A bound of the wrong type is malformed (attenu-ops#110), on every path: the draft makes "max" a
|
|
115
|
+
* number and "prefix" a string, an egress rank outside none < internal < any ranked above "any" and
|
|
116
|
+
* admitted every request, and a `field` or `applies_to` that is not a string named a different
|
|
117
|
+
* context field in each implementation. A token carrying one is refused as malformed, and a bundle
|
|
118
|
+
* reports the authority unreadable.
|
|
119
|
+
*/
|
|
120
|
+
function malformed(key, member, value, expected, shownAs) {
|
|
121
|
+
const kind = value === undefined ? "absent" : (shownAs ?? jsonKind(value));
|
|
122
|
+
return new TypeError(`${member} of constraint ${(0, display_js_1.pyRepr)(key)} is ${kind}, not ${expected}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A constraint's key is a string: it names the dimension ceilings pair by and, unless `field` says
|
|
126
|
+
* otherwise, the context field the ceiling reads. A number, null or a boolean key was read as
|
|
127
|
+
* String(key) here, so 5 and "5" were one dimension, and an absent key loaded and read the field
|
|
128
|
+
* "undefined"; the Python implementation read a field no JSON context carries. Every constraint is
|
|
129
|
+
* refused without one, an unknown type included, in the Python implementation's words.
|
|
130
|
+
*/
|
|
131
|
+
function checkKey(key) {
|
|
132
|
+
if (typeof key !== "string") {
|
|
133
|
+
throw new TypeError(`key of a constraint is ${key === undefined ? "absent" : jsonKind(key)}, not a string`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** A `max` is a JSON number. */
|
|
137
|
+
function checkMax(key, value) {
|
|
138
|
+
if (value === undefined || jsonKind(value) !== "a number")
|
|
139
|
+
throw malformed(key, "max", value, "a number");
|
|
140
|
+
}
|
|
141
|
+
/** A `prefix`, `field` or `applies_to` is a string; `field` and `applies_to` may be null, unset. */
|
|
142
|
+
function checkString(key, member, value, optional = false) {
|
|
143
|
+
if (optional && (value === null || value === undefined))
|
|
144
|
+
return;
|
|
145
|
+
if (typeof value !== "string")
|
|
146
|
+
throw malformed(key, member, value, "a string");
|
|
147
|
+
}
|
|
50
148
|
/**
|
|
51
149
|
* The request-context field a ceiling reads. Prefers an explicit `ctxField`,
|
|
52
150
|
* then the caller-keyed `field`, then the ceiling's own `key`.
|
|
@@ -58,11 +156,73 @@ function ctxFieldOf(ceiling) {
|
|
|
58
156
|
return ceiling.field;
|
|
59
157
|
return ceiling.key;
|
|
60
158
|
}
|
|
61
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* Python's `type(a) is type(b)`: whether two ceilings are of one class. Ceilings pair by key, and
|
|
161
|
+
* two of different classes under one key are not comparable: neither narrows the other, so an
|
|
162
|
+
* authority holding one is not narrower than an authority holding the other (attenu-ops#110).
|
|
163
|
+
*/
|
|
164
|
+
function sameType(a, b) {
|
|
165
|
+
return Object.getPrototypeOf(a) === Object.getPrototypeOf(b);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Uniform human-readable rendering of any ceiling. Never parsed back. A ceiling without its own
|
|
169
|
+
* `describe`, such as one this build does not define, prints as the Python implementation prints
|
|
170
|
+
* it: its key as the wire carried it, `=`, and its wire form as Python prints a dict.
|
|
171
|
+
*/
|
|
62
172
|
function describe(ceiling) {
|
|
63
173
|
if (typeof ceiling.describe === "function")
|
|
64
174
|
return ceiling.describe();
|
|
65
|
-
return `${ceiling
|
|
175
|
+
return `${(0, display_js_1.pyStr)(wireKeyOf(ceiling))}=${(0, display_js_1.pyRepr)(ceiling.toWire())}`;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* A ceiling's key as the wire carried it, which is what a finding names. A ceiling this build does
|
|
179
|
+
* not define keeps whatever its key was, null included, where its `key` property is text: the
|
|
180
|
+
* Python implementation prints a null key as `None`.
|
|
181
|
+
*/
|
|
182
|
+
function wireKeyOf(ceiling) {
|
|
183
|
+
return ceiling instanceof UnknownCeiling ? (ceiling.raw["key"] ?? null) : ceiling.key;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* A ceiling as a verifier finding prints it: `describe`'s text, with every value the bundle supplied
|
|
187
|
+
* printed through `shown` (display.ts), so the finding stays on one line.
|
|
188
|
+
*
|
|
189
|
+
* `describe` itself is left alone, so dashboards and `Authority.describe()` print a region called
|
|
190
|
+
* "São Paulo" as it is, and for values in the bare set the two agree character for character, except
|
|
191
|
+
* that an allow-list's or a deny-list's string members are printed in their escaped JSON form, quoted,
|
|
192
|
+
* so the string "1" and the number 1 read differently. A ceiling this build does not define prints the
|
|
193
|
+
* Python implementation's description of it — its key, `=`, and the wire object as Python prints a
|
|
194
|
+
* dict — as it is when that is printable ASCII, spaces included, and as escaped JSON otherwise. Same
|
|
195
|
+
* text as the Python implementation's finding.
|
|
196
|
+
*/
|
|
197
|
+
function describeInFinding(ceiling) {
|
|
198
|
+
// A bound the wire supplied: a number prints as `describe` prints it; anything else is shown.
|
|
199
|
+
const bound = (v) => (typeof v === "number" ? (0, canonical_js_1.pyNumber)(v) : (0, display_js_1.shown)(v));
|
|
200
|
+
// The typed members in wire order, as `describe` lists them, except that a string member is printed
|
|
201
|
+
// in its escaped JSON form, quoted, so a finding tells the string "1" from the number 1
|
|
202
|
+
// (attenu-ops#110). That form is ASCII, the Python implementation's on every Python version; its
|
|
203
|
+
// repr printed a printable non-ASCII character as it is. Every other member's text is bare.
|
|
204
|
+
const members = (values) => inWireOrder(values)
|
|
205
|
+
.map((v) => (typeof v === "string" ? (0, display_js_1.escaped)(v) : (0, display_js_1.shownText)(strOf(v), v)))
|
|
206
|
+
.join(", ");
|
|
207
|
+
// By exact class, as the Python implementation matches by exact type: a subclass of a built-in
|
|
208
|
+
// describes itself, and is printed the way any other ceiling this build does not define is.
|
|
209
|
+
const kind = ceiling.constructor;
|
|
210
|
+
if (kind === RowLimit)
|
|
211
|
+
return `${(0, display_js_1.shown)(ceiling.key)}<=${bound(ceiling.maxRows)}`;
|
|
212
|
+
if (kind === SpendCap)
|
|
213
|
+
return `${(0, display_js_1.shown)(ceiling.key)}<=${bound(ceiling.maxSpend)}`;
|
|
214
|
+
if (kind === CallLimit)
|
|
215
|
+
return `${(0, display_js_1.shown)(ceiling.key)}<=${bound(ceiling.maxCalls)}`;
|
|
216
|
+
if (kind === EgressRank)
|
|
217
|
+
return `${(0, display_js_1.shown)(ceiling.key)}<=${(0, display_js_1.shown)(ceiling.level)}`;
|
|
218
|
+
if (kind === Allow)
|
|
219
|
+
return `${(0, display_js_1.shown)(ceiling.key)} in [${members(ceiling.oneOf)}]`;
|
|
220
|
+
if (kind === Deny)
|
|
221
|
+
return `${(0, display_js_1.shown)(ceiling.key)} not in [${members(ceiling.notOneOf)}]`;
|
|
222
|
+
if (kind === Prefix)
|
|
223
|
+
return `${(0, display_js_1.shown)(ceiling.key)} startswith ${(0, display_js_1.shown)(ceiling.prefix)}`;
|
|
224
|
+
const text = describe(ceiling);
|
|
225
|
+
return /^[ -~]*$/.test(text) ? text : (0, display_js_1.escaped)(text);
|
|
66
226
|
}
|
|
67
227
|
/**
|
|
68
228
|
* A METERED ceiling bounds a consumed quantity the caller must declare (rows
|
|
@@ -73,8 +233,136 @@ function describe(ceiling) {
|
|
|
73
233
|
function isMetered(ceiling) {
|
|
74
234
|
return Boolean(ceiling.metered) || String(ceiling.key).startsWith("max_");
|
|
75
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* What a refusal calls a `one_of` / `not_one_of` that is not a list of members, or `null` for one
|
|
238
|
+
* that is: a JSON array, or in-process any other iterable of members (a Set).
|
|
239
|
+
*
|
|
240
|
+
* The draft defines both as an array. null used to become the empty list, a string its
|
|
241
|
+
* characters, and an object threw a TypeError in its own words; an absent list read as an empty
|
|
242
|
+
* one, so an absent deny-list bounded nothing. Each is malformed now, as in the Python
|
|
243
|
+
* implementation: a token carrying one is refused as malformed, and a bundle reports the authority
|
|
244
|
+
* unreadable.
|
|
245
|
+
*/
|
|
246
|
+
function notAnArray(values) {
|
|
247
|
+
if (values === undefined)
|
|
248
|
+
return "absent";
|
|
249
|
+
if (values === null)
|
|
250
|
+
return "null";
|
|
251
|
+
if (typeof values === "boolean")
|
|
252
|
+
return "a boolean";
|
|
253
|
+
if (typeof values === "number")
|
|
254
|
+
return "a number";
|
|
255
|
+
if (typeof values === "string")
|
|
256
|
+
return "a string";
|
|
257
|
+
if (Array.isArray(values))
|
|
258
|
+
return null;
|
|
259
|
+
if (values instanceof Map)
|
|
260
|
+
return "an object";
|
|
261
|
+
if (typeof values === "object" && typeof values[Symbol.iterator] === "function")
|
|
262
|
+
return null;
|
|
263
|
+
if (typeof values === "object")
|
|
264
|
+
return "an object";
|
|
265
|
+
return "a value that is not an array";
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* `values` as the members of an `Allow` or a `Deny`, a number `parseJson` read as the number it
|
|
269
|
+
* holds. Throws a TypeError naming the list and the key when `values` is not a list of members
|
|
270
|
+
* (`notAnArray`); the Python implementation raises ValueError with the same text.
|
|
271
|
+
*/
|
|
272
|
+
function memberList(key, listName, values) {
|
|
273
|
+
const kind = notAnArray(values);
|
|
274
|
+
if (kind !== null)
|
|
275
|
+
throw new TypeError(`${listName} of constraint ${(0, display_js_1.pyRepr)(key)} is ${kind}, not an array`);
|
|
276
|
+
return Array.from(values, (v) => plain(v));
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The context's own value for `field`, or `undefined`. A plain object inherits `constructor`,
|
|
280
|
+
* `toString` and the rest from Object.prototype, and a field named like one of them is absent unless
|
|
281
|
+
* the context holds it, as `ctx.get(field)` reads it in the Python implementation.
|
|
282
|
+
*/
|
|
283
|
+
function ownValue(ctx, field) {
|
|
284
|
+
return Object.prototype.hasOwnProperty.call(ctx, field) ? ctx[field] : undefined;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Where two members print alike ("1" and 1), JSON type decides their order: null, boolean, number,
|
|
288
|
+
* string, then anything else.
|
|
289
|
+
*/
|
|
290
|
+
function kindRank(value) {
|
|
291
|
+
if (value === null)
|
|
292
|
+
return 0;
|
|
293
|
+
if (typeof value === "boolean")
|
|
294
|
+
return 1;
|
|
295
|
+
if (typeof value === "number")
|
|
296
|
+
return 2;
|
|
297
|
+
if (typeof value === "string")
|
|
298
|
+
return 3;
|
|
299
|
+
return 4;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* `values` as the wire form, a denial's `limit` and `describe()` list them: sorted by `strOf`, then
|
|
303
|
+
* by JSON type (`kindRank`). That is a total order on distinct members, so an equal member set
|
|
304
|
+
* re-emits the same bytes whatever order it arrived in, in both implementations; ties used to keep
|
|
305
|
+
* their arrival order.
|
|
306
|
+
*/
|
|
76
307
|
function sortByStr(values) {
|
|
77
|
-
return Array.from(values).sort((a, b) => (0, canonical_js_1.compareCodePoints)(strOf(a), strOf(b)));
|
|
308
|
+
return Array.from(values).sort((a, b) => (0, canonical_js_1.compareCodePoints)(strOf(a), strOf(b)) || kindRank(a) - kindRank(b));
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The members of an `Allow` or a `Deny`: a set that cannot change once built, as the Python
|
|
312
|
+
* implementation's member set has no mutators. `add`, `delete` and `clear` throw a TypeError, and the
|
|
313
|
+
* members live in a private Set no Set method can reach, so the wire order computed once
|
|
314
|
+
* (`inWireOrder`) cannot go stale: a plain Set a caller could still add to let `permits()` decide by
|
|
315
|
+
* members the wire did not list (attenu-ops#110).
|
|
316
|
+
*/
|
|
317
|
+
class Members {
|
|
318
|
+
#values;
|
|
319
|
+
constructor(values) {
|
|
320
|
+
this.#values = new Set(values);
|
|
321
|
+
}
|
|
322
|
+
get size() {
|
|
323
|
+
return this.#values.size;
|
|
324
|
+
}
|
|
325
|
+
has(value) {
|
|
326
|
+
return this.#values.has(value);
|
|
327
|
+
}
|
|
328
|
+
forEach(callback, thisArg) {
|
|
329
|
+
this.#values.forEach((value) => callback.call(thisArg, value, value, this));
|
|
330
|
+
}
|
|
331
|
+
entries() {
|
|
332
|
+
return this.#values.entries();
|
|
333
|
+
}
|
|
334
|
+
keys() {
|
|
335
|
+
return this.#values.keys();
|
|
336
|
+
}
|
|
337
|
+
values() {
|
|
338
|
+
return this.#values.values();
|
|
339
|
+
}
|
|
340
|
+
[Symbol.iterator]() {
|
|
341
|
+
return this.#values.values();
|
|
342
|
+
}
|
|
343
|
+
add(_value) {
|
|
344
|
+
throw new TypeError("the members of an allow-list or a deny-list are read-only");
|
|
345
|
+
}
|
|
346
|
+
delete(_value) {
|
|
347
|
+
throw new TypeError("the members of an allow-list or a deny-list are read-only");
|
|
348
|
+
}
|
|
349
|
+
clear() {
|
|
350
|
+
throw new TypeError("the members of an allow-list or a deny-list are read-only");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The wire order (`sortByStr`) of an `Allow`'s or a `Deny`'s members, computed once per member set: a
|
|
355
|
+
* denial, `toWire()` and `describe()` each sorted the whole set again. The members cannot change
|
|
356
|
+
* (`Members`); each caller gets its own copy of the order.
|
|
357
|
+
*/
|
|
358
|
+
const wireOrders = new WeakMap();
|
|
359
|
+
function inWireOrder(members) {
|
|
360
|
+
let order = wireOrders.get(members);
|
|
361
|
+
if (order === undefined) {
|
|
362
|
+
order = sortByStr(members);
|
|
363
|
+
wireOrders.set(members, order);
|
|
364
|
+
}
|
|
365
|
+
return [...order];
|
|
78
366
|
}
|
|
79
367
|
function strOf(value) {
|
|
80
368
|
if (typeof value === "number")
|
|
@@ -93,21 +381,26 @@ function strOf(value) {
|
|
|
93
381
|
// =========================================================================
|
|
94
382
|
/** Per-call cap on rows read or returned. Context field: "rows". */
|
|
95
383
|
class RowLimit {
|
|
96
|
-
maxRows;
|
|
97
384
|
key = "max_rows";
|
|
98
385
|
ctxField = "rows";
|
|
386
|
+
maxRows;
|
|
99
387
|
constructor(maxRows) {
|
|
100
|
-
|
|
101
|
-
|
|
388
|
+
checkMax("max_rows", maxRows);
|
|
389
|
+
this.maxRows = plain(maxRows);
|
|
390
|
+
validateSafeNumber("max_rows", this.maxRows);
|
|
102
391
|
}
|
|
103
392
|
permits(ctx) {
|
|
104
|
-
const n = ctx
|
|
105
|
-
if (n === undefined || n === null
|
|
393
|
+
const n = ownValue(ctx, "rows");
|
|
394
|
+
if (n === undefined || n === null)
|
|
395
|
+
return reasons_js_1.Decision.allow();
|
|
396
|
+
const kind = wrongKind(n, NUMBER);
|
|
397
|
+
if (kind === null && plain(n) <= this.maxRows)
|
|
106
398
|
return reasons_js_1.Decision.allow();
|
|
107
399
|
return reasons_js_1.Decision.deny(new reasons_js_1.Reason(reasons_js_1.ReasonCode.CEILING_EXCEEDED, {
|
|
108
400
|
constraint: this.key,
|
|
109
401
|
limit: this.maxRows,
|
|
110
402
|
requested: n,
|
|
403
|
+
message: refusal(kind, "a maximum"),
|
|
111
404
|
}));
|
|
112
405
|
}
|
|
113
406
|
describe() {
|
|
@@ -117,7 +410,7 @@ class RowLimit {
|
|
|
117
410
|
return new RowLimit(Math.min(this.maxRows, other.maxRows));
|
|
118
411
|
}
|
|
119
412
|
subsumes(other) {
|
|
120
|
-
return this.maxRows >= other.maxRows;
|
|
413
|
+
return sameType(this, other) && this.maxRows >= other.maxRows;
|
|
121
414
|
}
|
|
122
415
|
toWire() {
|
|
123
416
|
return { key: this.key, max: this.maxRows };
|
|
@@ -129,21 +422,26 @@ class RowLimit {
|
|
|
129
422
|
exports.RowLimit = RowLimit;
|
|
130
423
|
/** Per-call cap on spend, currency-agnostic. Context field: "spend". */
|
|
131
424
|
class SpendCap {
|
|
132
|
-
maxSpend;
|
|
133
425
|
key = "max_spend";
|
|
134
426
|
ctxField = "spend";
|
|
427
|
+
maxSpend;
|
|
135
428
|
constructor(maxSpend) {
|
|
136
|
-
|
|
137
|
-
|
|
429
|
+
checkMax("max_spend", maxSpend);
|
|
430
|
+
this.maxSpend = plain(maxSpend);
|
|
431
|
+
validateSafeNumber("max_spend", this.maxSpend);
|
|
138
432
|
}
|
|
139
433
|
permits(ctx) {
|
|
140
|
-
const n = ctx
|
|
141
|
-
if (n === undefined || n === null
|
|
434
|
+
const n = ownValue(ctx, "spend");
|
|
435
|
+
if (n === undefined || n === null)
|
|
436
|
+
return reasons_js_1.Decision.allow();
|
|
437
|
+
const kind = wrongKind(n, NUMBER);
|
|
438
|
+
if (kind === null && plain(n) <= this.maxSpend)
|
|
142
439
|
return reasons_js_1.Decision.allow();
|
|
143
440
|
return reasons_js_1.Decision.deny(new reasons_js_1.Reason(reasons_js_1.ReasonCode.CEILING_EXCEEDED, {
|
|
144
441
|
constraint: this.key,
|
|
145
442
|
limit: this.maxSpend,
|
|
146
443
|
requested: n,
|
|
444
|
+
message: refusal(kind, "a maximum"),
|
|
147
445
|
}));
|
|
148
446
|
}
|
|
149
447
|
describe() {
|
|
@@ -153,7 +451,7 @@ class SpendCap {
|
|
|
153
451
|
return new SpendCap(Math.min(this.maxSpend, other.maxSpend));
|
|
154
452
|
}
|
|
155
453
|
subsumes(other) {
|
|
156
|
-
return this.maxSpend >= other.maxSpend;
|
|
454
|
+
return sameType(this, other) && this.maxSpend >= other.maxSpend;
|
|
157
455
|
}
|
|
158
456
|
toWire() {
|
|
159
457
|
return { key: this.key, max: this.maxSpend };
|
|
@@ -176,15 +474,22 @@ exports.SpendCap = SpendCap;
|
|
|
176
474
|
* does not; an explicit `calls` in the context still wins.
|
|
177
475
|
*/
|
|
178
476
|
class CallLimit {
|
|
179
|
-
maxCalls;
|
|
180
477
|
appliesTo;
|
|
181
478
|
key;
|
|
182
479
|
ctxField;
|
|
480
|
+
maxCalls;
|
|
183
481
|
constructor(maxCalls, appliesTo = null) {
|
|
184
|
-
this.maxCalls = maxCalls;
|
|
185
482
|
this.appliesTo = appliesTo;
|
|
186
|
-
|
|
483
|
+
checkString("max_calls", "applies_to", appliesTo, true);
|
|
484
|
+
// A pattern no scope matches ("*", "crm", "CRM.READ") applied to no call, so the limit bounded
|
|
485
|
+
// nothing. It follows the scope grammar: an exact scope, or a terminal `.*` wildcard.
|
|
486
|
+
if (appliesTo !== null && appliesTo !== undefined && !exports.SCOPE_RE.test(appliesTo)) {
|
|
487
|
+
throw malformed("max_calls", "applies_to", appliesTo, "a scope", (0, display_js_1.pyRepr)(appliesTo));
|
|
488
|
+
}
|
|
187
489
|
this.key = appliesTo ? `max_calls[${appliesTo}]` : "max_calls";
|
|
490
|
+
checkMax(this.key, maxCalls);
|
|
491
|
+
this.maxCalls = plain(maxCalls);
|
|
492
|
+
validateSafeNumber("max_calls", this.maxCalls);
|
|
188
493
|
this.ctxField = appliesTo ? `calls[${appliesTo}]` : "calls";
|
|
189
494
|
}
|
|
190
495
|
/** What this ceiling counts: the pattern it applies to, or every call. */
|
|
@@ -198,15 +503,19 @@ class CallLimit {
|
|
|
198
503
|
return held === scope || (held.endsWith(".*") && String(scope).startsWith(held.slice(0, -1)));
|
|
199
504
|
}
|
|
200
505
|
permits(ctx) {
|
|
201
|
-
if (!this.appliesToScope(ctx
|
|
506
|
+
if (!this.appliesToScope(ownValue(ctx, "_scope")))
|
|
507
|
+
return reasons_js_1.Decision.allow();
|
|
508
|
+
const n = ownValue(ctx, this.ctxField);
|
|
509
|
+
if (n === undefined || n === null)
|
|
202
510
|
return reasons_js_1.Decision.allow();
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
511
|
+
const kind = wrongKind(n, NUMBER);
|
|
512
|
+
if (kind === null && plain(n) <= this.maxCalls)
|
|
205
513
|
return reasons_js_1.Decision.allow();
|
|
206
514
|
return reasons_js_1.Decision.deny(new reasons_js_1.Reason(reasons_js_1.ReasonCode.CEILING_EXCEEDED, {
|
|
207
515
|
constraint: this.key,
|
|
208
516
|
limit: this.maxCalls,
|
|
209
517
|
requested: n,
|
|
518
|
+
message: refusal(kind, "a maximum"),
|
|
210
519
|
}));
|
|
211
520
|
}
|
|
212
521
|
describe() {
|
|
@@ -216,7 +525,7 @@ class CallLimit {
|
|
|
216
525
|
return new CallLimit(Math.min(this.maxCalls, other.maxCalls), this.appliesTo);
|
|
217
526
|
}
|
|
218
527
|
subsumes(other) {
|
|
219
|
-
return this.maxCalls >= other.maxCalls;
|
|
528
|
+
return sameType(this, other) && this.maxCalls >= other.maxCalls;
|
|
220
529
|
}
|
|
221
530
|
toWire() {
|
|
222
531
|
if (!this.appliesTo)
|
|
@@ -235,16 +544,23 @@ class EgressRank {
|
|
|
235
544
|
ctxField = "egress";
|
|
236
545
|
constructor(level) {
|
|
237
546
|
this.level = level;
|
|
547
|
+
// A rank outside the vocabulary ranked above "any", so the ceiling admitted every request.
|
|
548
|
+
if (typeof level !== "string" || !EGRESS_ORDER.includes(level)) {
|
|
549
|
+
throw malformed("egress", "rank", level, "'none', 'internal' or 'any'", typeof level === "string" ? (0, display_js_1.pyRepr)(level) : undefined);
|
|
550
|
+
}
|
|
238
551
|
}
|
|
239
552
|
permits(ctx) {
|
|
240
|
-
const val = ctx
|
|
241
|
-
if (val === undefined || val === null
|
|
553
|
+
const val = ownValue(ctx, "egress");
|
|
554
|
+
if (val === undefined || val === null)
|
|
555
|
+
return reasons_js_1.Decision.allow();
|
|
556
|
+
const kind = wrongKind(val, STRING);
|
|
557
|
+
if (kind === null && egressRankOf(val) <= egressRankOf(this.level))
|
|
242
558
|
return reasons_js_1.Decision.allow();
|
|
243
|
-
}
|
|
244
559
|
return reasons_js_1.Decision.deny(new reasons_js_1.Reason(reasons_js_1.ReasonCode.CEILING_EXCEEDED, {
|
|
245
560
|
constraint: this.key,
|
|
246
561
|
limit: this.level,
|
|
247
562
|
requested: val,
|
|
563
|
+
message: refusal(kind, "an egress rank"),
|
|
248
564
|
}));
|
|
249
565
|
}
|
|
250
566
|
describe() {
|
|
@@ -255,7 +571,7 @@ class EgressRank {
|
|
|
255
571
|
return new EgressRank(egressRankOf(this.level) <= egressRankOf(o.level) ? this.level : o.level);
|
|
256
572
|
}
|
|
257
573
|
subsumes(other) {
|
|
258
|
-
return egressRankOf(this.level) >= egressRankOf(other.level);
|
|
574
|
+
return sameType(this, other) && egressRankOf(this.level) >= egressRankOf(other.level);
|
|
259
575
|
}
|
|
260
576
|
toWire() {
|
|
261
577
|
return { key: this.key, rank: this.level };
|
|
@@ -270,7 +586,11 @@ exports.EgressRank = EgressRank;
|
|
|
270
586
|
// `key` here IS a caller choice, so it cannot double as the wire discriminator;
|
|
271
587
|
// these carry an explicit "type" on the wire.
|
|
272
588
|
// =========================================================================
|
|
273
|
-
/**
|
|
589
|
+
/**
|
|
590
|
+
* Membership allow-list: the context value MUST be one of `oneOf`. A member is its JSON type plus
|
|
591
|
+
* its value, as a Set holds it: `[1]` admits 1 and refuses `true` and `"1"`. A context value that
|
|
592
|
+
* is not a JSON scalar is refused (`wrongKind`).
|
|
593
|
+
*/
|
|
274
594
|
class Allow {
|
|
275
595
|
key;
|
|
276
596
|
field;
|
|
@@ -278,43 +598,55 @@ class Allow {
|
|
|
278
598
|
constructor(key, oneOf, field = null) {
|
|
279
599
|
this.key = key;
|
|
280
600
|
this.field = field;
|
|
281
|
-
|
|
601
|
+
checkKey(key);
|
|
602
|
+
this.oneOf = new Members(memberList(key, "one_of", oneOf));
|
|
603
|
+
checkString(key, "field", field, true);
|
|
282
604
|
}
|
|
283
605
|
ctxKey() {
|
|
284
606
|
return this.field ?? this.key;
|
|
285
607
|
}
|
|
286
608
|
permits(ctx) {
|
|
287
|
-
const val = ctx
|
|
288
|
-
if (val === undefined || val === null
|
|
609
|
+
const val = ownValue(ctx, this.ctxKey());
|
|
610
|
+
if (val === undefined || val === null)
|
|
611
|
+
return reasons_js_1.Decision.allow();
|
|
612
|
+
const kind = wrongKind(val, SCALAR);
|
|
613
|
+
if (kind === null && this.oneOf.has(plain(val)))
|
|
289
614
|
return reasons_js_1.Decision.allow();
|
|
290
615
|
return reasons_js_1.Decision.deny(new reasons_js_1.Reason(reasons_js_1.ReasonCode.CEILING_EXCEEDED, {
|
|
291
616
|
constraint: this.key,
|
|
292
|
-
limit:
|
|
617
|
+
limit: inWireOrder(this.oneOf),
|
|
293
618
|
requested: val,
|
|
619
|
+
message: refusal(kind, "one_of members"),
|
|
294
620
|
}));
|
|
295
621
|
}
|
|
296
622
|
describe() {
|
|
297
|
-
return `${this.key} in [${
|
|
623
|
+
return `${this.key} in [${inWireOrder(this.oneOf).map(strOf).join(", ")}]`;
|
|
298
624
|
}
|
|
299
625
|
narrow(other) {
|
|
300
626
|
const o = other;
|
|
301
627
|
return new Allow(this.key, Array.from(this.oneOf).filter((v) => o.oneOf.has(v)), this.field);
|
|
302
628
|
}
|
|
303
629
|
subsumes(other) {
|
|
304
|
-
return Array.from(other.oneOf).every((v) => this.oneOf.has(v));
|
|
630
|
+
return sameType(this, other) && Array.from(other.oneOf).every((v) => this.oneOf.has(v));
|
|
305
631
|
}
|
|
306
632
|
toWire() {
|
|
307
|
-
const d = { key: this.key, type: "allow", one_of:
|
|
633
|
+
const d = { key: this.key, type: "allow", one_of: inWireOrder(this.oneOf) };
|
|
308
634
|
if (this.field !== null && this.field !== this.key)
|
|
309
635
|
d["field"] = this.field;
|
|
310
636
|
return d;
|
|
311
637
|
}
|
|
312
638
|
static fromWire(d) {
|
|
313
|
-
|
|
639
|
+
// Anything but an array, an absent one_of included, is refused by the constructor.
|
|
640
|
+
return new Allow(d["key"], d["one_of"], d["field"] ?? null);
|
|
314
641
|
}
|
|
315
642
|
}
|
|
316
643
|
exports.Allow = Allow;
|
|
317
|
-
/**
|
|
644
|
+
/**
|
|
645
|
+
* Membership deny-list: the context value MUST NOT be one of `notOneOf`. A member is its JSON type
|
|
646
|
+
* plus its value, as a Set holds it: `[1]` refuses 1 and not `true` or `"1"`. A context value that
|
|
647
|
+
* is not a JSON scalar is refused as well (`wrongKind`): a deny-list never waves through a value it
|
|
648
|
+
* cannot compare, since waving `["rm"]` through because it is not the string "rm" would fail open.
|
|
649
|
+
*/
|
|
318
650
|
class Deny {
|
|
319
651
|
key;
|
|
320
652
|
field;
|
|
@@ -322,23 +654,29 @@ class Deny {
|
|
|
322
654
|
constructor(key, notOneOf, field = null) {
|
|
323
655
|
this.key = key;
|
|
324
656
|
this.field = field;
|
|
325
|
-
|
|
657
|
+
checkKey(key);
|
|
658
|
+
this.notOneOf = new Members(memberList(key, "not_one_of", notOneOf));
|
|
659
|
+
checkString(key, "field", field, true);
|
|
326
660
|
}
|
|
327
661
|
ctxKey() {
|
|
328
662
|
return this.field ?? this.key;
|
|
329
663
|
}
|
|
330
664
|
permits(ctx) {
|
|
331
|
-
const val = ctx
|
|
332
|
-
if (val === undefined || val === null
|
|
665
|
+
const val = ownValue(ctx, this.ctxKey());
|
|
666
|
+
if (val === undefined || val === null)
|
|
667
|
+
return reasons_js_1.Decision.allow();
|
|
668
|
+
const kind = wrongKind(val, SCALAR);
|
|
669
|
+
if (kind === null && !this.notOneOf.has(plain(val)))
|
|
333
670
|
return reasons_js_1.Decision.allow();
|
|
334
671
|
return reasons_js_1.Decision.deny(new reasons_js_1.Reason(reasons_js_1.ReasonCode.CEILING_EXCEEDED, {
|
|
335
672
|
constraint: this.key,
|
|
336
|
-
limit:
|
|
673
|
+
limit: inWireOrder(this.notOneOf),
|
|
337
674
|
requested: val,
|
|
675
|
+
message: refusal(kind, "not_one_of members"),
|
|
338
676
|
}));
|
|
339
677
|
}
|
|
340
678
|
describe() {
|
|
341
|
-
return `${this.key} not in [${
|
|
679
|
+
return `${this.key} not in [${inWireOrder(this.notOneOf).map(strOf).join(", ")}]`;
|
|
342
680
|
}
|
|
343
681
|
narrow(other) {
|
|
344
682
|
const o = other;
|
|
@@ -347,20 +685,21 @@ class Deny {
|
|
|
347
685
|
subsumes(other) {
|
|
348
686
|
// `this` admits a superset of `other`'s admitted set iff it forbids a
|
|
349
687
|
// subset of what `other` forbids.
|
|
350
|
-
return Array.from(this.notOneOf).every((v) => other.notOneOf.has(v));
|
|
688
|
+
return sameType(this, other) && Array.from(this.notOneOf).every((v) => other.notOneOf.has(v));
|
|
351
689
|
}
|
|
352
690
|
toWire() {
|
|
353
691
|
const d = {
|
|
354
692
|
key: this.key,
|
|
355
693
|
type: "deny",
|
|
356
|
-
not_one_of:
|
|
694
|
+
not_one_of: inWireOrder(this.notOneOf),
|
|
357
695
|
};
|
|
358
696
|
if (this.field !== null && this.field !== this.key)
|
|
359
697
|
d["field"] = this.field;
|
|
360
698
|
return d;
|
|
361
699
|
}
|
|
362
700
|
static fromWire(d) {
|
|
363
|
-
|
|
701
|
+
// Anything but an array, an absent not_one_of included, is refused by the constructor.
|
|
702
|
+
return new Deny(d["key"], d["not_one_of"], d["field"] ?? null);
|
|
364
703
|
}
|
|
365
704
|
}
|
|
366
705
|
exports.Deny = Deny;
|
|
@@ -373,19 +712,25 @@ class Prefix {
|
|
|
373
712
|
this.key = key;
|
|
374
713
|
this.prefix = prefix;
|
|
375
714
|
this.field = field;
|
|
715
|
+
checkKey(key);
|
|
716
|
+
checkString(key, "prefix", prefix);
|
|
717
|
+
checkString(key, "field", field, true);
|
|
376
718
|
}
|
|
377
719
|
ctxKey() {
|
|
378
720
|
return this.field ?? this.key;
|
|
379
721
|
}
|
|
380
722
|
permits(ctx) {
|
|
381
|
-
const val = ctx
|
|
382
|
-
if (val === undefined || val === null
|
|
723
|
+
const val = ownValue(ctx, this.ctxKey());
|
|
724
|
+
if (val === undefined || val === null)
|
|
725
|
+
return reasons_js_1.Decision.allow();
|
|
726
|
+
const kind = wrongKind(val, STRING);
|
|
727
|
+
if (kind === null && val.startsWith(this.prefix))
|
|
383
728
|
return reasons_js_1.Decision.allow();
|
|
384
|
-
}
|
|
385
729
|
return reasons_js_1.Decision.deny(new reasons_js_1.Reason(reasons_js_1.ReasonCode.CEILING_EXCEEDED, {
|
|
386
730
|
constraint: this.key,
|
|
387
731
|
limit: this.prefix,
|
|
388
732
|
requested: val,
|
|
733
|
+
message: refusal(kind, "a prefix"),
|
|
389
734
|
}));
|
|
390
735
|
}
|
|
391
736
|
describe() {
|
|
@@ -407,7 +752,7 @@ class Prefix {
|
|
|
407
752
|
return new Prefix(this.key, `${this.prefix}\u0000${o.prefix}`, this.field);
|
|
408
753
|
}
|
|
409
754
|
subsumes(other) {
|
|
410
|
-
return other.prefix.startsWith(this.prefix);
|
|
755
|
+
return sameType(this, other) && other.prefix.startsWith(this.prefix);
|
|
411
756
|
}
|
|
412
757
|
toWire() {
|
|
413
758
|
const d = { key: this.key, type: "prefix", prefix: this.prefix };
|
|
@@ -433,10 +778,15 @@ function registerCeiling(key, cls) {
|
|
|
433
778
|
* Fail-closed placeholder for a wire constraint this build does not recognise.
|
|
434
779
|
*
|
|
435
780
|
* `permits` always denies. `narrow` stays an unknown ceiling, so it can never
|
|
436
|
-
* resolve to something more permissive than "deny everything"
|
|
781
|
+
* resolve to something more permissive than "deny everything"; `Authority.meet`
|
|
782
|
+
* keeps a parent's against a request of any other type, so the child inherits
|
|
783
|
+
* it. `subsumes` is
|
|
437
784
|
* true only against an identical unknown ceiling — just enough reflexivity for
|
|
438
|
-
* `isNarrowerThan(self)`.
|
|
439
|
-
*
|
|
785
|
+
* `isNarrowerThan(self)`. Identical means the same RFC 8785 bytes, which is
|
|
786
|
+
* equality as JSON: `true` is not 1, `1.0` is 1, and key order is no difference
|
|
787
|
+
* at any depth; a value RFC 8785 cannot write is identical to nothing. `toWire`
|
|
788
|
+
* preserves the original bytes losslessly, so a chain that merely forwards
|
|
789
|
+
* constraints can still do so.
|
|
440
790
|
*/
|
|
441
791
|
class UnknownCeiling {
|
|
442
792
|
raw;
|
|
@@ -446,17 +796,28 @@ class UnknownCeiling {
|
|
|
446
796
|
this.key = key === null || key === undefined ? "" : String(key);
|
|
447
797
|
}
|
|
448
798
|
permits(_ctx) {
|
|
799
|
+
// In the Python implementation's words, and with the key as the wire carried it (null, a
|
|
800
|
+
// number), so a deny entry is the same bytes from both implementations.
|
|
801
|
+
const key = wireKeyOf(this);
|
|
449
802
|
return reasons_js_1.Decision.deny(new reasons_js_1.Reason(reasons_js_1.ReasonCode.UNKNOWN_CONSTRAINT, {
|
|
450
|
-
constraint:
|
|
451
|
-
message: `unrecognised constraint type for key=${
|
|
803
|
+
constraint: key,
|
|
804
|
+
message: `unrecognised constraint type for key=${(0, display_js_1.pyRepr)(key)}; fail-closed`,
|
|
452
805
|
}));
|
|
453
806
|
}
|
|
454
807
|
narrow(_other) {
|
|
455
808
|
return this;
|
|
456
809
|
}
|
|
457
810
|
subsumes(other) {
|
|
458
|
-
|
|
459
|
-
|
|
811
|
+
// Compared as RFC 8785 bytes, as the Python implementation compares them. JSON text with only
|
|
812
|
+
// the top-level keys sorted told `{"v":{"a":1,"b":2}}` from `{"v":{"b":2,"a":1}}` (attenu-ops#110).
|
|
813
|
+
if (!(other instanceof UnknownCeiling))
|
|
814
|
+
return false;
|
|
815
|
+
try {
|
|
816
|
+
return (0, canonical_js_1.canonicalJson)(other.raw) === (0, canonical_js_1.canonicalJson)(this.raw);
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
460
821
|
}
|
|
461
822
|
toWire() {
|
|
462
823
|
return { ...this.raw };
|
|
@@ -466,19 +827,28 @@ class UnknownCeiling {
|
|
|
466
827
|
}
|
|
467
828
|
}
|
|
468
829
|
exports.UnknownCeiling = UnknownCeiling;
|
|
469
|
-
function canonicalPairs(d) {
|
|
470
|
-
return Object.keys(d)
|
|
471
|
-
.sort(canonical_js_1.compareCodePoints)
|
|
472
|
-
.map((k) => [k, d[k]]);
|
|
473
|
-
}
|
|
474
830
|
/**
|
|
475
831
|
* Reconstruct a Ceiling from its wire form. Routes on "type" when present (it
|
|
476
832
|
* disambiguates the generic ceilings), else on "key". An unrecognised
|
|
477
833
|
* discriminator fails closed via `UnknownCeiling`.
|
|
834
|
+
*
|
|
835
|
+
* A constraint is a JSON object with a string `key`, and a `type` that, when present, is a string;
|
|
836
|
+
* anything else is malformed (attenu-ops#110), in the Python implementation's words too: `a
|
|
837
|
+
* constraint is a string, not an object`, `type of constraint 'max_rows' is null, not a string`.
|
|
838
|
+
* A constraint that is not an object loaded as an unknown constraint here, null aside, and a null
|
|
839
|
+
* `type` was read as absent, so the constraint was routed by its key: `{"key": "allow", "type":
|
|
840
|
+
* null, ...}` loaded as an allow-list, where the Python implementation loaded an unknown constraint.
|
|
478
841
|
*/
|
|
479
842
|
function ceilingFromWire(wire) {
|
|
480
843
|
const d = (0, canonical_js_1.toPlain)(wire);
|
|
481
|
-
|
|
844
|
+
if (d === null || typeof d !== "object" || Array.isArray(d)) {
|
|
845
|
+
throw new TypeError(`a constraint is ${jsonKind(d)}, not an object`);
|
|
846
|
+
}
|
|
847
|
+
checkKey(d["key"]);
|
|
848
|
+
const type = d["type"];
|
|
849
|
+
if (type !== undefined && typeof type !== "string")
|
|
850
|
+
throw malformed(d["key"], "type", type, "a string");
|
|
851
|
+
const discriminator = type ?? d["key"];
|
|
482
852
|
const cls = typeof discriminator === "string" ? REGISTRY.get(discriminator) : undefined;
|
|
483
853
|
if (cls === undefined)
|
|
484
854
|
return UnknownCeiling.fromWire(d);
|