attenu-guard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/LICENSE +202 -0
- package/README.md +200 -0
- package/SECURITY.md +34 -0
- package/bin/attenu-guard.js +4 -0
- package/dist/cjs/adapters/langgraph.d.ts +174 -0
- package/dist/cjs/adapters/langgraph.d.ts.map +1 -0
- package/dist/cjs/adapters/langgraph.js +242 -0
- package/dist/cjs/adapters/langgraph.js.map +1 -0
- package/dist/cjs/audit.d.ts +82 -0
- package/dist/cjs/audit.d.ts.map +1 -0
- package/dist/cjs/audit.js +195 -0
- package/dist/cjs/audit.js.map +1 -0
- package/dist/cjs/authority.d.ts +114 -0
- package/dist/cjs/authority.d.ts.map +1 -0
- package/dist/cjs/authority.js +238 -0
- package/dist/cjs/authority.js.map +1 -0
- package/dist/cjs/canonical.d.ts +91 -0
- package/dist/cjs/canonical.d.ts.map +1 -0
- package/dist/cjs/canonical.js +372 -0
- package/dist/cjs/canonical.js.map +1 -0
- package/dist/cjs/ceilings.d.ts +211 -0
- package/dist/cjs/ceilings.d.ts.map +1 -0
- package/dist/cjs/ceilings.js +476 -0
- package/dist/cjs/ceilings.js.map +1 -0
- package/dist/cjs/chain.d.ts +106 -0
- package/dist/cjs/chain.d.ts.map +1 -0
- package/dist/cjs/chain.js +0 -0
- package/dist/cjs/chain.js.map +1 -0
- package/dist/cjs/cli.d.ts +16 -0
- package/dist/cjs/cli.d.ts.map +1 -0
- package/dist/cjs/cli.js +100 -0
- package/dist/cjs/cli.js.map +1 -0
- package/dist/cjs/evidence.d.ts +163 -0
- package/dist/cjs/evidence.d.ts.map +1 -0
- package/dist/cjs/evidence.js +409 -0
- package/dist/cjs/evidence.js.map +1 -0
- package/dist/cjs/guard.d.ts +186 -0
- package/dist/cjs/guard.d.ts.map +1 -0
- package/dist/cjs/guard.js +432 -0
- package/dist/cjs/guard.js.map +1 -0
- package/dist/cjs/index.d.ts +52 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +99 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/reasons.d.ts +93 -0
- package/dist/cjs/reasons.d.ts.map +1 -0
- package/dist/cjs/reasons.js +139 -0
- package/dist/cjs/reasons.js.map +1 -0
- package/dist/cjs/strikes.d.ts +24 -0
- package/dist/cjs/strikes.d.ts.map +1 -0
- package/dist/cjs/strikes.js +30 -0
- package/dist/cjs/strikes.js.map +1 -0
- package/dist/cjs/wire.d.ts +83 -0
- package/dist/cjs/wire.d.ts.map +1 -0
- package/dist/cjs/wire.js +165 -0
- package/dist/cjs/wire.js.map +1 -0
- package/dist/esm/adapters/langgraph.d.ts +174 -0
- package/dist/esm/adapters/langgraph.d.ts.map +1 -0
- package/dist/esm/adapters/langgraph.js +199 -0
- package/dist/esm/adapters/langgraph.js.map +1 -0
- package/dist/esm/audit.d.ts +82 -0
- package/dist/esm/audit.d.ts.map +1 -0
- package/dist/esm/audit.js +189 -0
- package/dist/esm/audit.js.map +1 -0
- package/dist/esm/authority.d.ts +114 -0
- package/dist/esm/authority.d.ts.map +1 -0
- package/dist/esm/authority.js +233 -0
- package/dist/esm/authority.js.map +1 -0
- package/dist/esm/canonical.d.ts +91 -0
- package/dist/esm/canonical.d.ts.map +1 -0
- package/dist/esm/canonical.js +359 -0
- package/dist/esm/canonical.js.map +1 -0
- package/dist/esm/ceilings.d.ts +211 -0
- package/dist/esm/ceilings.d.ts.map +1 -0
- package/dist/esm/ceilings.js +460 -0
- package/dist/esm/ceilings.js.map +1 -0
- package/dist/esm/chain.d.ts +106 -0
- package/dist/esm/chain.d.ts.map +1 -0
- package/dist/esm/chain.js +0 -0
- package/dist/esm/chain.js.map +1 -0
- package/dist/esm/cli.d.ts +16 -0
- package/dist/esm/cli.d.ts.map +1 -0
- package/dist/esm/cli.js +97 -0
- package/dist/esm/cli.js.map +1 -0
- package/dist/esm/evidence.d.ts +163 -0
- package/dist/esm/evidence.d.ts.map +1 -0
- package/dist/esm/evidence.js +398 -0
- package/dist/esm/evidence.js.map +1 -0
- package/dist/esm/guard.d.ts +186 -0
- package/dist/esm/guard.d.ts.map +1 -0
- package/dist/esm/guard.js +427 -0
- package/dist/esm/guard.js.map +1 -0
- package/dist/esm/index.d.ts +52 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +42 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/reasons.d.ts +93 -0
- package/dist/esm/reasons.d.ts.map +1 -0
- package/dist/esm/reasons.js +134 -0
- package/dist/esm/reasons.js.map +1 -0
- package/dist/esm/strikes.d.ts +24 -0
- package/dist/esm/strikes.d.ts.map +1 -0
- package/dist/esm/strikes.js +26 -0
- package/dist/esm/strikes.js.map +1 -0
- package/dist/esm/wire.d.ts +83 -0
- package/dist/esm/wire.d.ts.map +1 -0
- package/dist/esm/wire.js +159 -0
- package/dist/esm/wire.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-chained audit log — the open, verifiable record of every authority decision.
|
|
3
|
+
*
|
|
4
|
+
* Each event is appended as one JSON line whose `hash` covers the event plus the
|
|
5
|
+
* previous line's hash. Any insertion, deletion or reordering breaks the chain
|
|
6
|
+
* and is detectable offline by anyone, with no vendor in the loop.
|
|
7
|
+
*
|
|
8
|
+
* The schema is versioned and published as `schema/agent-audit.schema.json` in
|
|
9
|
+
* the Python distribution, so other tools (SIEMs, observability) can ingest it.
|
|
10
|
+
* The hashes computed here are byte-identical to the Python library's for
|
|
11
|
+
* identical content — see `canonical.ts` for how that is guaranteed.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
|
+
import { canonicalBytes, parseJson, pyJsonDumps, toPlain, } from "./canonical.js";
|
|
17
|
+
export const SCHEMA_VERSION = 1;
|
|
18
|
+
export const GENESIS = "0".repeat(64);
|
|
19
|
+
/** `sha256(prev_hash || canonical(event-without-hash))`, as hex. */
|
|
20
|
+
export function hashEntry(prevHash, payload) {
|
|
21
|
+
const h = createHash("sha256");
|
|
22
|
+
h.update(Buffer.from(prevHash, "utf8"));
|
|
23
|
+
h.update(canonicalBytes(payload));
|
|
24
|
+
return h.digest("hex");
|
|
25
|
+
}
|
|
26
|
+
function asNumber(value) {
|
|
27
|
+
const plain = toPlain(value);
|
|
28
|
+
return typeof plain === "number" ? plain : undefined;
|
|
29
|
+
}
|
|
30
|
+
function withoutHash(entry) {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
33
|
+
if (k !== "hash")
|
|
34
|
+
out[k] = v;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Append-only, hash-chained decision log. */
|
|
39
|
+
export class AuditLog {
|
|
40
|
+
path;
|
|
41
|
+
sinks;
|
|
42
|
+
prev = GENESIS;
|
|
43
|
+
seq = 0;
|
|
44
|
+
_entries = [];
|
|
45
|
+
/**
|
|
46
|
+
* Timestamps are injected by the caller (the `Guard` uses a monotonic
|
|
47
|
+
* counter) so the log stays deterministic in tests and reproducible in
|
|
48
|
+
* replay. In production the runtime supplies a trusted timestamp.
|
|
49
|
+
*/
|
|
50
|
+
constructor(init = {}) {
|
|
51
|
+
const opts = typeof init === "string" || init === null ? { path: init } : init;
|
|
52
|
+
this.path = opts.path ?? null;
|
|
53
|
+
this.sinks = opts.sinks ?? [];
|
|
54
|
+
if (this.path) {
|
|
55
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
56
|
+
writeFileSync(this.path, ""); // fresh log
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
append(event, ts, fields = {}) {
|
|
60
|
+
const payload = {
|
|
61
|
+
v: SCHEMA_VERSION,
|
|
62
|
+
seq: this.seq,
|
|
63
|
+
ts,
|
|
64
|
+
event,
|
|
65
|
+
...fields,
|
|
66
|
+
prev_hash: this.prev,
|
|
67
|
+
};
|
|
68
|
+
payload["hash"] = hashEntry(this.prev, payload);
|
|
69
|
+
this.prev = payload["hash"];
|
|
70
|
+
this.seq += 1;
|
|
71
|
+
this._entries.push(payload);
|
|
72
|
+
if (this.path) {
|
|
73
|
+
// Python writes the ledger with `json.dumps(payload, sort_keys=True)` —
|
|
74
|
+
// its DEFAULT separators, not the compact pair the hash is taken over.
|
|
75
|
+
appendFileSync(this.path, pyJsonDumps(payload) + "\n");
|
|
76
|
+
}
|
|
77
|
+
for (const sink of this.sinks)
|
|
78
|
+
sink.write(payload);
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
|
81
|
+
get entries() {
|
|
82
|
+
return this._entries.slice();
|
|
83
|
+
}
|
|
84
|
+
get length() {
|
|
85
|
+
return this._entries.length;
|
|
86
|
+
}
|
|
87
|
+
[Symbol.iterator]() {
|
|
88
|
+
return this.entries[Symbol.iterator]();
|
|
89
|
+
}
|
|
90
|
+
// ---- verification -----------------------------------------------------
|
|
91
|
+
/** `[seq, hash]` of the last entry — the chain head; `[-1, GENESIS]` if empty. */
|
|
92
|
+
head() {
|
|
93
|
+
if (this._entries.length === 0)
|
|
94
|
+
return [-1, GENESIS];
|
|
95
|
+
const last = this._entries[this._entries.length - 1];
|
|
96
|
+
return [asNumber(last["seq"]), last["hash"]];
|
|
97
|
+
}
|
|
98
|
+
chainIdHint() {
|
|
99
|
+
return chainIdOf(this._entries);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A signed external COMMITMENT to the chain head. Publish it out of band; a
|
|
103
|
+
* later `verifyAnchor` then catches a log that was fully rewritten and
|
|
104
|
+
* re-hashed, which plain `verify` cannot — a consistent rewrite reproduces
|
|
105
|
+
* its own hashes. The signed head hash is the fixed point.
|
|
106
|
+
*/
|
|
107
|
+
anchor(signer, ts = 0) {
|
|
108
|
+
const [seq, head] = this.head();
|
|
109
|
+
const body = { v: SCHEMA_VERSION, chain_id: this.chainIdHint(), seq, head, ts };
|
|
110
|
+
return {
|
|
111
|
+
...body,
|
|
112
|
+
kid: signer.kid ?? null,
|
|
113
|
+
sig: signer.sign(canonicalBytes(body)).toString("hex"),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** The chain reproduces AND its head matches a SIGNED anchor. */
|
|
117
|
+
static verifyAnchor(entries, anchor, signer) {
|
|
118
|
+
const a = anchor ?? {};
|
|
119
|
+
const body = {};
|
|
120
|
+
for (const k of ["v", "chain_id", "seq", "head", "ts"])
|
|
121
|
+
body[k] = a[k] ?? null;
|
|
122
|
+
const sigHex = toPlain(a["sig"]) ?? "";
|
|
123
|
+
if (typeof sigHex !== "string" || !/^(?:[0-9a-fA-F]{2})*$/.test(sigHex)) {
|
|
124
|
+
return [false, "anchor signature not hex"];
|
|
125
|
+
}
|
|
126
|
+
const sig = Buffer.from(sigHex, "hex");
|
|
127
|
+
const kid = toPlain(a["kid"]);
|
|
128
|
+
if (!signer.verify(canonicalBytes(body), sig, kid)) {
|
|
129
|
+
return [false, "anchor signature invalid"];
|
|
130
|
+
}
|
|
131
|
+
const [ok, err] = AuditLog.verify(entries);
|
|
132
|
+
if (!ok)
|
|
133
|
+
return [false, err];
|
|
134
|
+
if (entries.length === 0)
|
|
135
|
+
return [asNumber(a["seq"]) === -1, null];
|
|
136
|
+
const last = entries[entries.length - 1];
|
|
137
|
+
if (last["hash"] !== toPlain(a["head"]) || asNumber(last["seq"]) !== asNumber(a["seq"])) {
|
|
138
|
+
return [false, "anchor head does not match the ledger head (ledger rewritten?)"];
|
|
139
|
+
}
|
|
140
|
+
return [true, null];
|
|
141
|
+
}
|
|
142
|
+
/** Recompute the chain. Returns `[ok, firstBadReason]`. */
|
|
143
|
+
static verify(entries) {
|
|
144
|
+
let prev = GENESIS;
|
|
145
|
+
let expectedSeq = 0;
|
|
146
|
+
for (const e of entries) {
|
|
147
|
+
const seq = asNumber(e["seq"]);
|
|
148
|
+
if (seq !== expectedSeq) {
|
|
149
|
+
return [false, `seq gap at ${expectedSeq} (got ${formatSeq(e["seq"])})`];
|
|
150
|
+
}
|
|
151
|
+
const stored = e["hash"];
|
|
152
|
+
const payload = withoutHash(e);
|
|
153
|
+
if (toPlain(payload["prev_hash"]) !== prev) {
|
|
154
|
+
return [false, `prev_hash mismatch at seq ${expectedSeq}`];
|
|
155
|
+
}
|
|
156
|
+
if (hashEntry(prev, payload) !== stored) {
|
|
157
|
+
return [false, `hash mismatch at seq ${expectedSeq}`];
|
|
158
|
+
}
|
|
159
|
+
prev = stored;
|
|
160
|
+
expectedSeq += 1;
|
|
161
|
+
}
|
|
162
|
+
return [true, null];
|
|
163
|
+
}
|
|
164
|
+
/** Read a `.jsonl` ledger, keeping every number's original literal. */
|
|
165
|
+
static load(path) {
|
|
166
|
+
return AuditLog.parseLines(readFileSync(path, "utf8"));
|
|
167
|
+
}
|
|
168
|
+
/** Parse `.jsonl` text into entries. Blank lines are skipped. */
|
|
169
|
+
static parseLines(text) {
|
|
170
|
+
return text
|
|
171
|
+
.split(/\r?\n/)
|
|
172
|
+
.filter((line) => line.trim() !== "")
|
|
173
|
+
.map((line) => parseJson(line));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function formatSeq(value) {
|
|
177
|
+
const plain = toPlain(value);
|
|
178
|
+
return plain === undefined || plain === null ? "None" : String(plain);
|
|
179
|
+
}
|
|
180
|
+
/** The first `chain_id` any entry carries, or `"chain"`. */
|
|
181
|
+
export function chainIdOf(entries) {
|
|
182
|
+
for (const e of entries) {
|
|
183
|
+
const id = toPlain(e["chain_id"]);
|
|
184
|
+
if (typeof id === "string" && id)
|
|
185
|
+
return id;
|
|
186
|
+
}
|
|
187
|
+
return "chain";
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authority — the core value object.
|
|
3
|
+
*
|
|
4
|
+
* An Authority is an immutable capability: a set of scopes plus typed `Ceiling`
|
|
5
|
+
* bounds and a TTL. The single most important operation is `meet`: the greatest
|
|
6
|
+
* authority that is within BOTH a parent's authority and a requested one. A
|
|
7
|
+
* child of a delegation can never hold more than the meet — attenuation is a
|
|
8
|
+
* lattice operation, enforced in code, not a convention.
|
|
9
|
+
*
|
|
10
|
+
* The guarantee everything else rests on:
|
|
11
|
+
*
|
|
12
|
+
* meet(parent, requested) <= parent for ALL requested.
|
|
13
|
+
*
|
|
14
|
+
* There is no code path by which a derived child authority can exceed its
|
|
15
|
+
* parent. That is what makes model-proposed authority safe to accept: the
|
|
16
|
+
* proposal is only ever an input to `meet`, and `meet` can only shrink.
|
|
17
|
+
*
|
|
18
|
+
* `isNarrowerThan` is exactly the wire protocol's subsumption relation
|
|
19
|
+
* (draft-asor-wimse-agent-delegation-chain-00), so a chain that verifies offline
|
|
20
|
+
* is one the library would have permitted, and the reverse.
|
|
21
|
+
*/
|
|
22
|
+
import { type CJson, type Json } from "./canonical.js";
|
|
23
|
+
import { type Ceiling, type Context } from "./ceilings.js";
|
|
24
|
+
import { Decision } from "./reasons.js";
|
|
25
|
+
/**
|
|
26
|
+
* Raised for STRUCTURAL failures — bad input or invalid chain state, such as
|
|
27
|
+
* delegating from a revoked or expired node, or a depth/fanout overflow.
|
|
28
|
+
* Deliberately distinct from a policy denial: a denial is a normal outcome,
|
|
29
|
+
* expressed as a `Decision`. A structural error means the caller did something
|
|
30
|
+
* invalid; a denial means the caller asked for something the authority model
|
|
31
|
+
* legitimately refuses.
|
|
32
|
+
*/
|
|
33
|
+
export declare class AuthorityError extends Error {
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
readonly detail: Record<string, Json>;
|
|
36
|
+
constructor(message: string, reason: string, detail?: Record<string, Json>);
|
|
37
|
+
}
|
|
38
|
+
export interface AuthorityInit {
|
|
39
|
+
scopes?: Iterable<string>;
|
|
40
|
+
ceilings?: Iterable<Ceiling>;
|
|
41
|
+
/** Seconds this authority remains valid from issuance; `null` is unbounded. */
|
|
42
|
+
ttl?: number | null;
|
|
43
|
+
}
|
|
44
|
+
/** The wire form of an Authority. */
|
|
45
|
+
export interface AuthorityWire {
|
|
46
|
+
scopes: string[];
|
|
47
|
+
constraints: Record<string, Json>[];
|
|
48
|
+
ttl: number | null;
|
|
49
|
+
[key: string]: CJson;
|
|
50
|
+
}
|
|
51
|
+
export declare class Authority {
|
|
52
|
+
/**
|
|
53
|
+
* Permission strings, e.g. `crm.read`. Scopes support one level of prefix
|
|
54
|
+
* wildcard: `crm.*` covers `crm.read` and `crm.write`. A child requesting
|
|
55
|
+
* `crm.read` under a parent holding `crm.*` is allowed; the reverse is not.
|
|
56
|
+
*/
|
|
57
|
+
readonly scopes: ReadonlySet<string>;
|
|
58
|
+
/**
|
|
59
|
+
* At most one ceiling per `key` (last one wins), sorted by key for a
|
|
60
|
+
* deterministic wire form and integrity seal. A dimension with no ceiling is
|
|
61
|
+
* unbounded on that dimension unless a parent in the chain bounds it —
|
|
62
|
+
* attenuation can only add or tighten bounds, never remove one.
|
|
63
|
+
*/
|
|
64
|
+
readonly ceilings: readonly Ceiling[];
|
|
65
|
+
/** Seconds from issuance. `null` is unbounded (discouraged). */
|
|
66
|
+
readonly ttl: number | null;
|
|
67
|
+
constructor(init?: AuthorityInit);
|
|
68
|
+
private byKey;
|
|
69
|
+
/** The ceiling bound to `key`, or `undefined`. */
|
|
70
|
+
ceiling(key: string): Ceiling | undefined;
|
|
71
|
+
/** Does a held scope cover a requested scope? Supports one `x.*` wildcard. */
|
|
72
|
+
static scopeCovers(held: string, requested: string): boolean;
|
|
73
|
+
coversScope(requested: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* The greatest authority within BOTH `this` and `other` — the attenuation.
|
|
76
|
+
* This is the only way a child authority is constructed. It is commutative
|
|
77
|
+
* and can only ever shrink relative to either input.
|
|
78
|
+
*/
|
|
79
|
+
meet(other: Authority): Authority;
|
|
80
|
+
/**
|
|
81
|
+
* `this <= other`: is `this` provably no more powerful than `other` in every
|
|
82
|
+
* dimension? True iff:
|
|
83
|
+
*
|
|
84
|
+
* 1. every scope of `this` is covered by `other` (wildcard-aware);
|
|
85
|
+
* 2. for every ceiling in `other` there is a ceiling of the same key in
|
|
86
|
+
* `this` that `other`'s ceiling subsumes. A ceiling present in `other`
|
|
87
|
+
* and ABSENT here means `this` is unbounded on that dimension, i.e. more
|
|
88
|
+
* powerful, so the relation is false. This holds for any ceiling key,
|
|
89
|
+
* including ones outside the built-in registry, which is what makes the
|
|
90
|
+
* relation sound for custom ceilings too;
|
|
91
|
+
* 3. `this.ttl` is not null and (`other.ttl` is null or `this.ttl <= other.ttl`).
|
|
92
|
+
*
|
|
93
|
+
* This is exactly the wire subsumption relation: the library relation and the
|
|
94
|
+
* token relation are the same relation.
|
|
95
|
+
*/
|
|
96
|
+
isNarrowerThan(other: Authority): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Is `scope` permitted under this authority, given a request context such as
|
|
99
|
+
* `{rows: 5000, egress: "none"}`?
|
|
100
|
+
*
|
|
101
|
+
* Checks scope coverage AND every ceiling this authority holds, collecting
|
|
102
|
+
* every failing reason — not just the first — so a single evaluation can
|
|
103
|
+
* explain everything wrong with a request. A ceiling whose context field is
|
|
104
|
+
* absent is not asserting anything on this call and is treated as satisfied.
|
|
105
|
+
*/
|
|
106
|
+
permits(scope: string, ctx?: Context | null): Decision;
|
|
107
|
+
withTtl(ttl: number): Authority;
|
|
108
|
+
toWire(): AuthorityWire;
|
|
109
|
+
static fromWire(wire: CJson): Authority;
|
|
110
|
+
/** A stable, human-readable one-liner. */
|
|
111
|
+
describe(): string;
|
|
112
|
+
toString(): string;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=authority.d.ts.map
|
|
@@ -0,0 +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,EAAgD,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AACzG,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;AAED,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;AAED,qBAAa,SAAS;IACpB;;;;OAIG;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;IAUpC,OAAO,CAAC,KAAK;IAMb,kDAAkD;IAClD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAMzC,8EAA8E;IAC9E,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;IAYvC,0CAA0C;IAC1C,QAAQ,IAAI,MAAM;IAMlB,QAAQ,IAAI,MAAM;CAGnB"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authority — the core value object.
|
|
3
|
+
*
|
|
4
|
+
* An Authority is an immutable capability: a set of scopes plus typed `Ceiling`
|
|
5
|
+
* bounds and a TTL. The single most important operation is `meet`: the greatest
|
|
6
|
+
* authority that is within BOTH a parent's authority and a requested one. A
|
|
7
|
+
* child of a delegation can never hold more than the meet — attenuation is a
|
|
8
|
+
* lattice operation, enforced in code, not a convention.
|
|
9
|
+
*
|
|
10
|
+
* The guarantee everything else rests on:
|
|
11
|
+
*
|
|
12
|
+
* meet(parent, requested) <= parent for ALL requested.
|
|
13
|
+
*
|
|
14
|
+
* There is no code path by which a derived child authority can exceed its
|
|
15
|
+
* parent. That is what makes model-proposed authority safe to accept: the
|
|
16
|
+
* proposal is only ever an input to `meet`, and `meet` can only shrink.
|
|
17
|
+
*
|
|
18
|
+
* `isNarrowerThan` is exactly the wire protocol's subsumption relation
|
|
19
|
+
* (draft-asor-wimse-agent-delegation-chain-00), so a chain that verifies offline
|
|
20
|
+
* is one the library would have permitted, and the reverse.
|
|
21
|
+
*/
|
|
22
|
+
import { compareCodePoints, sortedStrings, toPlain } from "./canonical.js";
|
|
23
|
+
import { ceilingFromWire, describe as describeCeiling } from "./ceilings.js";
|
|
24
|
+
import { Decision, Reason, ReasonCode } from "./reasons.js";
|
|
25
|
+
/**
|
|
26
|
+
* Raised for STRUCTURAL failures — bad input or invalid chain state, such as
|
|
27
|
+
* delegating from a revoked or expired node, or a depth/fanout overflow.
|
|
28
|
+
* Deliberately distinct from a policy denial: a denial is a normal outcome,
|
|
29
|
+
* expressed as a `Decision`. A structural error means the caller did something
|
|
30
|
+
* invalid; a denial means the caller asked for something the authority model
|
|
31
|
+
* legitimately refuses.
|
|
32
|
+
*/
|
|
33
|
+
export class AuthorityError extends Error {
|
|
34
|
+
reason;
|
|
35
|
+
detail;
|
|
36
|
+
constructor(message, reason, detail = {}) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "AuthorityError";
|
|
39
|
+
this.reason = reason;
|
|
40
|
+
this.detail = detail;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class Authority {
|
|
44
|
+
/**
|
|
45
|
+
* Permission strings, e.g. `crm.read`. Scopes support one level of prefix
|
|
46
|
+
* wildcard: `crm.*` covers `crm.read` and `crm.write`. A child requesting
|
|
47
|
+
* `crm.read` under a parent holding `crm.*` is allowed; the reverse is not.
|
|
48
|
+
*/
|
|
49
|
+
scopes;
|
|
50
|
+
/**
|
|
51
|
+
* At most one ceiling per `key` (last one wins), sorted by key for a
|
|
52
|
+
* deterministic wire form and integrity seal. A dimension with no ceiling is
|
|
53
|
+
* unbounded on that dimension unless a parent in the chain bounds it —
|
|
54
|
+
* attenuation can only add or tighten bounds, never remove one.
|
|
55
|
+
*/
|
|
56
|
+
ceilings;
|
|
57
|
+
/** Seconds from issuance. `null` is unbounded (discouraged). */
|
|
58
|
+
ttl;
|
|
59
|
+
constructor(init = {}) {
|
|
60
|
+
this.scopes = new Set(init.scopes ?? []);
|
|
61
|
+
const byKey = new Map();
|
|
62
|
+
for (const c of init.ceilings ?? [])
|
|
63
|
+
byKey.set(String(c.key), c);
|
|
64
|
+
this.ceilings = Array.from(byKey.keys())
|
|
65
|
+
.sort(compareCodePoints)
|
|
66
|
+
.map((k) => byKey.get(k));
|
|
67
|
+
this.ttl = init.ttl ?? null;
|
|
68
|
+
}
|
|
69
|
+
byKey() {
|
|
70
|
+
const m = new Map();
|
|
71
|
+
for (const c of this.ceilings)
|
|
72
|
+
m.set(String(c.key), c);
|
|
73
|
+
return m;
|
|
74
|
+
}
|
|
75
|
+
/** The ceiling bound to `key`, or `undefined`. */
|
|
76
|
+
ceiling(key) {
|
|
77
|
+
return this.byKey().get(key);
|
|
78
|
+
}
|
|
79
|
+
// ---- scope helpers ----------------------------------------------------
|
|
80
|
+
/** Does a held scope cover a requested scope? Supports one `x.*` wildcard. */
|
|
81
|
+
static scopeCovers(held, requested) {
|
|
82
|
+
if (held === requested)
|
|
83
|
+
return true;
|
|
84
|
+
if (held.endsWith(".*"))
|
|
85
|
+
return requested.startsWith(held.slice(0, -1)); // keep the dot
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
coversScope(requested) {
|
|
89
|
+
for (const held of this.scopes) {
|
|
90
|
+
if (Authority.scopeCovers(held, requested))
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// ---- the lattice ------------------------------------------------------
|
|
96
|
+
/**
|
|
97
|
+
* The greatest authority within BOTH `this` and `other` — the attenuation.
|
|
98
|
+
* This is the only way a child authority is constructed. It is commutative
|
|
99
|
+
* and can only ever shrink relative to either input.
|
|
100
|
+
*/
|
|
101
|
+
meet(other) {
|
|
102
|
+
// Scopes: keep a requested scope only if this side covers it, and keep this
|
|
103
|
+
// side's own concrete scopes that the other covers. The net effect is a
|
|
104
|
+
// wildcard-aware intersection, never larger than either side's coverage.
|
|
105
|
+
const merged = new Set();
|
|
106
|
+
for (const s of other.scopes)
|
|
107
|
+
if (this.coversScope(s))
|
|
108
|
+
merged.add(s);
|
|
109
|
+
for (const s of this.scopes)
|
|
110
|
+
if (other.coversScope(s))
|
|
111
|
+
merged.add(s);
|
|
112
|
+
// Remove only REDUNDANT scopes: one covered by a broader wildcard that is
|
|
113
|
+
// also present. This keeps the broadest legitimately-granted authority and
|
|
114
|
+
// only trims duplicates, so a wildcard granted by both sides survives.
|
|
115
|
+
const wildcards = Array.from(merged).filter((s) => s.endsWith(".*"));
|
|
116
|
+
const pruned = new Set(Array.from(merged).filter((s) => !wildcards.some((w) => w !== s && Authority.scopeCovers(w, s))));
|
|
117
|
+
// Ceilings: union of keys. Where BOTH sides bound a key, narrow it; where
|
|
118
|
+
// only one side bounds it, carry that bound through unchanged. A ceiling
|
|
119
|
+
// therefore only ever appears or tightens across a meet, never disappears —
|
|
120
|
+
// exactly the property `isNarrowerThan` checks.
|
|
121
|
+
const mine = this.byKey();
|
|
122
|
+
const theirs = other.byKey();
|
|
123
|
+
const keys = Array.from(new Set([...mine.keys(), ...theirs.keys()])).sort(compareCodePoints);
|
|
124
|
+
const ceilings = [];
|
|
125
|
+
for (const k of keys) {
|
|
126
|
+
const a = mine.get(k);
|
|
127
|
+
const b = theirs.get(k);
|
|
128
|
+
ceilings.push(a !== undefined && b !== undefined ? a.narrow(b) : (a ?? b));
|
|
129
|
+
}
|
|
130
|
+
const ttls = [this.ttl, other.ttl].filter((t) => t !== null);
|
|
131
|
+
const ttl = ttls.length > 0 ? Math.min(...ttls) : null;
|
|
132
|
+
return new Authority({ scopes: pruned, ceilings, ttl });
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* `this <= other`: is `this` provably no more powerful than `other` in every
|
|
136
|
+
* dimension? True iff:
|
|
137
|
+
*
|
|
138
|
+
* 1. every scope of `this` is covered by `other` (wildcard-aware);
|
|
139
|
+
* 2. for every ceiling in `other` there is a ceiling of the same key in
|
|
140
|
+
* `this` that `other`'s ceiling subsumes. A ceiling present in `other`
|
|
141
|
+
* and ABSENT here means `this` is unbounded on that dimension, i.e. more
|
|
142
|
+
* powerful, so the relation is false. This holds for any ceiling key,
|
|
143
|
+
* including ones outside the built-in registry, which is what makes the
|
|
144
|
+
* relation sound for custom ceilings too;
|
|
145
|
+
* 3. `this.ttl` is not null and (`other.ttl` is null or `this.ttl <= other.ttl`).
|
|
146
|
+
*
|
|
147
|
+
* This is exactly the wire subsumption relation: the library relation and the
|
|
148
|
+
* token relation are the same relation.
|
|
149
|
+
*/
|
|
150
|
+
isNarrowerThan(other) {
|
|
151
|
+
for (const s of this.scopes) {
|
|
152
|
+
if (!other.coversScope(s))
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const mine = this.byKey();
|
|
156
|
+
for (const [k, otherCeiling] of other.byKey()) {
|
|
157
|
+
const selfCeiling = mine.get(k);
|
|
158
|
+
if (selfCeiling === undefined)
|
|
159
|
+
return false; // unbounded here where other bounds
|
|
160
|
+
if (!otherCeiling.subsumes(selfCeiling))
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (other.ttl !== null) {
|
|
164
|
+
if (this.ttl === null || this.ttl > other.ttl)
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
// ---- policy evaluation ------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Is `scope` permitted under this authority, given a request context such as
|
|
172
|
+
* `{rows: 5000, egress: "none"}`?
|
|
173
|
+
*
|
|
174
|
+
* Checks scope coverage AND every ceiling this authority holds, collecting
|
|
175
|
+
* every failing reason — not just the first — so a single evaluation can
|
|
176
|
+
* explain everything wrong with a request. A ceiling whose context field is
|
|
177
|
+
* absent is not asserting anything on this call and is treated as satisfied.
|
|
178
|
+
*/
|
|
179
|
+
permits(scope, ctx = null) {
|
|
180
|
+
const context = ctx ?? {};
|
|
181
|
+
const reasons = [];
|
|
182
|
+
if (!this.coversScope(scope)) {
|
|
183
|
+
reasons.push(new Reason(ReasonCode.SCOPE_NOT_GRANTED, {
|
|
184
|
+
requested: scope,
|
|
185
|
+
message: `scope '${scope}' not covered by held scopes [${sortedStrings(this.scopes)
|
|
186
|
+
.map((s) => `'${s}'`)
|
|
187
|
+
.join(", ")}]`,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
// Reserved key so scoped ceilings can tell whether they apply.
|
|
191
|
+
const cctx = { ...context };
|
|
192
|
+
if (!("_scope" in cctx))
|
|
193
|
+
cctx["_scope"] = scope;
|
|
194
|
+
for (const c of this.ceilings) {
|
|
195
|
+
const decision = c.permits(cctx);
|
|
196
|
+
if (!decision.allowed)
|
|
197
|
+
reasons.push(...decision.reasons);
|
|
198
|
+
}
|
|
199
|
+
return reasons.length > 0 ? Decision.deny(reasons) : Decision.allow();
|
|
200
|
+
}
|
|
201
|
+
withTtl(ttl) {
|
|
202
|
+
return new Authority({ scopes: this.scopes, ceilings: this.ceilings, ttl });
|
|
203
|
+
}
|
|
204
|
+
// ---- wire form --------------------------------------------------------
|
|
205
|
+
toWire() {
|
|
206
|
+
return {
|
|
207
|
+
scopes: sortedStrings(this.scopes),
|
|
208
|
+
constraints: this.ceilings.map((c) => c.toWire()),
|
|
209
|
+
ttl: this.ttl,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
static fromWire(wire) {
|
|
213
|
+
const d = toPlain(wire) ?? {};
|
|
214
|
+
const scopes = d["scopes"] ?? [];
|
|
215
|
+
const constraints = d["constraints"] ?? [];
|
|
216
|
+
const ttl = d["ttl"];
|
|
217
|
+
return new Authority({
|
|
218
|
+
scopes,
|
|
219
|
+
ceilings: constraints.map((c) => ceilingFromWire(c)),
|
|
220
|
+
ttl: typeof ttl === "number" ? ttl : null,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/** A stable, human-readable one-liner. */
|
|
224
|
+
describe() {
|
|
225
|
+
const scopes = sortedStrings(this.scopes).join(", ");
|
|
226
|
+
const cs = this.ceilings.map((c) => describeCeiling(c)).sort(compareCodePoints).join(", ");
|
|
227
|
+
return `scopes=[${scopes}] ceilings=[${cs}] ttl=${this.ttl}`;
|
|
228
|
+
}
|
|
229
|
+
toString() {
|
|
230
|
+
return `Authority(${this.describe()})`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=authority.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authority.js","sourceRoot":"","sources":["../../src/authority.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* canonical.ts — byte-exact interoperability with the Python implementation.
|
|
3
|
+
*
|
|
4
|
+
* Every hash, seal and signature in Attenu is taken over a canonical JSON
|
|
5
|
+
* serialisation. The Python library produces those bytes with
|
|
6
|
+
* `json.dumps(obj, sort_keys=True, separators=(",", ":"))`, whose defaults
|
|
7
|
+
* include `ensure_ascii=True`. `JSON.stringify` differs from that in three
|
|
8
|
+
* ways that would each silently break cross-language verification:
|
|
9
|
+
*
|
|
10
|
+
* 1. key order — `JSON.stringify` preserves insertion order, Python sorts;
|
|
11
|
+
* 2. non-ASCII — Python escapes every code point above U+007E as `\uXXXX`
|
|
12
|
+
* (astral characters as a surrogate pair), `JSON.stringify` emits them raw;
|
|
13
|
+
* 3. numbers — Python distinguishes `int` from `float` (`100` vs `100.0`) and
|
|
14
|
+
* switches to exponential notation at different magnitudes than JavaScript
|
|
15
|
+
* (`1e-05` where `String()` gives `0.00001`).
|
|
16
|
+
*
|
|
17
|
+
* (1) and (2) are handled by `canonicalJson` below. (3) cannot be solved by a
|
|
18
|
+
* serialiser alone, because a JavaScript `number` has forgotten whether the
|
|
19
|
+
* document said `100` or `100.0`. So a document that is going to be re-hashed —
|
|
20
|
+
* a ledger or a bundle produced elsewhere — is read with `parseJson`, which
|
|
21
|
+
* keeps each number's original literal alongside its value as a `RawNumber`.
|
|
22
|
+
* `canonicalJson` re-emits that literal verbatim, so a Python-written entry
|
|
23
|
+
* hashes to the byte-identical digest here. Values created in this library are
|
|
24
|
+
* plain numbers, and `pyNumber` formats them the way Python's `json` would, so
|
|
25
|
+
* the reverse direction round-trips too.
|
|
26
|
+
*/
|
|
27
|
+
/** A number as it was written in the source document, plus its parsed value. */
|
|
28
|
+
export declare class RawNumber {
|
|
29
|
+
readonly raw: string;
|
|
30
|
+
readonly value: number;
|
|
31
|
+
constructor(raw: string, value: number);
|
|
32
|
+
valueOf(): number;
|
|
33
|
+
toJSON(): number;
|
|
34
|
+
}
|
|
35
|
+
/** A JSON value that may still carry original number literals. */
|
|
36
|
+
export type CJson = null | boolean | number | string | RawNumber | CJson[] | {
|
|
37
|
+
[key: string]: CJson;
|
|
38
|
+
};
|
|
39
|
+
/** A plain JSON value — what the rest of the library works with. */
|
|
40
|
+
export type Json = null | boolean | number | string | Json[] | {
|
|
41
|
+
[key: string]: Json;
|
|
42
|
+
};
|
|
43
|
+
/** Compare two strings by Unicode code point, as Python's `sorted` does. */
|
|
44
|
+
export declare function compareCodePoints(a: string, b: string): number;
|
|
45
|
+
/** Sort strings by code point (Python's `sorted(list_of_str)`). */
|
|
46
|
+
export declare function sortedStrings(values: Iterable<string>): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Format a number the way Python's `json.dumps` would.
|
|
49
|
+
*
|
|
50
|
+
* Integral values are emitted without a decimal point, matching a Python `int`.
|
|
51
|
+
* A TypeScript number cannot record that it was meant to be the float `100.0`,
|
|
52
|
+
* so such a value serialises as `100`; Python reads that back as an `int` and
|
|
53
|
+
* re-emits `100`, which keeps the chain verifiable in both directions. Values
|
|
54
|
+
* with a fractional part follow CPython's `repr`: shortest round-tripping
|
|
55
|
+
* digits, exponential notation when the decimal exponent is below -4 or at
|
|
56
|
+
* least 16, a two-digit signed exponent, and always a decimal point otherwise.
|
|
57
|
+
*/
|
|
58
|
+
export declare function pyNumber(n: number): string;
|
|
59
|
+
/** Escape a string the way Python's `json.dumps(ensure_ascii=True)` does. */
|
|
60
|
+
export declare function pyString(s: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Serialise to the exact bytes Python's
|
|
63
|
+
* `json.dumps(obj, sort_keys=True, separators=(",", ":"))` produces:
|
|
64
|
+
* sorted keys, no incidental whitespace, ASCII-escaped strings.
|
|
65
|
+
*
|
|
66
|
+
* Object entries whose value is `undefined` are omitted, matching a Python dict
|
|
67
|
+
* that simply never held the key. `null` is kept — it is a value.
|
|
68
|
+
*/
|
|
69
|
+
export declare function canonicalJson(value: CJson | undefined): string;
|
|
70
|
+
/**
|
|
71
|
+
* Serialise with sorted keys and ASCII escaping, choosing the separators.
|
|
72
|
+
*
|
|
73
|
+
* `canonicalJson` uses the compact `(",", ":")` pair every hash is taken over.
|
|
74
|
+
* The `.jsonl` ledger on disk is written with Python's DEFAULT separators
|
|
75
|
+
* (`", "` and `": "`), so a line read back by either language re-serialises to
|
|
76
|
+
* the same bytes its writer produced.
|
|
77
|
+
*/
|
|
78
|
+
export declare function pyJsonDumps(value: CJson | undefined, itemSeparator?: string, keySeparator?: string): string;
|
|
79
|
+
/** The canonical bytes — what every hash, seal and signature is taken over. */
|
|
80
|
+
export declare function canonicalBytes(value: CJson | undefined): Buffer;
|
|
81
|
+
/** Strip `RawNumber` wrappers, yielding ordinary JavaScript values. */
|
|
82
|
+
export declare function toPlain<T = Json>(value: CJson | undefined): T;
|
|
83
|
+
/**
|
|
84
|
+
* Parse JSON, keeping every number's original literal.
|
|
85
|
+
*
|
|
86
|
+
* `JSON.parse` is not used: it discards the literal, and with it the ability to
|
|
87
|
+
* reproduce the bytes the writer hashed. Objects preserve their key order for
|
|
88
|
+
* readability; canonical serialisation sorts them anyway.
|
|
89
|
+
*/
|
|
90
|
+
export declare function parseJson(text: string): CJson;
|
|
91
|
+
//# sourceMappingURL=canonical.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../../src/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,gFAAgF;AAChF,qBAAa,SAAS;IAElB,QAAQ,CAAC,GAAG,EAAE,MAAM;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM;gBADb,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM;IAExB,OAAO,IAAI,MAAM;IAGjB,MAAM,IAAI,MAAM;CAGjB;AAED,kEAAkE;AAClE,MAAM,MAAM,KAAK,GACb,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,GACT,KAAK,EAAE,GACP;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAA;CAAE,CAAC;AAE7B,oEAAoE;AACpE,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEvF,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAU9D;AAED,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAEhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM1C;AA2BD,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAgB1C;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,KAAK,GAAG,SAAS,EACxB,aAAa,SAAO,EACpB,YAAY,SAAO,GAClB,MAAM,CAwBR;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAE/D;AAED,uEAAuE;AACvE,wBAAgB,OAAO,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,CAAC,CAS7D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAO7C"}
|