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