@int-loop/core 0.1.0 → 0.3.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/dist/der.js ADDED
@@ -0,0 +1,246 @@
1
+ // A minimal DER (ASN.1 Distinguished Encoding Rules) reader and writer: just enough for
2
+ // RFC 3161 timestamp requests and responses, the CMS signatures inside them and the
3
+ // X.509 certificates that sign them. Written here rather than taken from a library so
4
+ // the Apache-2.0 verifier has no dependencies beyond zod (DECISIONS.md, 2026-11-09).
5
+ //
6
+ // Strict: definite lengths only, minimal length encoding, no trailing bytes. Anything
7
+ // else throws, and every caller turns a throw into a failed verification.
8
+ import { concatBytes } from "./hash.js";
9
+ export const TAG = {
10
+ BOOLEAN: 0x01,
11
+ INTEGER: 0x02,
12
+ BIT_STRING: 0x03,
13
+ OCTET_STRING: 0x04,
14
+ NULL: 0x05,
15
+ OID: 0x06,
16
+ UTF8_STRING: 0x0c,
17
+ PRINTABLE_STRING: 0x13,
18
+ IA5_STRING: 0x16,
19
+ UTC_TIME: 0x17,
20
+ GENERALIZED_TIME: 0x18,
21
+ SEQUENCE: 0x30,
22
+ SET: 0x31,
23
+ };
24
+ export const contextTag = (n, constructed = true) => 0x80 | (constructed ? 0x20 : 0) | n;
25
+ /** Reads exactly one element filling `bytes`. */
26
+ export function readDer(bytes) {
27
+ const { element, end } = readOne(bytes, 0);
28
+ if (end !== bytes.length)
29
+ throw new Error("trailing bytes after DER element");
30
+ return element;
31
+ }
32
+ /** The elements inside a constructed element (a SEQUENCE, SET or explicit tag). */
33
+ export function children(el) {
34
+ if ((el.tag & 0x20) === 0)
35
+ throw new Error(`tag 0x${el.tag.toString(16)} is not constructed`);
36
+ const out = [];
37
+ let pos = 0;
38
+ while (pos < el.content.length) {
39
+ const { element, end } = readOne(el.content, pos);
40
+ out.push(element);
41
+ pos = end;
42
+ }
43
+ return out;
44
+ }
45
+ function readOne(bytes, start) {
46
+ let pos = start;
47
+ const tag = bytes[pos++];
48
+ if (tag === undefined)
49
+ throw new Error("DER element truncated");
50
+ if ((tag & 0x1f) === 0x1f)
51
+ throw new Error("high-numbered DER tags are not supported");
52
+ const first = bytes[pos++];
53
+ if (first === undefined)
54
+ throw new Error("DER length truncated");
55
+ let length;
56
+ if (first < 0x80) {
57
+ length = first;
58
+ }
59
+ else {
60
+ const count = first & 0x7f;
61
+ if (count === 0)
62
+ throw new Error("indefinite DER length");
63
+ if (count > 4)
64
+ throw new Error("DER length too large");
65
+ length = 0;
66
+ for (let i = 0; i < count; i++) {
67
+ const b = bytes[pos++];
68
+ if (b === undefined)
69
+ throw new Error("DER length truncated");
70
+ if (i === 0 && b === 0)
71
+ throw new Error("non-minimal DER length");
72
+ length = length * 256 + b;
73
+ }
74
+ if (length < 0x80)
75
+ throw new Error("non-minimal DER length");
76
+ }
77
+ const end = pos + length;
78
+ if (end > bytes.length)
79
+ throw new Error("DER element truncated");
80
+ return { element: { tag, raw: bytes.slice(start, end), content: bytes.slice(pos, end) }, end };
81
+ }
82
+ export function expectTag(el, tag, what) {
83
+ if (el === undefined)
84
+ throw new Error(`${what} is missing`);
85
+ if (el.tag !== tag)
86
+ throw new Error(`${what}: expected tag 0x${tag.toString(16)}, found 0x${el.tag.toString(16)}`);
87
+ return el;
88
+ }
89
+ /** The elements of a SEQUENCE (or SET), checking its tag. */
90
+ export function seq(el, what, tag = TAG.SEQUENCE) {
91
+ return children(expectTag(el, tag, what));
92
+ }
93
+ // ---- Primitive values ---------------------------------------------------------
94
+ export function readOid(el, what = "object identifier") {
95
+ const c = expectTag(el, TAG.OID, what).content;
96
+ if (c.length === 0)
97
+ throw new Error(`${what} is empty`);
98
+ const parts = [];
99
+ let value = 0;
100
+ for (let i = 0; i < c.length; i++) {
101
+ const b = c[i];
102
+ if (value === 0 && b === 0x80)
103
+ throw new Error(`${what}: non-minimal encoding`);
104
+ value = value * 128 + (b & 0x7f);
105
+ if (value > Number.MAX_SAFE_INTEGER)
106
+ throw new Error(`${what}: arc too large`);
107
+ if ((b & 0x80) === 0) {
108
+ parts.push(value);
109
+ value = 0;
110
+ }
111
+ else if (i === c.length - 1) {
112
+ throw new Error(`${what} truncated`);
113
+ }
114
+ }
115
+ const [head, ...rest] = parts;
116
+ const firstArc = head < 40 ? 0 : head < 80 ? 1 : 2;
117
+ return [firstArc, head - firstArc * 40, ...rest].join(".");
118
+ }
119
+ /** An INTEGER's two's-complement bytes, checked for minimal encoding. */
120
+ export function readIntegerBytes(el, what = "integer") {
121
+ const c = expectTag(el, TAG.INTEGER, what).content;
122
+ if (c.length === 0)
123
+ throw new Error(`${what} is empty`);
124
+ if (c.length > 1 && ((c[0] === 0 && (c[1] & 0x80) === 0) || (c[0] === 0xff && (c[1] & 0x80) !== 0))) {
125
+ throw new Error(`${what}: non-minimal encoding`);
126
+ }
127
+ return c;
128
+ }
129
+ /** A small non-negative INTEGER (a version number, a status). */
130
+ export function readSmallInt(el, what = "integer") {
131
+ const c = readIntegerBytes(el, what);
132
+ if (c.length > 4 || (c[0] & 0x80) !== 0)
133
+ throw new Error(`${what} out of range`);
134
+ return c.reduce((n, b) => n * 256 + b, 0);
135
+ }
136
+ export function readBoolean(el, what = "boolean") {
137
+ const c = expectTag(el, TAG.BOOLEAN, what).content;
138
+ if (c.length !== 1 || (c[0] !== 0 && c[0] !== 0xff))
139
+ throw new Error(`${what}: invalid DER boolean`);
140
+ return c[0] === 0xff;
141
+ }
142
+ export function readOctetString(el, what = "octet string") {
143
+ return expectTag(el, TAG.OCTET_STRING, what).content;
144
+ }
145
+ /** A BIT STRING with no unused bits (keys and signatures always are). */
146
+ export function readBitStringBytes(el, what = "bit string") {
147
+ const c = expectTag(el, TAG.BIT_STRING, what).content;
148
+ if (c.length === 0 || c[0] !== 0)
149
+ throw new Error(`${what}: unused bits are not supported here`);
150
+ return c.slice(1);
151
+ }
152
+ /** UTCTime or GeneralizedTime, to milliseconds since the epoch. Only the Z (UTC) forms DER allows. */
153
+ export function readTime(el, what = "time") {
154
+ if (el === undefined)
155
+ throw new Error(`${what} is missing`);
156
+ const text = new TextDecoder("ascii").decode(el.content);
157
+ let m;
158
+ let year;
159
+ if (el.tag === TAG.UTC_TIME) {
160
+ m = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(text);
161
+ if (!m)
162
+ throw new Error(`${what}: invalid UTCTime`);
163
+ const yy = Number(m[1]);
164
+ year = yy >= 50 ? 1900 + yy : 2000 + yy;
165
+ }
166
+ else if (el.tag === TAG.GENERALIZED_TIME) {
167
+ // RFC 3161 allows fractional seconds, with no trailing zeros.
168
+ m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.(\d*[1-9]))?Z$/.exec(text);
169
+ if (!m)
170
+ throw new Error(`${what}: invalid GeneralizedTime`);
171
+ year = Number(m[1]);
172
+ }
173
+ else {
174
+ throw new Error(`${what}: expected a UTCTime or GeneralizedTime`);
175
+ }
176
+ const fraction = el.tag === TAG.GENERALIZED_TIME && m[7] ? Number(`0.${m[7]}`) : 0;
177
+ const ms = Date.UTC(year, Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6])) + Math.floor(fraction * 1000);
178
+ const check = new Date(ms);
179
+ if (check.getUTCMonth() !== Number(m[2]) - 1 || check.getUTCDate() !== Number(m[3]) || Number(m[4]) > 23 || Number(m[5]) > 59 || Number(m[6]) > 59) {
180
+ throw new Error(`${what}: not a real date and time`);
181
+ }
182
+ return ms;
183
+ }
184
+ // ---- Writing ------------------------------------------------------------------
185
+ function encodeLength(length) {
186
+ if (length < 0x80)
187
+ return Uint8Array.of(length);
188
+ const bytes = [];
189
+ for (let n = length; n > 0; n = Math.floor(n / 256))
190
+ bytes.unshift(n & 0xff);
191
+ return Uint8Array.of(0x80 | bytes.length, ...bytes);
192
+ }
193
+ export function encode(tag, ...content) {
194
+ const body = concatBytes(...content);
195
+ return concatBytes(Uint8Array.of(tag), encodeLength(body.length), body);
196
+ }
197
+ export const encodeSequence = (...items) => encode(TAG.SEQUENCE, ...items);
198
+ export const encodeNull = () => encode(TAG.NULL);
199
+ export const encodeBoolean = (value) => encode(TAG.BOOLEAN, Uint8Array.of(value ? 0xff : 0));
200
+ export const encodeOctetString = (bytes) => encode(TAG.OCTET_STRING, bytes);
201
+ /** A non-negative INTEGER from big-endian unsigned bytes, minimally encoded. */
202
+ export function encodeUnsignedInteger(bytes) {
203
+ let start = 0;
204
+ while (start < bytes.length - 1 && bytes[start] === 0)
205
+ start++;
206
+ const trimmed = bytes.length === 0 ? Uint8Array.of(0) : bytes.subarray(start);
207
+ return encode(TAG.INTEGER, (trimmed[0] & 0x80) !== 0 ? concatBytes(Uint8Array.of(0), trimmed) : trimmed);
208
+ }
209
+ export function encodeSmallInt(value) {
210
+ if (!Number.isSafeInteger(value) || value < 0)
211
+ throw new Error("expected a non-negative safe integer");
212
+ const bytes = [];
213
+ for (let n = value; n > 0; n = Math.floor(n / 256))
214
+ bytes.unshift(n & 0xff);
215
+ return encodeUnsignedInteger(Uint8Array.from(bytes.length ? bytes : [0]));
216
+ }
217
+ export function encodeOid(oid) {
218
+ const arcs = oid.split(".").map((a) => {
219
+ if (!/^(0|[1-9]\d*)$/.test(a))
220
+ throw new Error(`invalid OID ${oid}`);
221
+ return Number(a);
222
+ });
223
+ if (arcs.length < 2 || arcs[0] > 2 || (arcs[0] < 2 && arcs[1] >= 40))
224
+ throw new Error(`invalid OID ${oid}`);
225
+ const out = [];
226
+ for (const arc of [arcs[0] * 40 + arcs[1], ...arcs.slice(2)]) {
227
+ const chunk = [arc & 0x7f];
228
+ for (let n = Math.floor(arc / 128); n > 0; n = Math.floor(n / 128))
229
+ chunk.unshift(0x80 | (n & 0x7f));
230
+ out.push(...chunk);
231
+ }
232
+ return encode(TAG.OID, Uint8Array.from(out));
233
+ }
234
+ /** Decodes the base64 body of every PEM block of the given label (default CERTIFICATE). */
235
+ export function fromPem(text, label = "CERTIFICATE") {
236
+ const out = [];
237
+ const re = new RegExp(`-----BEGIN ${label}-----([\\s\\S]*?)-----END ${label}-----`, "g");
238
+ for (const m of text.matchAll(re)) {
239
+ const b64 = m[1].replace(/\s+/g, "");
240
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64))
241
+ throw new Error("invalid PEM base64");
242
+ const bin = atob(b64);
243
+ out.push(Uint8Array.from(bin, (ch) => ch.charCodeAt(0)));
244
+ }
245
+ return out;
246
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { type AnchorReport } from "./anchor.ts";
2
3
  import { LedgerEntry } from "./schema/ledger.ts";
3
4
  import { type VerificationFailure } from "./verify.ts";
4
5
  export declare const EVIDENCE_PACK_FORMAT = "int-loop-evidence-pack";
@@ -33,7 +34,7 @@ export declare const PackKeys: z.ZodObject<{
33
34
  export type PackKeys = z.infer<typeof PackKeys>;
34
35
  /** The hashes of free text a ledger entry references, which the pack should carry. */
35
36
  export declare function referencedTextHashes(entry: LedgerEntry): string[];
36
- export declare const PACK_README = "# int-loop evidence pack\n\nThis archive is a complete, signed record of decisions made through int-loop for one\ntenant. You can check it without trusting int-loop.\n\nRun:\n\n npx @int-loop/verify pack.zip\n\nor, to confirm the service keys are really int-loop's, download\nhttps://int-loop.com/.well-known/int-loop-keys.json and run:\n\n npx @int-loop/verify pack.zip --keys int-loop-keys.json\n\nThe verifier checks that:\n\n1. Every ledger entry links to the one before it (a SHA-256 hash chain from a zero genesis hash).\n2. Every entry carries a valid Ed25519 signature from an int-loop service key.\n3. Every passkey decision (L2 and above) carries a WebAuthn assertion from a passkey enrolled\n earlier in this ledger, by the person the decision names, over a challenge that commits to\n exactly what was displayed, the option chosen, and any reason, conditions or question typed.\n4. Every text in texts.jsonl matches the hash the ledger records for it.\n\nTexts marked \"erased\" were removed under the tenant's retention policy. Their hashes stay in\nthe ledger, so the chain still verifies.\n\nThe verifier is open source (Apache-2.0). This pack is evidence for your own assessment. It is\nnot legal advice and does not on its own show that any regulation is met.\n";
37
+ export declare const PACK_README = "# int-loop evidence pack\n\nThis archive is a complete, signed record of decisions made through int-loop for one\ntenant. You can check it without trusting int-loop.\n\nRun:\n\n npx @int-loop/verify pack.zip\n\nor, to confirm the service keys are really int-loop's, download\nhttps://int-loop.com/.well-known/int-loop-keys.json and run:\n\n npx @int-loop/verify pack.zip --keys int-loop-keys.json\n\nThe verifier checks that:\n\n1. Every ledger entry links to the one before it (a SHA-256 hash chain from a zero genesis hash).\n2. Every entry carries a valid Ed25519 signature from an int-loop service key.\n3. Every passkey decision (L2 and above) carries a WebAuthn assertion from a passkey enrolled\n earlier in this ledger, by the person the decision names, over a challenge that commits to\n exactly what was displayed, the option chosen, and any reason, conditions or question typed.\n4. Every text in texts.jsonl matches the hash the ledger records for it.\n5. Every anchor (an anchor.created entry) holds RFC 3161 timestamps from independent\n timestamp authorities, over a Merkle root that includes the anchored entry. Each\n timestamp must verify against a trusted timestamp authority root. This shows the\n ledger up to that entry existed, unchanged, no later than the stamped time.\n The verifier trusts a short built-in list of roots; pass --tsa-roots roots.pem to\n use your own.\n\nTexts marked \"erased\" were removed under the tenant's retention policy. Their hashes stay in\nthe ledger, so the chain still verifies.\n\nThe verifier is open source (Apache-2.0). This pack is evidence for your own assessment. It is\nnot legal advice and does not on its own show that any regulation is met.\n";
37
38
  export declare function crc32(data: Uint8Array): number;
38
39
  export declare function buildZip(files: {
39
40
  name: string;
@@ -49,11 +50,19 @@ export type PackReport = {
49
50
  texts_checked: number;
50
51
  texts_erased: number;
51
52
  keys_source: "pack" | "trusted";
53
+ anchors_checked: number;
54
+ /** The newest anchored entry, and the earliest time an independent TSA vouches for it. */
55
+ anchored_through: AnchorReport["anchored_through"];
56
+ tsa_roots_source: "default" | "supplied";
52
57
  failures: VerificationFailure[];
53
58
  };
59
+ export type PackVerifyOptions = {
60
+ /** Trusted timestamp authority roots, as DER certificates. Default: KNOWN_TSA_ROOTS. */
61
+ tsaRoots?: readonly Uint8Array[];
62
+ };
54
63
  /**
55
64
  * Verifies an evidence pack. With `trustedKeys` (from int-loop's published key list),
56
65
  * the pack's own keys.json must agree with them; without, the report says the keys came
57
66
  * only from the pack.
58
67
  */
59
- export declare function verifyEvidencePack(zip: Uint8Array, trustedKeys?: PackKeys): Promise<PackReport>;
68
+ export declare function verifyEvidencePack(zip: Uint8Array, trustedKeys?: PackKeys, options?: PackVerifyOptions): Promise<PackReport>;
@@ -10,9 +10,11 @@
10
10
  // The zip is stored, not compressed, with fixed timestamps: the same records always
11
11
  // produce the same bytes, and the reader needs no decompressor.
12
12
  import { z } from "zod";
13
+ import { verifyAnchors } from "./anchor.js";
13
14
  import { bytesEqual, fromBase64Url, sha256Hex, utf8 } from "./hash.js";
14
15
  import { Base64Url, Sha256Hex, TenantId, Timestamp } from "./schema/common.js";
15
16
  import { LedgerEntry, ServiceKeyId } from "./schema/ledger.js";
17
+ import { knownTsaRootsDer } from "./tsa-roots.js";
16
18
  import { verifyLedger } from "./verify.js";
17
19
  export const EVIDENCE_PACK_FORMAT = "int-loop-evidence-pack";
18
20
  export const PackManifest = z.strictObject({
@@ -75,6 +77,12 @@ The verifier checks that:
75
77
  earlier in this ledger, by the person the decision names, over a challenge that commits to
76
78
  exactly what was displayed, the option chosen, and any reason, conditions or question typed.
77
79
  4. Every text in texts.jsonl matches the hash the ledger records for it.
80
+ 5. Every anchor (an anchor.created entry) holds RFC 3161 timestamps from independent
81
+ timestamp authorities, over a Merkle root that includes the anchored entry. Each
82
+ timestamp must verify against a trusted timestamp authority root. This shows the
83
+ ledger up to that entry existed, unchanged, no later than the stamped time.
84
+ The verifier trusts a short built-in list of roots; pass --tsa-roots roots.pem to
85
+ use your own.
78
86
 
79
87
  Texts marked "erased" were removed under the tenant's retention policy. Their hashes stay in
80
88
  the ledger, so the chain still verifies.
@@ -201,7 +209,7 @@ export function readZip(zip) {
201
209
  * the pack's own keys.json must agree with them; without, the report says the keys came
202
210
  * only from the pack.
203
211
  */
204
- export async function verifyEvidencePack(zip, trustedKeys) {
212
+ export async function verifyEvidencePack(zip, trustedKeys, options = {}) {
205
213
  const failures = [];
206
214
  const report = (extra) => ({
207
215
  ok: false,
@@ -211,6 +219,9 @@ export async function verifyEvidencePack(zip, trustedKeys) {
211
219
  texts_checked: 0,
212
220
  texts_erased: 0,
213
221
  keys_source: trustedKeys ? "trusted" : "pack",
222
+ anchors_checked: 0,
223
+ anchored_through: null,
224
+ tsa_roots_source: options.tsaRoots ? "supplied" : "default",
214
225
  failures,
215
226
  ...extra,
216
227
  });
@@ -329,5 +340,16 @@ export async function verifyEvidencePack(zip, trustedKeys) {
329
340
  if (!present.has(h))
330
341
  fail("texts", `entry ${e.seq} references text ${h}, which is not in the pack`, e.seq);
331
342
  }
332
- return report({ ok: failures.length === 0, manifest, entries_checked: ledger.entries_checked, decisions_checked: ledger.decisions_checked, texts_checked, texts_erased });
343
+ const anchors = await verifyAnchors(parsed, options.tsaRoots ?? knownTsaRootsDer());
344
+ failures.push(...anchors.failures);
345
+ return report({
346
+ ok: failures.length === 0,
347
+ manifest,
348
+ entries_checked: ledger.entries_checked,
349
+ decisions_checked: ledger.decisions_checked,
350
+ texts_checked,
351
+ texts_erased,
352
+ anchors_checked: anchors.anchors_checked,
353
+ anchored_through: anchors.anchored_through,
354
+ });
333
355
  }
package/dist/index.d.ts CHANGED
@@ -2,11 +2,19 @@ export * from "./jcs.ts";
2
2
  export * from "./hash.ts";
3
3
  export * from "./cbor.ts";
4
4
  export * from "./binding.ts";
5
+ export * from "./comprehension.ts";
5
6
  export * from "./webauthn.ts";
6
7
  export * from "./ledger.ts";
7
8
  export * from "./verify.ts";
8
9
  export * from "./evidence-pack.ts";
9
10
  export * from "./receipt.ts";
11
+ export * from "./der.ts";
12
+ export * from "./x509.ts";
13
+ export * from "./tsp.ts";
14
+ export * from "./tsa-roots.ts";
15
+ export * from "./anchor.ts";
16
+ export * from "./controls.ts";
17
+ export * from "./report.ts";
10
18
  export * from "./schema/common.ts";
11
19
  export * from "./schema/request.ts";
12
20
  export * from "./schema/directory.ts";
package/dist/index.js CHANGED
@@ -2,11 +2,19 @@ export * from "./jcs.js";
2
2
  export * from "./hash.js";
3
3
  export * from "./cbor.js";
4
4
  export * from "./binding.js";
5
+ export * from "./comprehension.js";
5
6
  export * from "./webauthn.js";
6
7
  export * from "./ledger.js";
7
8
  export * from "./verify.js";
8
9
  export * from "./evidence-pack.js";
9
10
  export * from "./receipt.js";
11
+ export * from "./der.js";
12
+ export * from "./x509.js";
13
+ export * from "./tsp.js";
14
+ export * from "./tsa-roots.js";
15
+ export * from "./anchor.js";
16
+ export * from "./controls.js";
17
+ export * from "./report.js";
10
18
  export * from "./schema/common.js";
11
19
  export * from "./schema/request.js";
12
20
  export * from "./schema/directory.js";
@@ -0,0 +1,92 @@
1
+ import { type AssuranceLevel } from "./schema/common.ts";
2
+ import type { LedgerEntry } from "./schema/ledger.ts";
3
+ import { Pack, type Obligation } from "./schema/pack.ts";
4
+ export declare const REPORT_FORMAT = "int-loop.oversight-report";
5
+ export type ReportInput = {
6
+ /** The tenant's ledger from entry 1, in order. Entries after `throughSeq` are ignored. */
7
+ entries: readonly LedgerEntry[];
8
+ /** Texts by hash, as the evidence pack holds them; null for an erased text. */
9
+ texts: ReadonlyMap<string, string | null>;
10
+ /** The pack to report against, as `id@version`. It must have been activated in the ledger. */
11
+ packRef: string;
12
+ /** Inclusive UTC dates, YYYY-MM-DD. */
13
+ period: {
14
+ from: string;
15
+ to: string;
16
+ };
17
+ generatedAt: string;
18
+ /** The last entry the report considers. Default: the last entry given. */
19
+ throughSeq?: number;
20
+ };
21
+ export type ControlEvidence = {
22
+ id: string;
23
+ name: string;
24
+ status: "available" | "not_yet_available";
25
+ evidence: string[];
26
+ };
27
+ export type ObligationSection = Pick<Obligation, "id" | "citation" | "summary" | "applies_from" | "coverage" | "coverage_note"> & {
28
+ controls: ControlEvidence[];
29
+ };
30
+ export type ReportGap = {
31
+ kind: string;
32
+ message: string;
33
+ seqs: number[];
34
+ };
35
+ export type OversightReport = {
36
+ format: typeof REPORT_FORMAT;
37
+ format_version: 1;
38
+ scope_statement: string[];
39
+ tenant_id: string;
40
+ generated_at: string;
41
+ period: {
42
+ from: string;
43
+ to: string;
44
+ };
45
+ pack: Pick<Pack, "id" | "version" | "title" | "jurisdiction" | "status" | "review" | "sources"> & {
46
+ ref: string;
47
+ hash: string;
48
+ };
49
+ ledger: {
50
+ through_seq: number;
51
+ through_hash: string;
52
+ period_first_seq: number | null;
53
+ period_last_seq: number | null;
54
+ /** The first anchor recorded after the period that covers its last entry, as recorded. int-loop-verify checks the tokens. */
55
+ anchor: {
56
+ anchor_seq: number;
57
+ anchored_seq: number;
58
+ tsas: string[];
59
+ gen_times: string[];
60
+ } | null;
61
+ };
62
+ policy_hashes: string[];
63
+ summary: {
64
+ requests_in_scope: number;
65
+ decisions_in_scope: number;
66
+ distinct_approvers: number;
67
+ outcomes: Record<string, number>;
68
+ assurance: Record<AssuranceLevel, number>;
69
+ assurance_floor: AssuranceLevel | null;
70
+ decisions_at_or_above_floor: number;
71
+ training_floor: string[];
72
+ training_on_decision_day: {
73
+ met: number;
74
+ not_met: number;
75
+ unknown: number;
76
+ };
77
+ median_time_spent_ms: number | null;
78
+ };
79
+ obligations: {
80
+ in_force: ObligationSection[];
81
+ not_yet_in_force: ObligationSection[];
82
+ date_to_confirm: ObligationSection[];
83
+ };
84
+ gaps: ReportGap[];
85
+ out_of_scope: string[];
86
+ notes: string[];
87
+ };
88
+ export declare class ReportError extends Error {
89
+ }
90
+ export declare function buildOversightReport(input: ReportInput): Promise<OversightReport>;
91
+ /** Plain text, for the terminal and for reading. The JSON is the record. */
92
+ export declare function renderReportText(r: OversightReport): string;