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.
Files changed (73) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +27 -0
  3. package/dist/cjs/audit.d.ts.map +1 -1
  4. package/dist/cjs/audit.js +45 -11
  5. package/dist/cjs/audit.js.map +1 -1
  6. package/dist/cjs/authority.d.ts +12 -6
  7. package/dist/cjs/authority.d.ts.map +1 -1
  8. package/dist/cjs/authority.js +66 -24
  9. package/dist/cjs/authority.js.map +1 -1
  10. package/dist/cjs/canonical.d.ts +24 -0
  11. package/dist/cjs/canonical.d.ts.map +1 -1
  12. package/dist/cjs/canonical.js +49 -0
  13. package/dist/cjs/canonical.js.map +1 -1
  14. package/dist/cjs/ceilings.d.ts +71 -9
  15. package/dist/cjs/ceilings.d.ts.map +1 -1
  16. package/dist/cjs/ceilings.js +432 -62
  17. package/dist/cjs/ceilings.js.map +1 -1
  18. package/dist/cjs/cli.d.ts +12 -3
  19. package/dist/cjs/cli.d.ts.map +1 -1
  20. package/dist/cjs/cli.js +272 -20
  21. package/dist/cjs/cli.js.map +1 -1
  22. package/dist/cjs/display.d.ts +77 -0
  23. package/dist/cjs/display.d.ts.map +1 -0
  24. package/dist/cjs/display.js +239 -0
  25. package/dist/cjs/display.js.map +1 -0
  26. package/dist/cjs/evidence.d.ts +74 -5
  27. package/dist/cjs/evidence.d.ts.map +1 -1
  28. package/dist/cjs/evidence.js +656 -244
  29. package/dist/cjs/evidence.js.map +1 -1
  30. package/dist/cjs/guard.d.ts.map +1 -1
  31. package/dist/cjs/guard.js +7 -3
  32. package/dist/cjs/guard.js.map +1 -1
  33. package/dist/cjs/version.d.ts +1 -1
  34. package/dist/cjs/version.js +1 -1
  35. package/dist/cjs/wire.d.ts.map +1 -1
  36. package/dist/cjs/wire.js +3 -2
  37. package/dist/cjs/wire.js.map +1 -1
  38. package/dist/esm/audit.d.ts.map +1 -1
  39. package/dist/esm/audit.js +46 -12
  40. package/dist/esm/audit.js.map +1 -1
  41. package/dist/esm/authority.d.ts +12 -6
  42. package/dist/esm/authority.d.ts.map +1 -1
  43. package/dist/esm/authority.js +66 -24
  44. package/dist/esm/authority.js.map +1 -1
  45. package/dist/esm/canonical.d.ts +24 -0
  46. package/dist/esm/canonical.d.ts.map +1 -1
  47. package/dist/esm/canonical.js +46 -0
  48. package/dist/esm/canonical.js.map +1 -1
  49. package/dist/esm/ceilings.d.ts +71 -9
  50. package/dist/esm/ceilings.d.ts.map +1 -1
  51. package/dist/esm/ceilings.js +428 -62
  52. package/dist/esm/ceilings.js.map +1 -1
  53. package/dist/esm/cli.d.ts +12 -3
  54. package/dist/esm/cli.d.ts.map +1 -1
  55. package/dist/esm/cli.js +273 -21
  56. package/dist/esm/cli.js.map +1 -1
  57. package/dist/esm/display.d.ts +77 -0
  58. package/dist/esm/display.d.ts.map +1 -0
  59. package/dist/esm/display.js +227 -0
  60. package/dist/esm/display.js.map +1 -0
  61. package/dist/esm/evidence.d.ts +74 -5
  62. package/dist/esm/evidence.d.ts.map +1 -1
  63. package/dist/esm/evidence.js +639 -230
  64. package/dist/esm/evidence.js.map +1 -1
  65. package/dist/esm/guard.d.ts.map +1 -1
  66. package/dist/esm/guard.js +8 -4
  67. package/dist/esm/guard.js.map +1 -1
  68. package/dist/esm/version.d.ts +1 -1
  69. package/dist/esm/version.js +1 -1
  70. package/dist/esm/wire.d.ts.map +1 -1
  71. package/dist/esm/wire.js +3 -2
  72. package/dist/esm/wire.js.map +1 -1
  73. 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
@@ -1 +1 @@
1
- {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,EAKL,KAAK,KAAK,EACX,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,OAAO,QAAiB,CAAC;AAEtC,8BAA8B;AAC9B,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAEhD,0EAA0E;AAC1E,MAAM,WAAW,IAAI;IACnB,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;CACjC;AAED,oDAAoD;AACpD,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,KAAK,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;CAClC;AAED,oEAAoE;AACpE,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,CAKxE;AAeD;;;;;;;;GAQG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAS1C,QAAQ,CAAC,KAAK,EAAE,WAAW;IAR7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;gBAGT,KAAK,EAAE,WAAW,EAC3B,KAAK,EAAE,OAAO;CASjB;AAED,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;IACxB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,8CAA8C;AAC9C,qBAAa,QAAQ;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,KAAK,EAAE,SAAS,IAAI,EAAE,CAAC;IACvB,OAAO,CAAC,IAAI,CAAW;IACvB,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAE9C;;;;OAIG;gBACS,IAAI,GAAE,YAAY,GAAG,MAAM,GAAG,IAAS;IAoBnD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,GAAE,WAAgB,GAAG,WAAW;IAyBjF,IAAI,OAAO,IAAI,WAAW,EAAE,CAE3B;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,WAAW,CAAC;IAM1C,kFAAkF;IAClF,IAAI,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;IAMxB,OAAO,CAAC,WAAW;IAInB;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,GAAE,MAAM,GAAG,MAAU,GAAG,MAAM;IAUvD,iEAAiE;IACjE,MAAM,CAAC,YAAY,CACjB,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,IAAI,GAAG,SAAS,EAChD,MAAM,EAAE,MAAM,GACb,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IA6B3B,2DAA2D;IAC3D,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAsBxE,uEAAuE;IACvE,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE;IAIxC,iEAAiE;IACjE,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE;CAM/C;AAOD,4DAA4D;AAC5D,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAMjE"}
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
- if (!signer.verify((0, canonical_js_1.canonicalBytes)(body), sig, kid)) {
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 [asNumber(a["seq"]) === -1, null];
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
- if (last["hash"] !== (0, canonical_js_1.toPlain)(a["head"]) || asNumber(last["seq"]) !== asNumber(a["seq"])) {
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
- const seq = asNumber(e["seq"]);
203
- if (seq !== expectedSeq) {
204
- return [false, `seq gap at ${expectedSeq} (got ${formatSeq(e["seq"])})`];
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
- if (hashEntry(prev, payload) !== stored) {
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) {
@@ -1 +1 @@
1
- {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/audit.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/audit.ts"],"names":[],"mappings":"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"}
@@ -58,10 +58,10 @@ export declare class Authority {
58
58
  */
59
59
  readonly scopes: ReadonlySet<string>;
60
60
  /**
61
- * At most one ceiling per `key` (last one wins), sorted by key for a
62
- * deterministic wire form and integrity seal. A dimension with no ceiling is
63
- * unbounded on that dimension unless a parent in the chain bounds it —
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. It is commutative
79
- * and can only ever shrink relative to either input.
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;AAClG,OAAO,EAGL,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;AA+ED,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;AAaD,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;IAYpC,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;;;;OAIG;IACH,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS;IAsCjC;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO;IAkBzC;;;;;;;;OAQG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,OAAO,GAAG,IAAW,GAAG,QAAQ;IA0B5D,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS;IAM/B,MAAM,IAAI,aAAa;IAQvB,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS;IA+BvC,0CAA0C;IAC1C,QAAQ,IAAI,MAAM;IAMlB,QAAQ,IAAI,MAAM;CAGnB"}
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"}
@@ -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 ${JSON.stringify(input)} names dimension ${JSON.stringify(input["key"])} ` +
95
- `but this build reads it as ${JSON.stringify(emitted["key"])}; refusing rather ` +
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
- .filter((k) => !(k in emitted))
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 ${JSON.stringify(input)} carries members this build does not ` +
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 ${JSON.stringify(scope)}: expected lowercase dot-separated segments; ` +
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
- * At most one ceiling per `key` (last one wins), sorted by key for a
135
- * deterministic wire form and integrity seal. A dimension with no ceiling is
136
- * unbounded on that dimension unless a parent in the chain bounds it —
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. It is commutative
185
- * and can only ever shrink relative to either input.
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
- if (!otherCeiling.subsumes(selfCeiling))
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
- if (!("_scope" in cctx))
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 (!decision.allowed)
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
- throw new AuthorityError(`authority ${JSON.stringify(d)} carries members this build does not evaluate ` +
314
- `and will not ignore: ${unknownAuthorityMembers.join(", ")}`, "malformed_authority");
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"] ?? [];