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/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,78 @@ Versions follow semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.13.1] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **A constraint bound of the wrong type is malformed, so a token that carried one and verified before is now refused.** A `max` that is not a number (a boolean, a string, null, an array, an object, or no `max` at all), a `prefix` that is not a string, an egress `rank` other than `none`, `internal` or `any`, and a `field` or `applies_to` that is not a string are refused when the ceiling is built, with a TypeError `<member> of constraint <key> is <kind>, not <expected>`: `max of constraint 'max_rows' is a boolean, not a number`, `prefix of constraint 'path' is a number, not a string`, `rank of constraint 'egress' is 'everywhere', not 'none', 'internal' or 'any'`, `field of constraint 'region' is a number, not a string`. null leaves `field` and `applies_to` unset in-process and `field` unset on the wire; a wire `applies_to: null` is refused, as before, as a member this build does not read. So a token carrying one is refused as `malformed`, a bundle reports `unreadable_authority` or `unreadable_granted` with that text, and `new RowLimit("5")` or `new EgressRank("external")` throws. Every release from 0.1.0 through 0.13.0 accepted them: `max: true` read as 1, `max: "5"` was coerced, an unknown rank ranked above `any` and so admitted every request, `prefix: 5` matched as the text `5`, and `field: 5` read `ctx["5"]`. The draft defines `max` as a number and `prefix` as a string. The Python implementation raises ValueError with the same text
|
|
13
|
+
- **A constraint's `key` must be a string, so a token whose constraint had none, or a key of another type, is now refused.** A constraint of any type, an unknown type included, is refused when it is read, with a TypeError `key of a constraint is <absent|null|a number|a boolean|an array|an object>, not a string`, so a token is refused as `malformed` and a bundle reports `unreadable_authority` or `unreadable_granted`; `Allow`, `Deny` and `Prefix` throw it in-process too. Through 0.13.0, an allow-list, a deny-list or a prefix with no key loaded and read the context field `"undefined"`, so it bounded nothing; a key of 5, null or `true` was read as `String(key)`, so 5 and `"5"` were one dimension and one of two such constraints was dropped, where the Python implementation read a field no JSON context carries. Measured on the 0.13.0 package; the code is the same at every tag from 0.1.1. The draft makes `key` REQUIRED. The Python implementation raises ValueError with the same text
|
|
14
|
+
- **A constraint must be a JSON object, and its `type`, when present, a string, so a token carrying another value is now refused.** A constraint that is not an object is refused with a TypeError `a constraint is <a string|a number|a boolean|null|an array>, not an object`, and a `type` that is not a string with `type of constraint 'max_rows' is <null|a number|a boolean|an array|an object>, not a string`, the Python implementation's words; a token is refused as `malformed`, and a bundle reports `unreadable_authority` or `unreadable_granted`. Through 0.13.0, a constraint that is not an object loaded as an unknown constraint, which denies every action, except null, which threw a TypeError in JavaScript's words; a `type` that is a number, a boolean, a list or an object loaded as an unknown constraint; and a null `type` was read as absent, so the constraint was routed by its key: `{"key": "allow", "type": null, "one_of": ["us"]}` loaded as an allow-list, where the Python implementation loaded an unknown constraint, and `{"key": "max_rows", "type": null, "max": 5}` was refused as a member this build does not read. Measured on the 0.13.0 package; the code is the same at every tag from 0.1.1
|
|
15
|
+
- **An absent `one_of` or `not_one_of` is malformed.** Through 0.13.0, `{"key": "tool", "type": "deny"}` read as an empty deny-list and bounded nothing (measured on the 0.13.0 package; the code is the same at every tag from 0.1.1). It is now refused like a list of the wrong type, `not_one_of of constraint 'tool' is absent, not an array`. A constraint this build writes always carries its list
|
|
16
|
+
- **A scoped call limit's `applies_to` must be a scope.** It follows the scope grammar, an exact scope or a terminal `.*` wildcard, and any other string is refused: `applies_to of constraint 'max_calls' is '*', not a scope`. Through 0.13.0, `new CallLimit(1, "*")`, `"crm"` or `"CRM.READ"` applied to no call, so the limit bounded nothing (measured on the 0.13.0 package; the code is the same at every tag from 0.1.1)
|
|
17
|
+
- **A verifier finding prints every string member of an allow-list or a deny-list in its escaped JSON form, quoted.** `ceiling t in [1] looser than parent t in ["1"]`, where both lists printed `[1]`. That is the display rule 0.13.0 applied to a member that needs escapes, applied to every string member, so the text is ASCII and the same as the Python implementation's on every Python version: `["S\u00e3o\u0020Paulo", "us"]` where 0.13.0 printed `["S\u00e3o\u0020Paulo", us]`. A number, a boolean and null print bare, as before. `describe()` is unchanged
|
|
18
|
+
- **Two constraints under one key in one authority are malformed.** `Authority` throws a TypeError `two constraints share the key 'region'; an authority holds one per key`, in-process and on the wire, so a token carrying two is refused as `malformed` and a bundle reports `unreadable_authority` or `unreadable_granted`. Through 0.13.0 the last one won, silently: `[allow region in [us], deny region not in [rm]]` kept only the deny-list and permitted `region: eu` (measured on the 0.13.0 package). The Python implementation raises ValueError with the same text
|
|
19
|
+
- **`Allow.oneOf` and `Deny.notOneOf` are read-only at runtime.** Both were typed `ReadonlySet` and held a `Set`; each is now a read-only set whose `add`, `delete` and `clear` throw a TypeError, and which is not an `instanceof Set`, as the Python implementation's member set has no mutators. `has`, `size`, iteration, `forEach`, `keys`, `values` and `entries` work as before. The wire order of a member set is computed once, so a member added through a cast after the first `toWire()` would have been checked by `permits()` and missing from the wire
|
|
20
|
+
- **A token refused for a malformed constraint carries the error's text alone.** `invalid authorization_details: TypeError: max of constraint 'max_rows' is a boolean, not a number` is now `invalid authorization_details: max of constraint 'max_rows' is a boolean, not a number`, the Python implementation's message, and the same for an `AuthorityError`
|
|
21
|
+
- **`Ceiling.permits`, called directly, reads only the context's own fields.** `new RowLimit(5).permits(Object.create({ rows: 500 }))`, or a context whose `rows` is a getter on its prototype, was refused by 0.13.0 and is permitted now: an inherited field is not the context's, so it asserts nothing. `Authority.permits`, `Guard.check`, `VerifiedChain.permits` and `verifyBundle` already read only own fields in 0.13.0, through `{...context}`, so they are unchanged
|
|
22
|
+
- **A bundle that verified under an earlier release can now fail re-verification.** Evidence recorded by 0.1.0 through 0.13.0 fails where the earlier check was wrong: an allow whose context held a value a ceiling coerced, such as `rows: "50"` under a row cap, `tool: ["rm"]` under a deny-list, or `path: ["/tmp/x"]` or `flag: true` under a prefix, fails containment, and so does an allow whose context carried a `_scope` that moved the call off its meter. A bundle the Python implementation recorded through 0.19.0 fails the same way, with `rows: true` under a row cap or `tier: true` under `one_of: [1]`. A Python delegation whose meet widened across `true` and 1, in either direction, failed monotonicity here already, and now fails it there too
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **A deny-list let through a request value it could not compare.** `not_one_of: ["rm"]` permitted `tool: ["rm"]`, and any other array or object, in every release from 0.1.0 through 0.13.0: measured on the 0.1.0, 0.1.1, 0.5.0, 0.6.0, 0.7.0, 0.9.0, 0.12.0 and 0.13.0 packages, and the `Allow` and `Deny` checks are the same at every tag in between. A Set compares an array or an object by identity, so no request value ever equals a member: an allow-list refused one as a non-member, and a deny-list waved it through. Both lists now refuse a request value that is not a string, a number, a boolean or null, as `ceiling_exceeded` with the message `an array cannot be compared with not_one_of members; refused` (or `an object`, or `a value that is not JSON`; `one_of` for an allow-list). The rule is in `permits`, so it holds in `Guard.check`, in `VerifiedChain.permits` after `load()`, and in `verifyBundle`'s containment check. The Python implementation's change for attenu-ops#110 has the same rule, reason and message
|
|
26
|
+
- **A number read with this library's `parseJson` passed a deny-list.** `parseJson` returns every number as a `RawNumber`, which no Set member equals, so `not_one_of: [1]` permitted a context `{"tier": 1}` read with `parseJson`, in `Ceiling.permits` and in `Guard.check`, and `one_of: [1]` refused it. Measured on the 0.13.0 package; the check is the same at every tag from 0.1.1. A `RawNumber` now counts as the number it holds in every ceiling's type check, in a context, a bound or a member
|
|
27
|
+
- **`Allow` and `Deny` read a context field named like an `Object.prototype` member from the prototype.** On a plain object `ctx["constructor"]` is Object's constructor, so `Allow("constructor", …)` refused a context that did not hold that field, where the Python implementation reads the field as absent and permits. Both now read only the context's own fields, so an absent field asserts nothing, whatever its name
|
|
28
|
+
- **Different ceiling types under one key threw a TypeError, or delegated by a guess.** A child whose ceiling under a key is of another class than its parent's, such as a deny-list where the parent holds an allow-list, threw a TypeError (`undefined is not iterable`) out of `isNarrowerThan`, so out of `load()` and out of `verifyBundle`; a delegation across the two threw out of `meet` with nothing on the ledger; one that asked for a prefix under a parent's egress rank was granted the parent's rank, the request dropped; and one that asked for a constraint this build does not define under a parent's row cap failed the chain's own check, `attenuation invariant violated`. Measured on the 0.13.0 package; the `subsumes` and `narrow` code is the same at every tag from 0.1.1. Two different known types under one key are not comparable now: such a child is not narrower, so `load()` reports `not_narrower` and a bundle a monotonicity finding, and `Authority.meet` throws an AuthorityError `constraint 'region' has a different ceiling type on each side; neither narrows the other` with the reason `not_narrower`, so `Guard.delegate` records the refused delegation as `spawn_denied`, with that reason and the detail `{"constraint": "region"}`, and throws it. A constraint this build does not define is the one exception, in one direction: from a parent holding one, the child inherits it, as 0.13.0 did, since it denies every action. A request carrying one under a parent's other ceiling is refused like any other pair, as not narrower with a `spawn_denied` entry. 0.13.0 did one of three things there, by the parent's ceiling: under an allow-list, a deny-list or a prefix it threw a TypeError with nothing on the ledger; under an egress rank it delegated, granting the parent's rank and dropping the requested constraint; and under a numeric cap it recorded `spawn_denied` with the reason `integrity`; and a child holding one under a parent's other ceiling is not narrower, in `isNarrowerThan`, `load()` and a bundle's monotonicity check, as 0.13.0 found for a numeric cap, where an allow-list threw. The same in the Python implementation, whose `meet` raises the same AuthorityError
|
|
29
|
+
- **A custom ceiling could let a wider child of another class pass as narrower.** `isNarrowerThan` handed the parent's ceiling the child's ceiling under the same key whatever its class, so a custom ceiling whose `subsumes()` accepts any ceiling, as one written for its own class may, let a child holding a wider allow-list pass as narrower: in-process, and for a registered type in `load()` and in a bundle's monotonicity check. Measured on the 0.13.0 package. The relation checks the class first now, so such a child is not narrower
|
|
30
|
+
- **A custom ceiling's denial without a reason let the call through.** `Authority.permits` allowed a call whenever it had collected no reason, so a custom ceiling returning `Decision.deny([])` was allowed by `Guard.check` and by `VerifiedChain.permits`, and passed `verifyBundle`'s containment check (measured on the 0.13.0 package). Such a denial denies now in all three, with the reason `ceiling_exceeded`, the ceiling's key and the message `denied without a reason`, the same in the Python implementation. An archived bundle that recorded allows under such a ceiling, and verified before with the ceiling registered, now fails containment
|
|
31
|
+
- **A constraint member named like an `Object.prototype` member was ignored.** The check that refuses a constraint carrying a member this build does not read asked `k in emitted`, which is true for `constructor`, `toString`, `valueOf`, `__proto__` and the rest on every plain object, so `{"key": "max_rows", "max": 5, "constructor": 1}` loaded as `max_rows<=5` (measured on the 0.13.0 package), where the Python implementation refuses it. It reads the emitted constraint's own members now, so the token is refused as `malformed`, in the Python implementation's words
|
|
32
|
+
- **A null quantity passed a metered call.** Under strict metering, `{rows: null}` counted as a declared quantity, though every ceiling reads null as absent; and a context's `calls: null` replaced the guard's own call count, so it passed `new CallLimit(1)` any number of times. Measured on the 0.13.0 package; the check is the same at every tag from 0.1.1. A null quantity is now undeclared: strict metering refuses the call as `unmetered`, and the guard counts a call whose `calls` is null itself. Strict metering reads only the context's own fields, as the ceilings do. An explicit numeric count still wins: the declared-quantity limit the Python implementation's `docs/RED-TEAM.md` describes
|
|
33
|
+
- **Equal member sets re-emitted different bytes.** Members that print alike, such as `"1"` and 1, `"True"` and `true`, or `"None"` and null, kept the order they arrived in, so `one_of: ["1", 1]` re-emitted `["1",1]` and `one_of: [1, "1"]` re-emitted `[1,"1"]`. Measured on the 0.13.0 package; the sort is the same at every tag from 0.1.1. They are ordered by JSON type now, null, boolean, number, then string, the same in the Python implementation
|
|
34
|
+
- **A `one_of` or `not_one_of` that is not an array was read anyway.** null became the empty list and a string its characters, so `not_one_of: null` was an empty deny-list and `one_of: "us"` the allow-list `["s", "u"]`, and a token or a bundle carrying one verified; an object, a number or a boolean threw a TypeError in its own words. Measured on the 0.1.0, 0.1.1, 0.5.0, 0.6.0, 0.7.0, 0.9.0, 0.12.0 and 0.13.0 packages. Each is now malformed, as the draft defines both as an array: `Allow` and `Deny` throw a TypeError naming the list and the key, such as `one_of of constraint 'region' is an object, not an array` (or `null`, `a string`, `a number`, `a boolean`), so a token carrying one is refused as `malformed` and a bundle reports `unreadable_authority` or `unreadable_granted` with that text. The Python implementation raises ValueError with the same text
|
|
35
|
+
- **Unknown constraints were compared as JSON text with only the top-level keys sorted.** Two constraints equal as JSON differed when a nested object listed its keys in another order, so a bundle whose child repeated its parent's unknown constraint with a nested object's keys reordered failed monotonicity, in every release from 0.1.0 through 0.13.0. (A token cannot carry such a reorder: its RFC 8785 bytes sort every level.) An unknown constraint now subsumes only one with the same RFC 8785 bytes: `true` is not 1, `1.0` is 1, and key order is no difference at any depth. A value this build cannot write in RFC 8785, such as an integer past 2^53, is identical to nothing. The Python implementation compares the same way, except for an integral float past 2^53 (see Known differences)
|
|
36
|
+
- **A ceiling coerced a request value of the wrong JSON type.** `<=` let `"50"`, `[50]` and `true` pass a row cap, and every other numeric cap, and `String()` let `["/tmp/x"]` pass the prefix `"/tmp/"` as the text `/tmp/x` and `true` pass the prefix `"t"` as `true`, in every release from 0.1.0 through 0.13.0: measured on the 0.1.0, 0.1.1, 0.5.0, 0.6.0, 0.7.0, 0.9.0, 0.12.0 and 0.13.0 packages, and the five checks are the same at every tag in between. Every built-in ceiling now refuses a request value of the wrong type as `ceiling_exceeded` and never coerces it: a numeric cap takes a number, a prefix and an egress rank take a string, and an allow-list or a deny-list takes a string, a number or a boolean. The message names the value's JSON type and what it could not be compared with: `a string cannot be compared with a maximum; refused`, `an array cannot be compared with a prefix; refused`, `a boolean cannot be compared with an egress rank; refused`, and the membership messages above, in the Python implementation's words. null and an absent field still assert nothing, and every ceiling reads only the context's own fields, so a prefix on a field named `constructor` no longer refuses a context that does not hold one. The Python implementation now refuses such a value too, where it raised, so an allow whose context holds `{"rows": "many"}` fails containment in both
|
|
37
|
+
- **A request context could choose which scoped call limit applied.** `Authority.permits` set the reserved `_scope` only when the context did not carry one, so a context's own `_scope` decided which scoped `CallLimit` applied: after `new CallLimit(1, "crm.read")` had refused a second call, a third carrying `_scope: "other.x"` was allowed, and a call could be charged to another scope's limit instead, in every release from 0.1.0 through 0.13.0. `_scope` is now always the scope being checked, and a context's value is ignored, in `Guard.check`, `VerifiedChain.permits` and `verifyBundle`'s containment check. A `calls` count supplied in the context still wins over the guard's own count: the declared-quantity limit the Python implementation's `docs/RED-TEAM.md` describes
|
|
38
|
+
- **An unknown constraint described itself and denied in other words than the Python implementation.** `describe()` printed it as JSON text, `quota={"key":"quota","type":"x-unknown","max":1}`, and its denial read `unrecognised constraint type for key="quota"; fail-closed`, in every release from 0.1.0 through 0.13.0, so the deny entry a Guard writes for the same call was different bytes from the two implementations. Both now print it as the Python implementation does, `quota={'key': 'quota', 'type': 'x-unknown', 'max': 1}` and `unrecognised constraint type for key='quota'; fail-closed`, except where Known differences says otherwise. A key that is not a string, which the reason's `constraint` recorded as text, is now malformed (see Changed)
|
|
39
|
+
- **A number with a fraction printed in JavaScript's form where the Python implementation prints its repr.** A message quoting a constraint this build refuses read `'min': 0.00001` in 0.13.0, where the Python implementation reads `'min': 1e-05`. A number that has lost its literal now prints as Python's repr of the float, there and in an unknown constraint's `describe()`; an integral one still prints as an integer (see Known differences)
|
|
40
|
+
|
|
41
|
+
### Known differences
|
|
42
|
+
- A ceiling bound or an unknown constraint value written as an integral number with a fraction or an exponent, such as `100.0` or `1e5`, prints as an integer here, in `describe()` and in a verifier finding (`max_rows<=100`, `100000`), and as a float in the Python implementation (`100.0`, `100000.0`). This build reads the number before it prints it. A bound written as `-0` prints as `-0.0` here and as `0` there. Verdicts and hashes are the same: RFC 8785 writes both as `100`, and both as `0`
|
|
43
|
+
- A list or an object as a `one_of` or `not_one_of` member is accepted here and compared by object identity, so no request value matches it, and a child that repeats it is not narrower than the parent holding it; a lone root token carrying one verifies. The Python implementation refuses it: as `malformed` in a token, and as `unreadable_authority` or `unreadable_granted` in a bundle
|
|
44
|
+
- An unknown constraint holding an integral float at or past 2^53, such as `v: 1e16`, is identical to nothing here, since this build cannot write that number in RFC 8785, so a child that repeats its parent's constraint is not narrower. The Python implementation writes such a float, and there the two subsume each other
|
|
45
|
+
- An unknown constraint whose object has member names that look like integers, such as `"10"` and `"2"`, prints with those members first, in ascending numeric order, in `describe()` and in a verifier finding, since a JavaScript object orders such names that way. The Python implementation prints its members in wire order
|
|
46
|
+
- Whether a character prints as itself or as an escape in a Python-style `repr` follows each runtime's Unicode tables, and the Python implementation's varies across Python versions too. That reaches a message that quotes a key or a constraint through `repr`, such as `type of constraint '<key>' is null, not a string`, and an unknown constraint's `describe()`. A character only the newer table defines prints as itself in one and escaped in the other: U+1CCFA, which Unicode 17 assigns, prints as itself here on Node 22.22 (Unicode 17) and as `\U0001ccfa` in the Python implementation on Python 3.14 (Unicode 16). The members of an allow-list or a deny-list in a finding are ASCII, so they do not vary
|
|
47
|
+
|
|
48
|
+
## [0.13.0] - 2026-10-05
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
- `attenu-guard verify <file> --entries` prints, after the existing output, `entries:` and one line per ledger entry: `seq`, `event`, `node`, `scope`, the envelope `state` (with `observed` and `witness` when it is witness-signed) and `failed`, the checks whose failure is about that entry. A failure is matched to its entry by index, never by seq, so an entry whose seq is missing, null, a boolean, a string or another entry's still shows its failures, and an entry without a seq prints `seq=null`. A failure about no single entry stays in the bundle-level output only. `state` and `witness` are read by index too, from `witnesses`, so of two entries sharing a seq only the one the witness signed reads witness-signed; `observed` comes from `results`, keyed by seq, and is shown only on the witnessed entry. An entry whose seq is a boolean, a string, null or a fractional number is process-asserted: no envelope covers it. An integral seq prints as the integer it is, so `1.0` prints `seq=1`. On a plain ledger there is no state, and a broken chain shows `failed=integrity` on the entry it breaks at. A value is printed as it is only when it is printable ASCII with no space, `"` or `\`, or an integer; anything else is printed as compact JSON with non-ASCII and spaces escaped, so a value read from the bundle cannot end its line or start a forged one: a line is `key=value` tokens split by single spaces, the key is the text before the first `=`, a value never contains a space, and a value that starts with `"` is a JSON string. Without the flag the output is byte-identical and the exit codes do not change (attenu-io/attenu-guard#23)
|
|
52
|
+
- `not_after` on a trust-set row: an RFC 3339 date-time in UTC written with `Z`, such as `2026-10-05T00:00:00Z`, compared to the microsecond. `verifyBundle` and `verifyEnvelopes` take `now`, a `Date` or a string in the same form, and the current time by default. Unlike the Python implementation, `now` does not take a number: a JavaScript epoch counts milliseconds and a Python one seconds, and a number read in the wrong unit would move the verification time a thousandfold. A row whose `not_after` is at or before it is left out of the trust set, so an envelope naming that kid fails `envelope_unknown_witness`, and the message says the key expired and when. A malformed `not_after`, `null` included, throws, naming the kid, as other bad rows do. Rows without it behave as before, and `--witness-keys` honours it. Raised in review on dev.to by mickyarun and peterbuildssecure (attenu-io/attenu-guard#22)
|
|
53
|
+
- `report.envelopes.witnesses` maps the index in `bundle.entries` of every witness-signed entry to the `witness.kid` of the envelope that verified for it. It is keyed by index, the entry the subject resolved to and the one `failure_entries` names, so of two entries sharing a seq only the copy the witness signed is in it, and an entry two envelopes dispute is not. `states` and `results` stay keyed by seq
|
|
54
|
+
- `test/fixtures/parity/`: CLI parity cases recorded from the Python CLI by `tools/gen_fixtures.py` (a boolean, a string, a missing and a null seq under `--entries`; a seq or `v` written `1.0`, `2.0`, `3.0`, `9.0`, `-0.0` or `1.5` in a ledger, an envelope subject, an envelope and a bundle; a bundle `v` of `true`; an anchor `seq` or `v` past 2^53 under `--hs256-key`, and an entry carrying an integer past 2^53; an allow `scope` and an anchor `sig` that are not strings; versions of five types in one `mixed_entry_versions` list; two entries sharing a seq with the witness's signature on the later copy; an envelope naming seq 1 that binds an entry whose seq is `true`; an expired `not_after` row; a row carrying `notAfter`; a kid named twice; a node name carrying line breaks). The TypeScript CLI must print the recorded stdout and exit code byte for byte; the generator writes them only when the installed Python has that verifier, so CI's drift check starts comparing them when its pin does
|
|
55
|
+
- `attenu-guard verify` ends its summary line with `ungated=N` when the bundle has N allows that passed through un-gated, marked `policy: "unlisted"`, which containment does not measure and `actions_checked` does not count. A bundle without them prints exactly as before, as in the Python implementation
|
|
56
|
+
- `report.failure_entries`, from `verifyBundle()` and `verifyEnvelopes()`, in step with `failures` and `failure_details`: the index in `bundle.entries` of the entry each failure is about, or `null` for a failure about no single entry. It is exact where a seq is not, and it is what `--entries` attributes by. `failure_details` keeps its five members
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- **A widened child verified OK when its spawn named no real parent.** Every published release from 0.1.0 through 0.12.0 verified such a bundle as OK. `verifyBundle` checked monotonicity only for a spawn whose `parent` named a node it had read, so a spawn whose parent was absent, null, not a string, or a node not in the bundle was skipped, and a child granted more than its parent passed. The process being watched writes the ledger, so it could record such a child. Monotonicity is now read in ledger order: a spawn's `parent` must be a string naming a node that the root or an earlier spawn defined, that no earlier `kill` revoked, and that is not the spawn's own node. Otherwise the spawn fails with `monotonicity: <node> names no parent defined earlier in this bundle (parent <value>)`, attributed to that spawn by index. The same rule, finding for finding, as the Python implementation
|
|
60
|
+
- **Four more ways around monotonicity and containment are closed in the same pass.** A node defined twice is `monotonicity: <node> is defined a second time in this bundle (first at seq N)`; before, only the last definition was read. A spawn from a parent that an earlier `kill` revoked is `monotonicity: <node> is spawned from <parent> after <parent> was revoked at seq N`. An allow on a node that an earlier `kill` revoked is `containment: allow of <scope> on <node> after <node> was revoked at seq N`, since a revoked node holds no authority. An allow on a node defined only later in the ledger is now `containment: allow on unknown node <node>`. A spawn naming itself or a later node as its parent, so any cycle, falls under the parent rule above. A kill revokes only the string ids in its `revoked` list, and the first kill of a node stands
|
|
61
|
+
- **A `node` of the wrong type is a finding.** A root's or a spawn's `node` that is absent or not a string is the existing unreadable finding, `root <node>: unreadable authority (node is not a string)` or `spawn <node>: unreadable granted (node is not a string)`; a bundle whose root had no `node` verified OK here. An allow's is an allow on an unknown node. Any other entry's is the new `invalid_node: seq=N event='<event>' carries node <node>, which is not a string`, with a null `node` counted as absent. `delegationGraph` and the execution-binding report count only string node ids, and the first definition of a node stands in the graph as well
|
|
62
|
+
- **A value from a bundle could add a line to `attenu-guard verify` output.** Finding messages printed node names, seqs, call ids, hashes, ledger field names and ceiling values without quotes, so a value carrying a line break ended its line and started a forged one: a node named `run\nOK\nx` printed a line reading `OK` above the real `FAILED`, and a forged seq did the same to a plain ledger's `TAMPERED` line. Every such value now prints by one rule, the one `--entries` uses: as it always has when it is printable ASCII 0x21-0x7E other than `"` and `\`, as whitespace-free escaped JSON otherwise. Values printed the way Python's `repr` prints them were quoted here without being escaped; they now escape exactly as `repr` does. That includes the messages inside `unreadable authority (...)` and `unreadable granted (...)`, which joined a member name bare and printed values through JSON, which leaves U+2028, DEL and C1 characters as they are: a constraint member named `y\nOK` printed a line reading `OK)`. A property test mutates committed bundles at every depth with line breaks, separators and terminal escapes and checks that every failure stays one printable line. Ceiling values inside a finding are printed the same way, and `describe()` and `Authority.describe()` are unchanged. Clean bundles print byte for byte as before. Same rule and same strings as the Python implementation
|
|
63
|
+
- **A malformed `--witness-keys` file stopped the CLI with a stack trace.** The file is now checked when it is loaded, and a bad row prints one line, `cannot use --witness-keys FILE: REASON`, naming the kid, exit 2; a file that is not JSON, or not an array of rows, prints the same line with its own reason. A file that cannot be read, the trust file or the bundle itself, prints `cannot read FILE: REASON`, exit 1, where it used to throw. A bundle the strict parser refuses (a duplicate member, a number past the double range, a lone surrogate), or a file that is neither a bundle nor a ledger, prints `cannot parse FILE: REASON`, exit 1, where it also threw; the Python CLI exits 1 on a file it cannot parse
|
|
64
|
+
- **Failure strings that differed from the Python implementation's.** An absent `v` or `seq` printed as `undefined` or `null` where Python prints `None` (`root_version_mismatch`, `mixed_entry_versions`, the envelope seq messages), and a boolean seq as `true` where Python prints `True` (`seq gap at 1 (got True)`); `mixed_entry_versions` listed versions of mixed types in an order of its own and `false` beside `0`, and now lists numbers first in numeric order, then the rest by Python's repr, each value Python counts equal once; `adapter[...]`, `receipt[...]`, `deny carries allow-only field(s)` and `v2_field_on_v1` used JSON spellings where Python prints its repr, and `v2_field_on_v1` said `schemaVersion: 1` where Python says `schema_version=1`. Found by running both CLIs over mutated bundles; each is now pinned against Python's own output
|
|
65
|
+
- **`witnessKeys` rows are read whole, one row per kid.** A row member other than `kid`, `alg`, `public_key_hex` and `not_after` is refused, and so is a second row naming a kid already named, each naming the kid. Read by projection, a misspelled `notAfter` was a key that never expired, and of two rows for one kid the later won, so a row added to expire a key could leave it trusted. In the `{kid: row}` form a row may repeat its kid and must agree with it. A trust set that carries extra members, such as a comment, is refused until they are removed
|
|
66
|
+
- **A `seq` or `v` follows the schema's integer type, by the same rule in both implementations.** A `seq` or `v` is an integral number that is not a boolean, as JSON Schema 2020-12 defines `integer` (`schema/agent-audit.schema.json` in the Python distribution): `1.0` is 1 and `-0` is 0, and RFC 8785 writes both as the integer, so they hash alike; a boolean, a string, null or a fractional number is not one. This build already read seqs and versions by value, and the Python implementation now reads them by the same rule, in the ledger, the anchor, the integrity position, the envelope subject, `signEnvelope` and every `v`, so a seq or version written `1.0` reads as 1 in both, and both print an integral `seq` or `v` as the integer (`seq gap at 1 (got 2)`, `envelope v=2`). A version that is not a number is compared as Python compares it, so a bundle whose `v` is a list, carried unchanged by its anchor and every entry, is reported once, as `unsupported_version`, where `anchor_version_mismatch`, `root_version_mismatch` and `mixed_entry_versions` also fired
|
|
67
|
+
- **An anchor whose `seq` or `v` is a number past 2^53 stopped `attenu-guard verify --hs256-key` with a stack trace.** JCS cannot represent such a number here, so there is no signing input to check the anchor's signature over, and `verifyBundle` threw where it reports. `AuditLog.verifyAnchor` now returns `anchor signature invalid`, and the CLI prints `integrity(anchor): anchor signature invalid`, as the Python implementation does. An anchor holding an integer past 2^53 anywhere gets that line before its `sig` is read, as in Python, so one whose `sig` is not hex as well reads the same in both
|
|
68
|
+
- **An entry carrying a number past 2^53 stopped `attenu-guard verify` with a stack trace, with or without a key.** JCS cannot represent such a number here, so the hash recorded on the entry cannot be reproduced. `AuditLog.verify` now reports the existing integrity failure at that entry, `hash mismatch at seq N`, and `verifyBundle` and the CLI report it where they threw. The Python implementation reports an integer past that range the same way
|
|
69
|
+
- **An allow whose `scope` is not a string stopped `attenu-guard verify` with a stack trace.** Against a node holding a wildcard the scope reached `startsWith` and `verifyBundle` threw; against a plain scope it already failed containment. It is now that containment failure either way, at that allow, as the Python implementation reports it: `containment: allow of 5 on c:n0 outside its authority ['crm.*']`, with the scope printed the way Python's `repr` prints it
|
|
70
|
+
- **An allow whose `context` was not an object, such as `[1]`, `5`, `"x"` or `true`, verified as if it had none.** It is now the containment failure at that allow, `containment: allow of 'mail.send' on vectors:n0 outside its authority ['crm.*', 'mail.send']`, and the bundle does not verify. An absent context, and a value the Python implementation reads as none (null, false, 0, `""`, `[]` or `{}`), is no context, as before
|
|
71
|
+
- **An envelope over an entry without a seq was checked against no hash.** The recomputed hashes and the count of envelopes per entry were kept by seq, so a subject naming an entry that has no seq was compared with nothing, and its mismatch message printed `None` for the recomputed hash; of two such entries, the second envelope was reported as a duplicate of the first. Both are kept by entry index now, as in the Python implementation: a subject's seq finds the entry whose seq is an integral number that is not a boolean, or, for an entry with no seq, the entry at that index, and the message shows the hash recomputed for that entry. An entry whose seq is null is at no seq, where it was found by its index: an envelope naming that index is now `no entry at seq N in this bundle`. `signEnvelope` resolves a seq the same way, so where two entries share a seq it signs the later one, which is the one the verifier checks it against
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
- The README and the `WITNESS_SIGNED` doc comment say what a witness signature attests: the entry's hash and chain position, plus what the witness-key holder observed. It does not attest that the action was permitted. Raised in review on dev.to by mickyarun and peterbuildssecure (attenu-io/attenu-guard#22)
|
|
75
|
+
|
|
76
|
+
### Known differences
|
|
77
|
+
- A float literal past 2^53 in a ledger field, such as `1e300`, verifies in the Python implementation and fails here as a hash mismatch at that entry, because this canonicalizer refuses an integral number past 2^53 where RFC 8785 serializes any finite double
|
|
78
|
+
- An allow whose `context` carries a value of the wrong type for a ceiling, such as `{"rows": "many"}`, fails containment here, and the Python implementation raises on it
|
|
79
|
+
- `delegationGraph` lists an edge only for a parent that is a non-empty string, which keeps its exported type, where the Python implementation's `delegation_graph` lists any parent it reads as true. The difference is in that view only and no verdict depends on it
|
|
80
|
+
|
|
9
81
|
## [0.12.0] - 2026-09-28
|
|
10
82
|
|
|
11
83
|
### Fixed
|
package/README.md
CHANGED
|
@@ -206,6 +206,33 @@ the per-entry state, the result and the report line — `witness-signed
|
|
|
206
206
|
(matched)`, and so on — for every entry, so a reader sees which hops were
|
|
207
207
|
covered before reading which one failed.
|
|
208
208
|
|
|
209
|
+
`witness-signed` says which key signed, not that the action was allowed. A witness
|
|
210
|
+
signature covers the entry's hash and chain position, plus what the witness-key
|
|
211
|
+
holder observed. It does not attest that the action was permitted. Monotonicity
|
|
212
|
+
and containment answer that, from the ledger: a witness can sign an allow that
|
|
213
|
+
containment rejects, and then the bundle fails. `attenu-guard verify FILE
|
|
214
|
+
--entries` prints one line per entry with its state and the checks that failed
|
|
215
|
+
on it, so the signature and the failure are read side by side. A failure is
|
|
216
|
+
matched to its entry by index, so an entry whose seq is missing or forged still
|
|
217
|
+
shows it. Each line is `key=value` tokens split by single spaces: the key is the
|
|
218
|
+
text before the first `=`, a value never contains a space, and a value that
|
|
219
|
+
starts with `"` is a JSON string.
|
|
220
|
+
|
|
221
|
+
A trust-set row may carry `not_after`, an RFC 3339 date-time in UTC written with
|
|
222
|
+
`Z`, such as `2026-10-05T00:00:00Z`. A row whose `not_after` is at or before the
|
|
223
|
+
verification time is not trusted, so an envelope naming its kid fails
|
|
224
|
+
`envelope_unknown_witness`, and the message says when the key expired. The
|
|
225
|
+
verification time is `now` in `verifyBundle`'s options, and the current time
|
|
226
|
+
when it is not given. A row is read whole: a member other than `kid`, `alg`,
|
|
227
|
+
`public_key_hex` and `not_after`, or a second row for one kid, is refused. A
|
|
228
|
+
trust file the CLI cannot use is one line naming the file and, for a bad row,
|
|
229
|
+
the kid, with exit code 2.
|
|
230
|
+
|
|
231
|
+
No value from a bundle can add a line to `attenu-guard verify` output. A value
|
|
232
|
+
printed without quotes prints as it is only when it is printable ASCII with no
|
|
233
|
+
space, `"` or `\`, and as escaped JSON otherwise; a quoted value is escaped the
|
|
234
|
+
way Python's `repr` escapes it. Clean bundles print as they always have.
|
|
235
|
+
|
|
209
236
|
One entry, at most one envelope. A second envelope naming a `subject.seq` an
|
|
210
237
|
earlier one in the same array already named is `envelope_duplicate_subject` at
|
|
211
238
|
the covered entry, and that entry reports `process-asserted`: two observations
|
package/dist/cjs/audit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/audit.ts"],"names":[],"mappings":"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"}
|
package/dist/cjs/audit.js
CHANGED
|
@@ -19,6 +19,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
19
19
|
const node_fs_1 = require("node:fs");
|
|
20
20
|
const node_path_1 = require("node:path");
|
|
21
21
|
const canonical_js_1 = require("./canonical.js");
|
|
22
|
+
const display_js_1 = require("./display.js");
|
|
22
23
|
exports.SCHEMA_VERSION = 1;
|
|
23
24
|
exports.GENESIS = "0".repeat(64);
|
|
24
25
|
/** `sha256(prev_hash || canonical(event-without-hash))`, as hex. */
|
|
@@ -171,25 +172,43 @@ class AuditLog {
|
|
|
171
172
|
if (key !== "kid" && key !== "sig" && key !== "verified")
|
|
172
173
|
body[key] = value;
|
|
173
174
|
}
|
|
175
|
+
// An integer past ±(2^53 - 1) has no RFC 8785 form, so no signer produced this anchor and its
|
|
176
|
+
// signature does not verify, whatever `sig` holds. The Python implementation's canonicalizer
|
|
177
|
+
// refuses one before the signature is read; this is the same order, so the same anchor gets
|
|
178
|
+
// the same line.
|
|
179
|
+
if ((0, canonical_js_1.holdsUnsafeInteger)(body))
|
|
180
|
+
return [false, "anchor signature invalid"];
|
|
174
181
|
const sigHex = (0, canonical_js_1.toPlain)(a["sig"]) ?? "";
|
|
175
182
|
if (typeof sigHex !== "string" || !/^(?:[0-9a-fA-F]{2})*$/.test(sigHex)) {
|
|
176
183
|
return [false, "anchor signature not hex"];
|
|
177
184
|
}
|
|
178
185
|
const sig = Buffer.from(sigHex, "hex");
|
|
179
186
|
const kid = (0, canonical_js_1.toPlain)(a["kid"]);
|
|
180
|
-
|
|
187
|
+
let signingInput;
|
|
188
|
+
try {
|
|
189
|
+
signingInput = (0, canonical_js_1.canonicalBytes)(body);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// A member JCS cannot represent here, such as a float past ±(2^53 - 1) in `seq` or `v`, leaves
|
|
193
|
+
// no signing input to check the signature over, so the signature does not verify. Reported,
|
|
194
|
+
// never thrown: a bundle is attacker-supplied. The Python implementation, which can write
|
|
195
|
+
// such a float, reports the same anchor the same way, its signature failing to verify.
|
|
196
|
+
return [false, "anchor signature invalid"];
|
|
197
|
+
}
|
|
198
|
+
if (!signer.verify(signingInput, sig, kid)) {
|
|
181
199
|
return [false, "anchor signature invalid"];
|
|
182
200
|
}
|
|
183
201
|
const [ok, err] = AuditLog.verify(entries);
|
|
184
202
|
if (!ok)
|
|
185
203
|
return [false, err];
|
|
186
204
|
if (entries.length === 0)
|
|
187
|
-
return [
|
|
205
|
+
return [(0, canonical_js_1.integral)(a["seq"]) === -1, null];
|
|
188
206
|
if ((0, canonical_js_1.toPlain)(a["chain_id"]) !== firstChainId(entries)) {
|
|
189
207
|
return [false, "anchor chain_id does not match the ledger entries"];
|
|
190
208
|
}
|
|
191
209
|
const last = entries[entries.length - 1];
|
|
192
|
-
|
|
210
|
+
// The anchor's seq is read by the seq rule (`integral`): `1.0` is 1, and `true` is not.
|
|
211
|
+
if (last["hash"] !== (0, canonical_js_1.toPlain)(a["head"]) || (0, canonical_js_1.integral)(last["seq"]) !== (0, canonical_js_1.integral)(a["seq"])) {
|
|
193
212
|
return [false, "anchor head does not match the ledger head (ledger rewritten?)"];
|
|
194
213
|
}
|
|
195
214
|
return [true, null];
|
|
@@ -199,16 +218,35 @@ class AuditLog {
|
|
|
199
218
|
let prev = exports.GENESIS;
|
|
200
219
|
let expectedSeq = 0;
|
|
201
220
|
for (const e of entries) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
221
|
+
// An integral number and never a boolean (`integral`), as the schema's integer type
|
|
222
|
+
// defines: `1.0` is 1, and RFC 8785 writes it as 1, so it hashes as 1. A chain re-hashed
|
|
223
|
+
// around `"seq": true` or `"seq": 1.5` at index 1 is a seq gap there, as it is to the Python
|
|
224
|
+
// implementation.
|
|
225
|
+
if ((0, canonical_js_1.integral)(e["seq"]) !== expectedSeq) {
|
|
226
|
+
// `seq` is the entry's own value, which a forged ledger chooses: printed by the one rule
|
|
227
|
+
// (display.ts `shown`) so it cannot end the reader's line and start another, and an
|
|
228
|
+
// integral one as its integer.
|
|
229
|
+
return [false, `seq gap at ${expectedSeq} (got ${(0, display_js_1.shown)((0, canonical_js_1.intOr)(e["seq"]))})`];
|
|
205
230
|
}
|
|
206
231
|
const stored = e["hash"];
|
|
207
232
|
const payload = withoutHash(e);
|
|
208
233
|
if ((0, canonical_js_1.toPlain)(payload["prev_hash"]) !== prev) {
|
|
209
234
|
return [false, `prev_hash mismatch at seq ${expectedSeq}`];
|
|
210
235
|
}
|
|
211
|
-
|
|
236
|
+
let reproduced;
|
|
237
|
+
try {
|
|
238
|
+
reproduced = hashEntry(prev, payload) === stored;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// A member JCS cannot represent here, such as a number past ±(2^53 - 1), leaves no hash to
|
|
242
|
+
// reproduce the recorded one with: the existing integrity failure at this entry, reported
|
|
243
|
+
// and never thrown, since a ledger is attacker-supplied. The Python implementation reports
|
|
244
|
+
// an integer past that range the same way. A float past it hashes there, so a chain carrying
|
|
245
|
+
// `1e300` verifies in Python and fails here: this JCS refuses an integral number past that
|
|
246
|
+
// range, where RFC 8785 serializes any finite double.
|
|
247
|
+
reproduced = false;
|
|
248
|
+
}
|
|
249
|
+
if (!reproduced) {
|
|
212
250
|
return [false, `hash mismatch at seq ${expectedSeq}`];
|
|
213
251
|
}
|
|
214
252
|
prev = stored;
|
|
@@ -229,10 +267,6 @@ class AuditLog {
|
|
|
229
267
|
}
|
|
230
268
|
}
|
|
231
269
|
exports.AuditLog = AuditLog;
|
|
232
|
-
function formatSeq(value) {
|
|
233
|
-
const plain = (0, canonical_js_1.toPlain)(value);
|
|
234
|
-
return plain === undefined || plain === null ? "None" : String(plain);
|
|
235
|
-
}
|
|
236
270
|
/** The first `chain_id` any entry carries, or `"chain"`. */
|
|
237
271
|
function chainIdOf(entries) {
|
|
238
272
|
for (const e of entries) {
|
package/dist/cjs/audit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/audit.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/audit.ts"],"names":[],"mappings":"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"}
|
package/dist/cjs/authority.d.ts
CHANGED
|
@@ -58,10 +58,10 @@ export declare class Authority {
|
|
|
58
58
|
*/
|
|
59
59
|
readonly scopes: ReadonlySet<string>;
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
* deterministic wire form and integrity seal. A dimension
|
|
63
|
-
* unbounded on that dimension unless a parent in the chain
|
|
64
|
-
* attenuation can only add or tighten bounds, never remove one.
|
|
61
|
+
* One ceiling per `key` (a second one under the same key throws a TypeError),
|
|
62
|
+
* sorted by key for a deterministic wire form and integrity seal. A dimension
|
|
63
|
+
* with no ceiling is unbounded on that dimension unless a parent in the chain
|
|
64
|
+
* bounds it — attenuation can only add or tighten bounds, never remove one.
|
|
65
65
|
*/
|
|
66
66
|
readonly ceilings: readonly Ceiling[];
|
|
67
67
|
/** Seconds from issuance. `null` is unbounded (discouraged). */
|
|
@@ -75,8 +75,14 @@ export declare class Authority {
|
|
|
75
75
|
coversScope(requested: string): boolean;
|
|
76
76
|
/**
|
|
77
77
|
* The greatest authority within BOTH `this` and `other` — the attenuation.
|
|
78
|
-
* This is the only way a child authority is constructed
|
|
79
|
-
* and can only ever shrink
|
|
78
|
+
* This is the only way a child authority is constructed, with `this` the
|
|
79
|
+
* parent and `other` the request. It is commutative and can only ever shrink
|
|
80
|
+
* relative to either input, except in one case, by design (attenu-ops#110):
|
|
81
|
+
* a constraint this build does not define on the parent's side passes down,
|
|
82
|
+
* so the child inherits it, and it denies every action; one in the request
|
|
83
|
+
* under a parent's ceiling of another type is refused with an AuthorityError
|
|
84
|
+
* (reason `not_narrower`), as two different ceiling types under one key are.
|
|
85
|
+
* So `parent.meet(request)` and `request.meet(parent)` differ there.
|
|
80
86
|
*/
|
|
81
87
|
meet(other: Authority): Authority;
|
|
82
88
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authority.d.ts","sourceRoot":"","sources":["../../src/authority.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAA6C,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"authority.d.ts","sourceRoot":"","sources":["../../src/authority.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAA6C,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAElG,OAAO,EAML,KAAK,OAAO,EACZ,KAAK,OAAO,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,QAAQ,EAAsB,MAAM,cAAc,CAAC;AAE5D;;;;;;;GAOG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBAE1B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAM;CAM/E;AAqFD,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1B,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC7B,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;IACpC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAGnB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;CACtB;AAWD,qBAAa,SAAS;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAErC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAEtC,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEhB,IAAI,GAAE,aAAkB;IAmBpC,OAAO,CAAC,KAAK;IAMb,kDAAkD;IAClD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAMzC,4EAA4E;IAC5E,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAM5D,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IASvC;;;;;;;;;;OAUG;IACH,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS;IAuDjC;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO;IAoBzC;;;;;;;;OAQG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,OAAO,GAAG,IAAW,GAAG,QAAQ;IAgC5D,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAM/B,MAAM,IAAI,aAAa;IAQvB,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS;IAiCvC,0CAA0C;IAC1C,QAAQ,IAAI,MAAM;IAMlB,QAAQ,IAAI,MAAM;CAGnB"}
|
package/dist/cjs/authority.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.Authority = exports.AuthorityError = void 0;
|
|
25
25
|
const canonical_js_1 = require("./canonical.js");
|
|
26
|
+
const display_js_1 = require("./display.js");
|
|
26
27
|
const ceilings_js_1 = require("./ceilings.js");
|
|
27
28
|
const reasons_js_1 = require("./reasons.js");
|
|
28
29
|
/**
|
|
@@ -90,13 +91,19 @@ function ceilingFromWireWhole(c) {
|
|
|
90
91
|
// cannot see because the member set is unchanged. Checked on its own rather
|
|
91
92
|
// than by returning to whole-value equality, which broke conformant tokens a
|
|
92
93
|
// revision ago.
|
|
94
|
+
// The values in these messages are the bundle's, and the messages reach `attenu-guard verify`
|
|
95
|
+
// output inside "unreadable authority (...)". So they print through Python's `repr`, as the
|
|
96
|
+
// Python port prints them: quoted, every non-printable character escaped, one line.
|
|
93
97
|
if (emitted["key"] !== input["key"]) {
|
|
94
|
-
throw new AuthorityError(`constraint ${
|
|
95
|
-
`but this build reads it as ${
|
|
98
|
+
throw new AuthorityError(`constraint ${(0, display_js_1.pyRepr)(input)} names dimension ${(0, display_js_1.pyRepr)(input["key"] ?? null)} ` +
|
|
99
|
+
`but this build reads it as ${(0, display_js_1.pyRepr)(emitted["key"] ?? null)}; refusing rather ` +
|
|
96
100
|
"than silently changing which dimension is bounded", "malformed_constraint");
|
|
97
101
|
}
|
|
98
102
|
const dropped = Object.keys(input)
|
|
99
|
-
|
|
103
|
+
// The emission's OWN members: `k in emitted` found `constructor`, `toString` and the rest of
|
|
104
|
+
// Object.prototype on every plain object, so a member named like one was ignored, where the
|
|
105
|
+
// Python implementation refuses it (attenu-ops#110).
|
|
106
|
+
.filter((k) => !Object.prototype.hasOwnProperty.call(emitted, k))
|
|
100
107
|
// `field` is read and then not re-emitted when it equals `key`, because at
|
|
101
108
|
// that point it is redundant. Absent from the emission does not mean
|
|
102
109
|
// unread, so exempt it only on EVIDENCE that this ceiling parsed it: the
|
|
@@ -106,18 +113,17 @@ function ceilingFromWireWhole(c) {
|
|
|
106
113
|
// (which never read `field` at all) and unconditionally for a custom
|
|
107
114
|
// ceiling deriving `ctxField` from its own input.
|
|
108
115
|
.filter((k) => !(k === "field" && ceiling.field === input["field"]))
|
|
109
|
-
.sort();
|
|
116
|
+
.sort(canonical_js_1.compareCodePoints);
|
|
110
117
|
if (dropped.length > 0) {
|
|
111
|
-
throw new AuthorityError(`constraint ${
|
|
112
|
-
`evaluate and will not ignore: ${dropped.join(", ")}`, "malformed_constraint");
|
|
118
|
+
throw new AuthorityError(`constraint ${(0, display_js_1.pyRepr)(input)} carries members this build does not ` +
|
|
119
|
+
`evaluate and will not ignore: ${dropped.map(display_js_1.pyStrRepr).join(", ")}`, "malformed_constraint");
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
122
|
return ceiling;
|
|
116
123
|
}
|
|
117
|
-
const SCOPE_RE = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*\.(?:[a-z][a-z0-9_-]*|\*)$/;
|
|
118
124
|
function validateScope(scope) {
|
|
119
|
-
if (typeof scope !== "string" || !SCOPE_RE.test(scope)) {
|
|
120
|
-
throw new TypeError(`invalid scope ${
|
|
125
|
+
if (typeof scope !== "string" || !ceilings_js_1.SCOPE_RE.test(scope)) {
|
|
126
|
+
throw new TypeError(`invalid scope ${(0, display_js_1.pyRepr)((scope ?? null))}: expected lowercase dot-separated segments; ` +
|
|
121
127
|
"'*' is permitted only as the complete final segment after a dot");
|
|
122
128
|
}
|
|
123
129
|
}
|
|
@@ -131,10 +137,10 @@ class Authority {
|
|
|
131
137
|
*/
|
|
132
138
|
scopes;
|
|
133
139
|
/**
|
|
134
|
-
*
|
|
135
|
-
* deterministic wire form and integrity seal. A dimension
|
|
136
|
-
* unbounded on that dimension unless a parent in the chain
|
|
137
|
-
* attenuation can only add or tighten bounds, never remove one.
|
|
140
|
+
* One ceiling per `key` (a second one under the same key throws a TypeError),
|
|
141
|
+
* sorted by key for a deterministic wire form and integrity seal. A dimension
|
|
142
|
+
* with no ceiling is unbounded on that dimension unless a parent in the chain
|
|
143
|
+
* bounds it — attenuation can only add or tighten bounds, never remove one.
|
|
138
144
|
*/
|
|
139
145
|
ceilings;
|
|
140
146
|
/** Seconds from issuance. `null` is unbounded (discouraged). */
|
|
@@ -145,8 +151,14 @@ class Authority {
|
|
|
145
151
|
validateScope(scope);
|
|
146
152
|
this.scopes = scopes;
|
|
147
153
|
const byKey = new Map();
|
|
148
|
-
for (const c of init.ceilings ?? [])
|
|
154
|
+
for (const c of init.ceilings ?? []) {
|
|
155
|
+
// One constraint per key, on every path (attenu-ops#110): the last one won, silently, so
|
|
156
|
+
// [allow region in [us], deny region not in [rm]] kept only the deny-list.
|
|
157
|
+
if (byKey.has(String(c.key))) {
|
|
158
|
+
throw new TypeError(`two constraints share the key ${(0, display_js_1.pyRepr)(c.key)}; an authority holds one per key`);
|
|
159
|
+
}
|
|
149
160
|
byKey.set(String(c.key), c);
|
|
161
|
+
}
|
|
150
162
|
this.ceilings = Array.from(byKey.keys())
|
|
151
163
|
.sort(canonical_js_1.compareCodePoints)
|
|
152
164
|
.map((k) => byKey.get(k));
|
|
@@ -181,8 +193,14 @@ class Authority {
|
|
|
181
193
|
// ---- the lattice ------------------------------------------------------
|
|
182
194
|
/**
|
|
183
195
|
* The greatest authority within BOTH `this` and `other` — the attenuation.
|
|
184
|
-
* This is the only way a child authority is constructed
|
|
185
|
-
* and can only ever shrink
|
|
196
|
+
* This is the only way a child authority is constructed, with `this` the
|
|
197
|
+
* parent and `other` the request. It is commutative and can only ever shrink
|
|
198
|
+
* relative to either input, except in one case, by design (attenu-ops#110):
|
|
199
|
+
* a constraint this build does not define on the parent's side passes down,
|
|
200
|
+
* so the child inherits it, and it denies every action; one in the request
|
|
201
|
+
* under a parent's ceiling of another type is refused with an AuthorityError
|
|
202
|
+
* (reason `not_narrower`), as two different ceiling types under one key are.
|
|
203
|
+
* So `parent.meet(request)` and `request.meet(parent)` differ there.
|
|
186
204
|
*/
|
|
187
205
|
meet(other) {
|
|
188
206
|
// Scopes: keep a requested scope only if this side covers it, and keep this
|
|
@@ -211,6 +229,19 @@ class Authority {
|
|
|
211
229
|
for (const k of keys) {
|
|
212
230
|
const a = mine.get(k);
|
|
213
231
|
const b = theirs.get(k);
|
|
232
|
+
if (a !== undefined && b !== undefined && !(0, ceilings_js_1.sameType)(a, b)) {
|
|
233
|
+
// Two ceiling types under one key have no common narrowing, so the delegation is refused
|
|
234
|
+
// as not narrower, and `Guard.delegate` records it as `spawn_denied` (attenu-ops#110). One
|
|
235
|
+
// exception: this side's constraint this build does not define, which denies every action,
|
|
236
|
+
// is kept, so the child inherits it, as 0.13.0 delegated from a parent holding one. A
|
|
237
|
+
// request carrying one under this side's other ceiling is refused: the child would not be
|
|
238
|
+
// narrower (`isNarrowerThan`).
|
|
239
|
+
if (a instanceof ceilings_js_1.UnknownCeiling) {
|
|
240
|
+
ceilings.push(a);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
throw new AuthorityError(`constraint ${(0, display_js_1.pyRepr)(k)} has a different ceiling type on each side; neither narrows the other`, "not_narrower", { constraint: k });
|
|
244
|
+
}
|
|
214
245
|
ceilings.push(a !== undefined && b !== undefined ? a.narrow(b) : (a ?? b));
|
|
215
246
|
}
|
|
216
247
|
const ttls = [this.ttl, other.ttl].filter((t) => t !== null);
|
|
@@ -243,7 +274,9 @@ class Authority {
|
|
|
243
274
|
const selfCeiling = mine.get(k);
|
|
244
275
|
if (selfCeiling === undefined)
|
|
245
276
|
return false; // unbounded here where other bounds
|
|
246
|
-
|
|
277
|
+
// Two ceiling types under one key are not comparable, so not narrower (a custom ceiling's
|
|
278
|
+
// subsumes() need not handle another type).
|
|
279
|
+
if (!(0, ceilings_js_1.sameType)(selfCeiling, otherCeiling) || !otherCeiling.subsumes(selfCeiling))
|
|
247
280
|
return false;
|
|
248
281
|
}
|
|
249
282
|
if (other.ttl !== null) {
|
|
@@ -273,14 +306,21 @@ class Authority {
|
|
|
273
306
|
.join(", ")}]`,
|
|
274
307
|
}));
|
|
275
308
|
}
|
|
276
|
-
// Reserved key so scoped ceilings can tell whether they apply.
|
|
309
|
+
// Reserved key so scoped ceilings can tell whether they apply. Always the scope being checked: a
|
|
310
|
+
// `_scope` in the caller's context is ignored, so it can move no call off its own meter or onto
|
|
311
|
+
// another (attenu-ops#110).
|
|
277
312
|
const cctx = { ...context };
|
|
278
|
-
|
|
279
|
-
cctx["_scope"] = scope;
|
|
313
|
+
cctx["_scope"] = scope;
|
|
280
314
|
for (const c of this.ceilings) {
|
|
281
315
|
const decision = c.permits(cctx);
|
|
282
|
-
if (
|
|
316
|
+
if (decision.allowed)
|
|
317
|
+
continue;
|
|
318
|
+
// A denial with no reason still denies: an empty list read as an allow, so a custom ceiling's
|
|
319
|
+
// bare `Decision.deny([])` let every call through (attenu-ops#110).
|
|
320
|
+
if (decision.reasons.length > 0)
|
|
283
321
|
reasons.push(...decision.reasons);
|
|
322
|
+
else
|
|
323
|
+
reasons.push(new reasons_js_1.Reason(reasons_js_1.ReasonCode.CEILING_EXCEEDED, { constraint: c.key, message: "denied without a reason" }));
|
|
284
324
|
}
|
|
285
325
|
return reasons.length > 0 ? reasons_js_1.Decision.deny(reasons) : reasons_js_1.Decision.allow();
|
|
286
326
|
}
|
|
@@ -308,10 +348,12 @@ class Authority {
|
|
|
308
348
|
// entry's TOP-LEVEL keys. Kept in step with the Python port.
|
|
309
349
|
const unknownAuthorityMembers = Object.keys(d)
|
|
310
350
|
.filter((k) => k !== "scopes" && k !== "constraints" && k !== "ttl")
|
|
311
|
-
.sort();
|
|
351
|
+
.sort(canonical_js_1.compareCodePoints);
|
|
312
352
|
if (unknownAuthorityMembers.length > 0) {
|
|
313
|
-
|
|
314
|
-
|
|
353
|
+
// Through Python's `repr`, as the Python port prints it: this message reaches
|
|
354
|
+
// `attenu-guard verify` output, and a member name is the bundle's.
|
|
355
|
+
throw new AuthorityError(`authority ${(0, display_js_1.pyRepr)(d)} carries members this build does not evaluate ` +
|
|
356
|
+
`and will not ignore: ${unknownAuthorityMembers.map(display_js_1.pyStrRepr).join(", ")}`, "malformed_authority");
|
|
315
357
|
}
|
|
316
358
|
const scopes = d["scopes"] ?? [];
|
|
317
359
|
const constraints = d["constraints"] ?? [];
|