@zanii/blackbox 0.22.0 → 0.23.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.
Files changed (87) hide show
  1. package/README.md +1 -0
  2. package/dist/analysis/accepted.d.ts +26 -0
  3. package/dist/analysis/accepted.js +61 -0
  4. package/dist/analysis/detectors.js +12 -9
  5. package/dist/analysis/faults.js +71 -0
  6. package/dist/analysis/grounding.js +5 -9
  7. package/dist/analysis/guard.d.ts +72 -0
  8. package/dist/analysis/guard.js +240 -0
  9. package/dist/analysis/hallucination.js +2 -2
  10. package/dist/analysis/index.d.ts +21 -1
  11. package/dist/analysis/index.js +101 -19
  12. package/dist/analysis/injection.d.ts +2 -1
  13. package/dist/analysis/injection.js +16 -16
  14. package/dist/analysis/landing.js +60 -14
  15. package/dist/analysis/plan-checks.d.ts +55 -0
  16. package/dist/analysis/plan-checks.js +748 -0
  17. package/dist/analysis/taxonomy.js +1 -0
  18. package/dist/analysis/waste.js +2 -3
  19. package/dist/approvals/index.d.ts +12 -3
  20. package/dist/approvals/index.js +36 -10
  21. package/dist/authority/index.js +3 -3
  22. package/dist/aviation/index.d.ts +422 -0
  23. package/dist/aviation/index.js +1014 -0
  24. package/dist/aviation/qrh.d.ts +4 -0
  25. package/dist/aviation/qrh.js +171 -0
  26. package/dist/budgets/index.d.ts +63 -0
  27. package/dist/budgets/index.js +161 -0
  28. package/dist/cli.js +172 -24
  29. package/dist/compliance/index.d.ts +2 -0
  30. package/dist/compliance/index.js +26 -2
  31. package/dist/cost/batch.d.ts +26 -0
  32. package/dist/cost/batch.js +173 -0
  33. package/dist/cost/index.d.ts +21 -0
  34. package/dist/cost/index.js +115 -30
  35. package/dist/cv/index.js +1 -1
  36. package/dist/duty/index.js +2 -1
  37. package/dist/evals/index.d.ts +233 -0
  38. package/dist/evals/index.js +488 -0
  39. package/dist/federation/index.d.ts +9 -0
  40. package/dist/federation/index.js +8 -1
  41. package/dist/finance/index.js +3 -4
  42. package/dist/fleet/index.d.ts +25 -0
  43. package/dist/fleet/index.js +57 -17
  44. package/dist/fleet/insights.js +4 -1
  45. package/dist/health/index.js +2 -2
  46. package/dist/hooks/ai-sdk.d.ts +17 -19
  47. package/dist/hooks/ai-sdk.js +13 -7
  48. package/dist/hooks/index.d.ts +1 -0
  49. package/dist/hooks/index.js +1 -0
  50. package/dist/hooks/mastra.d.ts +26 -0
  51. package/dist/hooks/mastra.js +63 -0
  52. package/dist/index.d.ts +22 -9
  53. package/dist/index.js +23 -9
  54. package/dist/mcp-requests/index.d.ts +26 -0
  55. package/dist/mcp-requests/index.js +154 -0
  56. package/dist/mcp-server/index.d.ts +12 -0
  57. package/dist/mcp-server/index.js +84 -2
  58. package/dist/mcp-wrap/index.js +8 -4
  59. package/dist/otlp/index.d.ts +155 -0
  60. package/dist/otlp/index.js +468 -0
  61. package/dist/otlp/protobuf.d.ts +7 -0
  62. package/dist/otlp/protobuf.js +314 -0
  63. package/dist/packs/index.d.ts +2 -0
  64. package/dist/packs/index.js +5 -1
  65. package/dist/pdf/arabic.d.ts +45 -0
  66. package/dist/pdf/arabic.js +373 -0
  67. package/dist/pdf/index.d.ts +77 -0
  68. package/dist/pdf/index.js +673 -0
  69. package/dist/policy/index.d.ts +0 -4
  70. package/dist/policy/index.js +13 -5
  71. package/dist/realtime/index.d.ts +33 -0
  72. package/dist/realtime/index.js +204 -0
  73. package/dist/reconcile/record.d.ts +12 -0
  74. package/dist/reconcile/record.js +114 -65
  75. package/dist/session/index.d.ts +43 -0
  76. package/dist/session/index.js +83 -0
  77. package/dist/undo/index.js +3 -3
  78. package/dist/verify/anchor.d.ts +5 -1
  79. package/dist/verify/anchor.js +13 -4
  80. package/dist/verify/envelope.d.ts +1 -1
  81. package/dist/verify/envelope.js +5 -0
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/dist/walls/index.js +2 -2
  85. package/dist/warehouse/index.d.ts +62 -0
  86. package/dist/warehouse/index.js +142 -0
  87. package/package.json +6 -3
@@ -0,0 +1,673 @@
1
+ // Signed PDF/A-3b reports (spec/pdf.md): a report's Markdown laid out as text in an embedded font,
2
+ // its JSON (and any other copy) attached as associated files, and a PAdES baseline B signature by
3
+ // an Ed25519 identity. Pure and deterministic: the same input, key and time give the same bytes in
4
+ // both SDKs (spec/vectors/report-pdf.json). Mirrors sdks/python/src/zanii_blackbox/pdf.py.
5
+ //
6
+ // Latin is drawn in WinAnsi with the DejaVu Sans subset; Arabic is shaped to its presentation forms
7
+ // and put in visual order (./arabic.ts), and drawn with the Noto Sans Arabic subset (spec/pdf.md §6).
8
+ import { createHash, createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
9
+ import { didFromPublicKey } from "@zanii/core";
10
+ import { bidiLevels, prepare, reorderLine, shapeClusters, } from "./arabic.js";
11
+ const enc = new TextEncoder();
12
+ const latin1 = (s) => Uint8Array.from(s, (c) => c.charCodeAt(0));
13
+ const cat = (parts) => new Uint8Array(Buffer.concat(parts));
14
+ const sha = (alg, b) => new Uint8Array(createHash(alg).update(b).digest());
15
+ // ---------------------------------------------------------------- text
16
+ /** cp1252's 0x80-0x9F, by code point. */
17
+ const CP1252 = {
18
+ 8364: 0x80,
19
+ 8218: 0x82,
20
+ 402: 0x83,
21
+ 8222: 0x84,
22
+ 8230: 0x85,
23
+ 8224: 0x86,
24
+ 8225: 0x87,
25
+ 710: 0x88,
26
+ 8240: 0x89,
27
+ 352: 0x8a,
28
+ 8249: 0x8b,
29
+ 338: 0x8c,
30
+ 381: 0x8e,
31
+ 8216: 0x91,
32
+ 8217: 0x92,
33
+ 8220: 0x93,
34
+ 8221: 0x94,
35
+ 8226: 0x95,
36
+ 8211: 0x96,
37
+ 8212: 0x97,
38
+ 732: 0x98,
39
+ 8482: 0x99,
40
+ 353: 0x9a,
41
+ 8250: 0x9b,
42
+ 339: 0x9c,
43
+ 382: 0x9e,
44
+ 376: 0x9f,
45
+ };
46
+ /** Whether a code point is in WinAnsi (tab counts, drawn as a space). */
47
+ const inWinAnsi = (cp) => cp === 9 || (cp >= 0x20 && cp <= 0x7e) || (cp >= 0xa0 && cp <= 0xff) || cp in CP1252;
48
+ /** One code point in WinAnsi; anything else is "?" (the attachment keeps the exact text). */
49
+ function winAnsi(cp) {
50
+ if (cp === 9)
51
+ return 32;
52
+ if ((cp >= 0x20 && cp <= 0x7e) || (cp >= 0xa0 && cp <= 0xff))
53
+ return cp;
54
+ return CP1252[cp] ?? 0x3f;
55
+ }
56
+ const codes = (s) => Array.from(s, (c) => winAnsi(c.codePointAt(0)));
57
+ const SIZES = { h1: 150, h2: 125, h3: 110, body: 90 }; // tenths of a point
58
+ const PAGE_W = 5950;
59
+ const PAGE_H = 8420;
60
+ const MARGIN = 560;
61
+ const WIDTH = PAGE_W - 2 * MARGIN;
62
+ const MARK_GAP = 40; // font units between stacked marks (§6.4)
63
+ function context(assets, lang) {
64
+ const a = assets.arabic;
65
+ if (lang === "ar" && !a)
66
+ throw new Error("lang ar needs the Arabic assets");
67
+ return {
68
+ m: assets.metrics,
69
+ P: a ? prepare(a.unicode) : null,
70
+ arabic: new Map((a?.metrics.glyphs ?? []).map((g) => [g[0], g.slice(1)])),
71
+ marks: new Map((a?.metrics.marks ?? []).map((g) => [g[0], g[1]])),
72
+ p: lang === "ar" ? 1 : 0,
73
+ used: new Map(),
74
+ };
75
+ }
76
+ function glyph(cp, x) {
77
+ if (!inWinAnsi(cp)) {
78
+ const a = x.arabic.get(cp);
79
+ if (a) {
80
+ const [gid, w, y0, y1] = a;
81
+ return { f: 2, code: gid, w, y0, y1, mark: x.marks.get(cp) ?? null, actual: null };
82
+ }
83
+ }
84
+ const code = winAnsi(cp);
85
+ const w = x.m.widths[code - x.m.first_char] ?? 0;
86
+ return { f: 1, code, w, y0: 0, y1: 0, mark: null, actual: null };
87
+ }
88
+ /** The Markdown as lines, wrapped to the page in logical order, then each put in visual order
89
+ * (integer arithmetic only). */
90
+ function layout(markdown, x) {
91
+ const out = [];
92
+ for (const raw of markdown.split("\n")) {
93
+ let line = raw.trimEnd();
94
+ if (/^<\/?div\b/.test(line) || /^\|[-|: ]+\|$/.test(line))
95
+ continue;
96
+ let size = SIZES.body;
97
+ const h = /^(#{1,3}) (.*)$/.exec(line);
98
+ if (h) {
99
+ size = [SIZES.h1, SIZES.h2, SIZES.h3][h[1].length - 1];
100
+ line = h[2];
101
+ }
102
+ else if (/^\|.*\|$/.test(line)) {
103
+ // §6.3: each cell is isolated, as in an HTML table cell
104
+ const [a, b] = x.P ? [x.p ? "⁧" : "⁦", "⁩"] : ["", ""];
105
+ line = line
106
+ .slice(1, -1)
107
+ .split("|")
108
+ .map((c) => a + c.trim() + b)
109
+ .join(" ");
110
+ }
111
+ else if (line.startsWith("> "))
112
+ line = line.slice(2);
113
+ line = line.replaceAll("**", "").replaceAll("`", "");
114
+ if (line === "") {
115
+ out.push({ size: 0, vis: [], idx: [], odd: [], inOrder: true });
116
+ continue;
117
+ }
118
+ const clusters = x.P
119
+ ? shapeClusters(line, x.P)
120
+ : Array.from(line, (c) => ({ cps: [c.codePointAt(0)], src: c, cls: "L" }));
121
+ const levels = x.P ? bidiLevels(clusters, x.p, x.P) : clusters.map(() => 0);
122
+ const cw = clusters.map((c) => c.cps.reduce((n, cp) => {
123
+ const g = glyph(cp, x);
124
+ return n + (g.mark === null ? g.w : 0);
125
+ }, 0));
126
+ const has = (cs) => cs.some((i) => clusters[i].cps.length > 0);
127
+ const fits = (cs) => cs.reduce((n, i) => n + cw[i], 0) * size <= WIDTH * 1000;
128
+ const words = [[]];
129
+ const seps = [];
130
+ clusters.forEach((c, i) => {
131
+ if (c.src === " ") {
132
+ seps.push(i);
133
+ words.push([]);
134
+ }
135
+ else
136
+ words.at(-1).push(i);
137
+ });
138
+ const lines = [];
139
+ let cur = [];
140
+ words.forEach((word, w) => {
141
+ const next = has(cur) ? [...cur, seps[w - 1], ...word] : [...cur, ...word];
142
+ if (fits(next)) {
143
+ cur = next;
144
+ return;
145
+ }
146
+ if (has(cur))
147
+ lines.push(cur);
148
+ cur = [];
149
+ for (const c of word) {
150
+ if (has(cur) && !fits([...cur, c])) {
151
+ lines.push(cur);
152
+ cur = [];
153
+ }
154
+ cur.push(c);
155
+ }
156
+ });
157
+ lines.push(cur);
158
+ for (const l of lines) {
159
+ const vis = x.P
160
+ ? reorderLine(l, clusters, levels, x.p, x.P)
161
+ : l.map((index) => ({ index, cps: clusters[index].cps, odd: false }));
162
+ out.push({
163
+ size,
164
+ vis: vis.map((c) => c.cps.map((cp, k) => {
165
+ const g = glyph(cp, x);
166
+ const was = clusters[c.index].cps[k];
167
+ return cp === was ? g : { ...g, actual: was };
168
+ })),
169
+ idx: vis.map((c) => c.index),
170
+ odd: vis.map((c) => c.odd),
171
+ inOrder: vis.every((c, k) => k === 0 || c.index > vis[k - 1].index),
172
+ });
173
+ }
174
+ }
175
+ return out;
176
+ }
177
+ const pt = (v) => `${Math.floor(v / 10)}.${v % 10}`;
178
+ /** A length in 1/10000 pt. */
179
+ const pt4 = (v) => {
180
+ const a = Math.abs(v);
181
+ return `${v < 0 ? "-" : ""}${Math.floor(a / 10000)}.${String(a % 10000).padStart(4, "0")}`;
182
+ };
183
+ const pdfString = (cs) => `(${cs.map((c) => (c === 0x28 || c === 0x29 || c === 0x5c ? `\\${String.fromCharCode(c)}` : String.fromCharCode(c))).join("")})`;
184
+ const utf16 = (s) => Array.from({ length: s.length }, (_, i) => s.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0")).join("");
185
+ const mirrorOf = (g) => g.actual !== null;
186
+ /** §6.4: one line drawn glyph by glyph (runs of one font, marks centered on their base). */
187
+ function drawLine(line, y, x) {
188
+ const { size, vis } = line;
189
+ const total = vis.reduce((n, c) => n + c.reduce((k, g) => k + (g.mark === null ? g.w : 0), 0), 0);
190
+ if (!vis.some((c) => c.length))
191
+ return "";
192
+ let at = x.p ? (PAGE_W - MARGIN) * 1000 - size * total : MARGIN * 1000;
193
+ const y0 = y * 1000;
194
+ // Each run (one font, one direction) is placed by its own matrix, in visual order inside the run,
195
+ // and the runs are written in logical order (§6.4).
196
+ const ops = [];
197
+ let run = [];
198
+ let runX = 0;
199
+ let runKey = 0;
200
+ let runOdd = false;
201
+ const str = (f, gs) => f === 1
202
+ ? pdfString(gs.map((g) => g.code))
203
+ : `<${gs.map((g) => g.code.toString(16).padStart(4, "0")).join("")}>`;
204
+ const emit = (gs, gx, gy, key) => {
205
+ const f = gs[0].f;
206
+ for (const g of gs)
207
+ if (f === 2)
208
+ x.used.set(g.code, g.w);
209
+ const tj = `1 0 0 1 ${pt4(gx)} ${pt4(gy)} Tm ${str(f, gs)} Tj`;
210
+ const a = gs[0].actual;
211
+ // a mirrored bracket copies as the character it stands for
212
+ ops.push({
213
+ key,
214
+ f,
215
+ s: a === null
216
+ ? tj
217
+ : `/Span << /ActualText <FEFF${utf16(String.fromCodePoint(a))}> >> BDC ${tj} EMC`,
218
+ });
219
+ };
220
+ // the marks on the run's letters, written after it so that a run isn't split
221
+ let marks = [];
222
+ const flush = () => {
223
+ if (run.length)
224
+ emit(run, runX, y0, runKey);
225
+ for (const [g, gx, gy, index] of marks)
226
+ emit([g], gx, gy, run.length ? runKey : index);
227
+ run = [];
228
+ marks = [];
229
+ };
230
+ vis.forEach((cluster, k) => {
231
+ const index = line.idx[k];
232
+ const odd = line.odd[k];
233
+ const base = cluster.find((g) => g.mark === null);
234
+ const bx = at;
235
+ let top = base ? base.y1 : 0;
236
+ let bottom = base ? base.y0 : 0;
237
+ for (const g of cluster) {
238
+ if (g.mark === null) {
239
+ if (run.length &&
240
+ (run[0].f !== g.f ||
241
+ runOdd !== odd ||
242
+ mirrorOf(g) ||
243
+ mirrorOf(run[0])))
244
+ flush();
245
+ if (!run.length) {
246
+ runX = at;
247
+ runKey = index;
248
+ runOdd = odd;
249
+ }
250
+ runKey = Math.min(runKey, index);
251
+ run.push(g);
252
+ at += size * g.w;
253
+ continue;
254
+ }
255
+ let rise;
256
+ if (g.y0 + g.y1 >= 0) {
257
+ rise = Math.max(0, top + MARK_GAP - g.y0);
258
+ top = g.y1 + rise;
259
+ }
260
+ else {
261
+ rise = Math.min(0, bottom - MARK_GAP - g.y1);
262
+ bottom = g.y0 + rise;
263
+ }
264
+ marks.push([
265
+ g,
266
+ bx + Math.floor((size * (base ? base.w : 0)) / 2) - size * g.mark,
267
+ y0 + size * rise,
268
+ index,
269
+ ]);
270
+ }
271
+ });
272
+ flush();
273
+ // a stable sort: the marks stay after the run that holds their letters
274
+ ops.sort((a, b) => a.key - b.key);
275
+ let font = 0;
276
+ const body = ops.map((o) => {
277
+ const tf = o.f === font ? "" : `/F${o.f} ${pt(size)} Tf `;
278
+ font = o.f;
279
+ return tf + o.s;
280
+ });
281
+ return `BT ${body.join(" ")} ET\n`;
282
+ }
283
+ /** Page content streams (latin1 strings). */
284
+ function pages(markdown, x) {
285
+ const out = [];
286
+ let page = "";
287
+ let y = PAGE_H - MARGIN;
288
+ let top = true;
289
+ for (const line of layout(markdown, x)) {
290
+ const { size } = line;
291
+ const lead = size === 0 ? 60 : Math.floor((size * 14) / 10);
292
+ const before = size > SIZES.body && !top ? 60 : 0;
293
+ if (y - before - lead < MARGIN) {
294
+ out.push(page);
295
+ page = "";
296
+ y = PAGE_H - MARGIN;
297
+ top = true;
298
+ }
299
+ if (size === 0) {
300
+ if (!top)
301
+ y -= lead;
302
+ continue;
303
+ }
304
+ y -= (top ? 0 : before) + lead;
305
+ top = false;
306
+ // a left-to-right line in WinAnsi alone is drawn as one string, as in v1
307
+ if (x.p === 0 && line.inOrder && line.vis.every((c) => c.every((g) => g.f === 1)))
308
+ page += `BT /F1 ${pt(size)} Tf ${pt(MARGIN)} ${pt(y)} Td ${pdfString(line.vis.flat().map((g) => g.code))} Tj ET\n`;
309
+ else
310
+ page += drawLine(line, y, x);
311
+ }
312
+ out.push(page);
313
+ return out;
314
+ }
315
+ // ---------------------------------------------------------------- DER
316
+ const len = (n) => {
317
+ if (n < 0x80)
318
+ return [n];
319
+ const b = [];
320
+ for (let v = n; v > 0; v = Math.floor(v / 256))
321
+ b.unshift(v % 256);
322
+ return [0x80 | b.length, ...b];
323
+ };
324
+ const tlv = (tag, ...parts) => {
325
+ const body = cat(parts);
326
+ return cat([Uint8Array.from([tag, ...len(body.length)]), body]);
327
+ };
328
+ const seq = (...p) => tlv(0x30, ...p);
329
+ const set = (...p) => tlv(0x31, ...p);
330
+ function oid(s) {
331
+ const [a, b, ...rest] = s.split(".").map(Number);
332
+ const out = [a * 40 + b];
333
+ for (const n of rest) {
334
+ const g = [n & 0x7f];
335
+ for (let v = Math.floor(n / 128); v > 0; v = Math.floor(v / 128))
336
+ g.unshift(0x80 | (v & 0x7f));
337
+ out.push(...g);
338
+ }
339
+ return tlv(0x06, Uint8Array.from(out));
340
+ }
341
+ const OID = {
342
+ data: "1.2.840.113549.1.7.1",
343
+ signedData: "1.2.840.113549.1.7.2",
344
+ contentType: "1.2.840.113549.1.9.3",
345
+ messageDigest: "1.2.840.113549.1.9.4",
346
+ signingCertV2: "1.2.840.113549.1.9.16.2.47",
347
+ sha256: "2.16.840.1.101.3.4.2.1",
348
+ sha512: "2.16.840.1.101.3.4.2.3",
349
+ ed25519: "1.3.101.112",
350
+ cn: "2.5.4.3",
351
+ };
352
+ /** One DER element at `at` (constructed ones parsed to their children). */
353
+ function der(buf, at = 0, depth = 0) {
354
+ if (depth > 20 || at + 2 > buf.length)
355
+ throw new Error("der: bad input");
356
+ const tag = buf[at];
357
+ let n = buf[at + 1];
358
+ let p = at + 2;
359
+ if (n & 0x80) {
360
+ const k = n & 0x7f;
361
+ if (k < 1 || k > 4)
362
+ throw new Error("der: bad length");
363
+ n = 0;
364
+ for (let i = 0; i < k; i++)
365
+ n = n * 256 + buf[p++];
366
+ }
367
+ if (p + n > buf.length)
368
+ throw new Error("der: truncated");
369
+ const value = buf.subarray(p, p + n);
370
+ const children = [];
371
+ if (tag & 0x20)
372
+ for (let q = 0; q < value.length;) {
373
+ const c = der(value, q, depth + 1);
374
+ children.push(c);
375
+ q += c.raw.length;
376
+ }
377
+ return { tag, raw: buf.subarray(at, p + n), value, children };
378
+ }
379
+ function oidOf(n) {
380
+ if (n?.tag !== 0x06)
381
+ return "";
382
+ const v = n.value;
383
+ const parts = [Math.floor(v[0] / 40), v[0] % 40];
384
+ let x = 0;
385
+ for (const b of v.subarray(1)) {
386
+ x = x * 128 + (b & 0x7f);
387
+ if (!(b & 0x80)) {
388
+ parts.push(x);
389
+ x = 0;
390
+ }
391
+ }
392
+ return parts.join(".");
393
+ }
394
+ // ---------------------------------------------------------------- the signature
395
+ const PKCS8 = Buffer.from("302e020100300506032b657004220420", "hex");
396
+ const SPKI = Buffer.from("302a300506032b6570032100", "hex");
397
+ const privateKey = (seed) => createPrivateKey({
398
+ key: Buffer.concat([PKCS8, Buffer.from(seed)]),
399
+ format: "der",
400
+ type: "pkcs8",
401
+ });
402
+ const ed25519 = () => seq(oid(OID.ed25519));
403
+ /** spec/pdf.md §3: the identity's self-signed X.509 certificate (v1, CN = its did:key). */
404
+ export function pdfCertificate(seed) {
405
+ const key = privateKey(seed);
406
+ const pub = new Uint8Array(Buffer.from(createPublicKey(key).export({ format: "jwk" }).x, "base64url"));
407
+ const name = seq(set(seq(oid(OID.cn), tlv(0x0c, enc.encode(didFromPublicKey(pub))))));
408
+ const serial = sha("sha256", pub).slice(0, 8);
409
+ serial[0] = (serial[0] & 0x7f) | 0x40;
410
+ const tbs = seq(tlv(0x02, serial), ed25519(), name, seq(tlv(0x17, latin1("000101000000Z")), tlv(0x18, latin1("99991231235959Z"))), name, seq(ed25519(), tlv(0x03, Uint8Array.of(0), pub)));
411
+ const sig = new Uint8Array(sign(null, tbs, key));
412
+ return seq(tbs, ed25519(), tlv(0x03, Uint8Array.of(0), sig));
413
+ }
414
+ /** CMS SignedData, detached, PAdES baseline B (spec/pdf.md §3). */
415
+ function cms(content, seed) {
416
+ const cert = pdfCertificate(seed);
417
+ const tbs = der(der(cert).children[0]?.raw);
418
+ const issuer = tbs.children[2]?.raw;
419
+ const serial = tbs.children[0]?.raw;
420
+ const attrs = [
421
+ seq(oid(OID.contentType), set(oid(OID.data))),
422
+ seq(oid(OID.messageDigest), set(tlv(0x04, sha("sha512", content)))),
423
+ seq(oid(OID.signingCertV2), set(seq(seq(seq(tlv(0x04, sha("sha256", cert))))))),
424
+ ].sort((a, b) => Buffer.compare(a, b));
425
+ const sig = new Uint8Array(sign(null, set(...attrs), privateKey(seed)));
426
+ const digestAlg = seq(oid(OID.sha512));
427
+ const signer = seq(tlv(0x02, Uint8Array.of(1)), seq(issuer, serial), digestAlg, tlv(0xa0, ...attrs), ed25519(), tlv(0x04, sig));
428
+ return seq(oid(OID.signedData), tlv(0xa0, seq(tlv(0x02, Uint8Array.of(1)), set(digestAlg), seq(oid(OID.data)), tlv(0xa0, cert), set(signer))));
429
+ }
430
+ // ---------------------------------------------------------------- the file
431
+ const SIG_BYTES = 4096;
432
+ const ISO = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.\d+)?Z$/;
433
+ const NAME = /^[A-Za-z0-9._-]{1,64}$/;
434
+ const xml = (s) => s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
435
+ const ascii = (s) => pdfString(codes(s));
436
+ /** spec/pdf.md: the report as a signed PDF/A-3b file. `seed` is the signer's Ed25519 seed. */
437
+ export function reportPdf(input, seed, assets) {
438
+ const t = ISO.exec(input.at);
439
+ if (!t)
440
+ throw new Error("at must be ISO-8601 UTC with Z");
441
+ const [, Y, Mo, D, H, Mi, S] = t;
442
+ const pdfDate = `D:${Y}${Mo}${D}${H}${Mi}${S}Z`;
443
+ const xmpDate = `${Y}-${Mo}-${D}T${H}:${Mi}:${S}Z`;
444
+ const files = [...input.attachments].sort((a, b) => (a.name < b.name ? -1 : 1));
445
+ for (const f of files)
446
+ if (!NAME.test(f.name) || !/^[a-z]+\/[a-z0-9.+-]+$/.test(f.mime))
447
+ throw new Error(`bad attachment ${f.name}`);
448
+ const m = assets.metrics;
449
+ const lang = input.lang ?? "en";
450
+ if (lang !== "en" && lang !== "ar")
451
+ throw new Error("lang must be en or ar");
452
+ const ctx = context(assets, lang);
453
+ const contents = pages(input.markdown, ctx);
454
+ const f2 = ctx.used.size ? 11 + 2 * files.length + 2 * contents.length : 0;
455
+ const FONT = `ZBBXPA+${m.name}`;
456
+ const fileObj = (i) => 11 + 2 * i;
457
+ const pageObj = (i) => 11 + 2 * files.length + 2 * i;
458
+ const objs = [];
459
+ const dict = (s) => latin1(s);
460
+ const stream = (head, body) => cat([
461
+ latin1(`<< ${head}${head ? " " : ""}/Length ${body.length} >>\nstream\n`),
462
+ body,
463
+ latin1("\nendstream"),
464
+ ]);
465
+ const afs = files.map((_, i) => `${fileObj(i)} 0 R`).join(" ");
466
+ objs[1] = dict(`<< /Type /Catalog /Pages 2 0 R /Metadata 3 0 R /OutputIntents [4 0 R] /AcroForm << /Fields [10 0 R] /SigFlags 3 >>${files.length ? ` /Names << /EmbeddedFiles << /Names [${files.map((f, i) => `(${f.name}) ${fileObj(i)} 0 R`).join(" ")}] >> >> /AF [${afs}]` : ""} /Lang (${lang}) >>`);
467
+ objs[2] = dict(`<< /Type /Pages /Kids [${contents.map((_, i) => `${pageObj(i)} 0 R`).join(" ")}] /Count ${contents.length} >>`);
468
+ const xmp = enc.encode(`<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>\n<x:xmpmeta xmlns:x="adobe:ns:meta/">\n<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n<rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">\n<pdfaid:part>3</pdfaid:part>\n<pdfaid:conformance>B</pdfaid:conformance>\n<dc:format>application/pdf</dc:format>\n<dc:title><rdf:Alt><rdf:li xml:lang="x-default">${xml(input.title)}</rdf:li></rdf:Alt></dc:title>\n<xmp:CreateDate>${xmpDate}</xmp:CreateDate>\n<xmp:ModifyDate>${xmpDate}</xmp:ModifyDate>\n<xmp:CreatorTool>Zanii Blackbox</xmp:CreatorTool>\n<pdf:Producer>zanii-blackbox pdf v1</pdf:Producer>\n</rdf:Description>\n</rdf:RDF>\n</x:xmpmeta>\n<?xpacket end="w"?>`);
469
+ objs[3] = stream("/Type /Metadata /Subtype /XML", xmp);
470
+ objs[4] = dict("<< /Type /OutputIntent /S /GTS_PDFA1 /OutputConditionIdentifier (Gray gamma 2.2) /Info (Gray gamma 2.2) /DestOutputProfile 5 0 R >>");
471
+ objs[5] = stream("/N 1", assets.icc);
472
+ objs[6] = dict(`<< /Type /Font /Subtype /TrueType /BaseFont /${FONT} /FirstChar ${m.first_char} /LastChar ${m.first_char + m.widths.length - 1} /Widths [${m.widths.join(" ")}] /Encoding /WinAnsiEncoding /FontDescriptor 7 0 R >>`);
473
+ objs[7] = dict(`<< /Type /FontDescriptor /FontName /${FONT} /Flags ${m.flags} /FontBBox [${m.bbox.join(" ")}] /ItalicAngle ${m.italic_angle} /Ascent ${m.ascent} /Descent ${m.descent} /CapHeight ${m.cap_height} /StemV ${m.stem_v} /FontFile2 8 0 R >>`);
474
+ objs[8] = stream(`/Length1 ${assets.font.length}`, assets.font);
475
+ const BR = "/ByteRange [0 ********** ********** **********]";
476
+ objs[9] = dict(`<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached ${BR} /Contents <${"0".repeat(SIG_BYTES * 2)}> /M (${pdfDate}) >>`);
477
+ objs[10] = dict(`<< /Type /Annot /Subtype /Widget /FT /Sig /T (Signature1) /V 9 0 R /F 132 /Rect [0 0 0 0] /P ${pageObj(0)} 0 R >>`);
478
+ files.forEach((f, i) => {
479
+ objs[fileObj(i)] = dict(`<< /Type /Filespec /F (${f.name}) /UF (${f.name}) /AFRelationship /${f.relationship} /Desc ${ascii(f.description)} /EF << /F ${fileObj(i) + 1} 0 R /UF ${fileObj(i) + 1} 0 R >> >>`);
480
+ objs[fileObj(i) + 1] = stream(`/Type /EmbeddedFile /Subtype /${f.mime.replace("/", "#2F")} /Params << /ModDate (${pdfDate}) /Size ${f.bytes.length} >>`, f.bytes);
481
+ });
482
+ contents.forEach((c, i) => {
483
+ objs[pageObj(i)] = dict(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 6 0 R${f2 ? ` /F2 ${f2} 0 R` : ""} >> >> /Contents ${pageObj(i) + 1} 0 R${i === 0 ? " /Annots [10 0 R]" : ""} >>`);
484
+ objs[pageObj(i) + 1] = stream("", latin1(c));
485
+ });
486
+ if (f2)
487
+ arabicFont(objs, f2, assets.arabic, ctx.used, stream);
488
+ const parts = [latin1("%PDF-1.7\n%\xe2\xe3\xcf\xd3\n")];
489
+ let at = parts[0]?.length;
490
+ const offsets = [];
491
+ for (let n = 1; n < objs.length; n++) {
492
+ offsets[n] = at;
493
+ const o = cat([latin1(`${n} 0 obj\n`), objs[n], latin1("\nendobj\n")]);
494
+ parts.push(o);
495
+ at += o.length;
496
+ }
497
+ const id = Buffer.from(sha("sha256", enc.encode(`${input.title}\n${input.at}\n${input.markdown}`)).slice(0, 16)).toString("hex");
498
+ parts.push(latin1(`xref\n0 ${objs.length}\n0000000000 65535 f \n${offsets
499
+ .slice(1)
500
+ .map((o) => `${String(o).padStart(10, "0")} 00000 n \n`)
501
+ .join("")}trailer\n<< /Size ${objs.length} /Root 1 0 R /ID [<${id}> <${id}>] >>\nstartxref\n${at}\n%%EOF\n`));
502
+ const pdf = cat(parts);
503
+ // §3: the signature covers every byte but its own /Contents
504
+ const text = Buffer.from(pdf).toString("latin1");
505
+ const a = text.indexOf("/Contents <", offsets[9]) + 10;
506
+ const b = a + SIG_BYTES * 2 + 2;
507
+ const range = `[0 ${a} ${b} ${pdf.length - b}]`.padEnd(BR.length - 11, " ");
508
+ pdf.set(latin1(range), text.indexOf(BR, offsets[9]) + 11);
509
+ const signed = cms(cat([pdf.subarray(0, a), pdf.subarray(b)]), seed);
510
+ if (signed.length > SIG_BYTES)
511
+ throw new Error("signature too large");
512
+ pdf.set(latin1(Buffer.from(signed).toString("hex")), a + 1);
513
+ return pdf;
514
+ }
515
+ /** §6.5: the Arabic subset as a Type0 font (Identity-H, glyph ids as CIDs), with the widths and
516
+ * the ToUnicode entries of the glyphs drawn. Objects n to n + 4. */
517
+ function arabicFont(objs, n, a, used, stream) {
518
+ const am = a.metrics;
519
+ const name = `ZBBXPB+${am.name}`;
520
+ const gids = [...used.keys()].sort((x, y) => x - y);
521
+ const text = new Map(am.to_unicode.map(([g, t]) => [g, t.replaceAll(" ", "")]));
522
+ objs[n] = latin1(`<< /Type /Font /Subtype /Type0 /BaseFont /${name} /Encoding /Identity-H /DescendantFonts [${n + 1} 0 R] /ToUnicode ${n + 4} 0 R >>`);
523
+ objs[n + 1] = latin1(`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${name} /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ${n + 2} 0 R /W [${gids.map((g) => `${g} [${used.get(g)}]`).join(" ")}] /CIDToGIDMap /Identity >>`);
524
+ objs[n + 2] = latin1(`<< /Type /FontDescriptor /FontName /${name} /Flags ${am.flags} /FontBBox [${am.bbox.join(" ")}] /ItalicAngle ${am.italic_angle} /Ascent ${am.ascent} /Descent ${am.descent} /CapHeight ${am.cap_height} /StemV ${am.stem_v} /FontFile2 ${n + 3} 0 R >>`);
525
+ objs[n + 3] = stream(`/Length1 ${a.font.length}`, a.font);
526
+ const hex4 = (g) => g.toString(16).toUpperCase().padStart(4, "0");
527
+ let chars = "";
528
+ for (let i = 0; i < gids.length; i += 100) {
529
+ const part = gids.slice(i, i + 100).filter((g) => text.has(g));
530
+ chars += `${part.length} beginbfchar\n${part.map((g) => `<${hex4(g)}> <${text.get(g)}>\n`).join("")}endbfchar\n`;
531
+ }
532
+ objs[n + 4] = stream("", latin1(`/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n${chars}endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend`));
533
+ }
534
+ /** spec/pdf.md §4: checks the signature (the ByteRange digest and the CMS signature, against the
535
+ * certificate inside) and the PDF/A-3b structure as far as it can be checked offline. */
536
+ export function verifyPdf(pdf) {
537
+ const text = Buffer.from(pdf).toString("latin1");
538
+ const problems = [];
539
+ const pdfa = [];
540
+ let signer = null;
541
+ const br = [...text.matchAll(/\/ByteRange \[\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*\]/g)].at(-1);
542
+ const m = [...text.matchAll(/\/M \(D:(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)Z\)/g)].at(-1);
543
+ const signedAt = m ? `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z` : null;
544
+ if (!br)
545
+ problems.push("no signature");
546
+ else {
547
+ const [r0, r1, r2, r3] = br.slice(1, 5).map(Number);
548
+ if (r0 !== 0 || r1 >= r2 || r2 + r3 !== pdf.length)
549
+ problems.push("the signature does not cover the whole file");
550
+ else if (text[r1] !== "<" || text[r2 - 1] !== ">")
551
+ problems.push("bad ByteRange");
552
+ else
553
+ try {
554
+ signer = checkCms(Buffer.from(text.slice(r1 + 1, r2 - 1), "hex"), cat([pdf.subarray(0, r1), pdf.subarray(r2, r2 + r3)]), problems);
555
+ }
556
+ catch {
557
+ problems.push("the signature is not CMS");
558
+ }
559
+ }
560
+ // PDF/A-3b, the parts checkable without veraPDF
561
+ if (!text.startsWith("%PDF-1.") || !/^%PDF-1\.\d\r?\n%[\x80-\xff]{4}/.test(text))
562
+ pdfa.push("header");
563
+ if (!/<pdfaid:part>3<\/pdfaid:part>/.test(text) || !/<pdfaid:conformance>B</.test(text))
564
+ pdfa.push("XMP pdfaid");
565
+ if (!/\/OutputIntents \[/.test(text) || !/\/S \/GTS_PDFA1/.test(text))
566
+ pdfa.push("OutputIntent");
567
+ if (/\/Encrypt\b|\/JavaScript\b|\/JS\b/.test(text))
568
+ pdfa.push("forbidden key");
569
+ if (!/\/ID \[<[0-9a-f]+> <[0-9a-f]+>\]/.test(text))
570
+ pdfa.push("trailer ID");
571
+ if (!text.endsWith("%%EOF\n"))
572
+ pdfa.push("EOF");
573
+ const fonts = text.match(/\/Type \/FontDescriptor\b/g)?.length ?? 0;
574
+ if (fonts === 0 || (text.match(/\/FontFile2 \d+ 0 R/g)?.length ?? 0) < fonts)
575
+ pdfa.push("fonts embedded");
576
+ const xref = objects(text, pdfa);
577
+ const attachments = [];
578
+ for (const spec of text.matchAll(/\/Type \/Filespec \/F \(([^)]*)\) \/UF \([^)]*\) \/AFRelationship \/(\w+) .*?\/EF << \/F (\d+) 0 R/g)) {
579
+ const at = xref.get(Number(spec[3]));
580
+ const s = at === undefined ? null : streamAt(pdf, text, at);
581
+ if (!s || !/\/Type \/EmbeddedFile/.test(s.head) || !/\/ModDate/.test(s.head)) {
582
+ pdfa.push(`embedded file ${spec[1]}`);
583
+ continue;
584
+ }
585
+ attachments.push({
586
+ name: spec[1],
587
+ mime: (/\/Subtype \/([\w#.+-]+)/.exec(s.head)?.[1] ?? "").replace("#2F", "/"),
588
+ relationship: spec[2],
589
+ sha256: Buffer.from(sha("sha256", s.bytes)).toString("hex"),
590
+ size: s.bytes.length,
591
+ });
592
+ }
593
+ if ((text.match(/\/Type \/Filespec/g)?.length ?? 0) !== attachments.length)
594
+ pdfa.push("AFRelationship");
595
+ return {
596
+ ok: problems.length === 0 && pdfa.length === 0,
597
+ signer,
598
+ signed_at: signedAt,
599
+ pdfa: { ok: pdfa.length === 0, problems: pdfa },
600
+ attachments,
601
+ problems,
602
+ };
603
+ }
604
+ /** The xref table: object number to offset, each checked to start that object. */
605
+ function objects(text, pdfa) {
606
+ const out = new Map();
607
+ const sx = /startxref\n(\d+)\n%%EOF\n$/.exec(text);
608
+ const x = sx ? Number(sx[1]) : -1;
609
+ const head = /^xref\n0 (\d+)\n/.exec(text.slice(x, x + 30));
610
+ if (!head) {
611
+ pdfa.push("xref");
612
+ return out;
613
+ }
614
+ const n = Number(head[1]);
615
+ const start = x + head[0].length;
616
+ for (let i = 1; i < n; i++) {
617
+ const off = Number(text.slice(start + 20 * i, start + 20 * i + 10));
618
+ if (!text.startsWith(`${i} 0 obj\n`, off)) {
619
+ pdfa.push("xref");
620
+ return out;
621
+ }
622
+ out.set(i, off);
623
+ }
624
+ return out;
625
+ }
626
+ function streamAt(pdf, text, at) {
627
+ const s = text.indexOf("stream\n", at);
628
+ const head = text.slice(at, s);
629
+ const n = /\/Length (\d+) >>\n$/.exec(head);
630
+ if (s < 0 || !n)
631
+ return null;
632
+ return { head, bytes: pdf.subarray(s + 7, s + 7 + Number(n[1])) };
633
+ }
634
+ /** The CMS checks of spec/pdf.md §4; the signer's did:key, or null with a problem added. */
635
+ function checkCms(sig, content, problems) {
636
+ const ci = der(sig);
637
+ if (oidOf(ci.children[0]) !== OID.signedData)
638
+ throw new Error("not signed data");
639
+ const sd = ci.children[1]?.children[0];
640
+ const [, , encap, certs, infos] = sd.children;
641
+ if ((encap?.children.length ?? 0) !== 1)
642
+ problems.push("the signature is not detached");
643
+ const cert = certs?.children[0];
644
+ const si = infos?.children[0];
645
+ const [, , digestAlg, attrs, sigAlg, value] = si.children;
646
+ const alg = oidOf(digestAlg?.children[0]);
647
+ const hashName = alg === OID.sha512 ? "sha512" : alg === OID.sha256 ? "sha256" : null;
648
+ if (!hashName || !attrs || attrs.tag !== 0xa0)
649
+ throw new Error("bad signer info");
650
+ const attr = (o) => attrs.children.find((a) => oidOf(a.children[0]) === o)?.children[1];
651
+ if (oidOf(attr(OID.contentType)?.children[0]) !== OID.data)
652
+ problems.push("content type");
653
+ const md = attr(OID.messageDigest)?.children[0]?.value;
654
+ if (!md || Buffer.compare(md, sha(hashName, content)) !== 0)
655
+ problems.push("the file was changed after signing");
656
+ const certHash = attr(OID.signingCertV2)?.children[0]?.children[0]?.children[0]?.children.at(-1);
657
+ if (!certHash || Buffer.compare(certHash.value, sha("sha256", cert.raw)) !== 0)
658
+ problems.push("signing-certificate-v2");
659
+ const tbs = cert.children[0];
660
+ const spki = tbs.children[tbs.children[0]?.tag === 0xa0 ? 6 : 5];
661
+ const pub = spki.children[1].value.subarray(1);
662
+ if (oidOf(sigAlg?.children[0]) !== OID.ed25519 || pub.length !== 32) {
663
+ problems.push("not an Ed25519 signature");
664
+ return null;
665
+ }
666
+ const key = createPublicKey({ key: Buffer.concat([SPKI, pub]), format: "der", type: "spki" });
667
+ const signedAttrs = cat([Uint8Array.of(0x31), attrs.raw.subarray(1)]);
668
+ if (!verify(null, signedAttrs, key, value.value)) {
669
+ problems.push("bad signature");
670
+ return null;
671
+ }
672
+ return didFromPublicKey(new Uint8Array(pub));
673
+ }
@@ -76,9 +76,5 @@ export declare function audited(compiled: readonly Compiled[], names: readonly s
76
76
  rule: string;
77
77
  would: Rule["action"];
78
78
  }>;
79
- /**
80
- * spec/policy.md §2 as findings: POLICY_DENIED for MCP calls the gateway refused (tool.call meta.policy),
81
- * POLICY_VIOLATION for tools a model response asked for that a deny rule matches.
82
- */
83
79
  export declare function policyFindings(lines: readonly string[], bodies: Bodies, compiled: CompiledPolicy): Finding[];
84
80
  export {};