@zanii/blackbox 0.13.0 → 0.15.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/README.md CHANGED
@@ -83,6 +83,18 @@ verifyAnswerCredential(credential, lines, bodies, packs); // { ok, problems }: t
83
83
 
84
84
  The same checks run offline as pure functions: `toolHallucinations`, `groundingFindings`, `factsIn`, `loadReferencePacks`, `answerRisk`, `detectorAccuracy`.
85
85
 
86
+ ## New in 0.15.0
87
+
88
+ - M-of-N shares: `splitSecret` / `combineShares`, and `blackbox key-split --m 3 --n 5` / `blackbox key-combine`, so no single custodian holds the gateway's key passphrase.
89
+ - `otlpFromProtobuf(bytes)`: OTLP/HTTP protobuf spans, read without a protobuf library (gateway v0.17 takes them on `/v1/traces`).
90
+ - Works with gateway v0.17: the passphrase from Google Cloud KMS or Azure Key Vault, retention per tenant, closed-session summaries kept, the trace exported while a session runs.
91
+
92
+ ## New in 0.14.0
93
+
94
+ - Price uplifts (`uplifts` in a price table): US-only inference, regional endpoints and fast modes priced above list, per deployment (`upliftPercent`).
95
+ - Statements in any currency: the `fx:<CUR>:<rate>` unit, `fxRate(currency)` (the ECB's rate through Frankfurter), and `blackbox statement --currency EUR` with the rate, date and source in the statement.
96
+ - Tax invoices for zero-rated and reverse-charge customers (`vat_treatment`).
97
+
86
98
  ## New in 0.13.0
87
99
 
88
100
  - `fromOtlp(body)`: OpenTelemetry spans (GenAI semconv or OpenInference) as session events, the mapping behind gateway v0.15's `POST /v1/traces`. Point any framework's OTLP exporter at the gateway with the session's token, and its spans join the record.
@@ -31,6 +31,8 @@ export interface Plans {
31
31
  }
32
32
  /** Parses a plans file (spec/billing.md §1); throws on anything that isn't integer fils. */
33
33
  export declare function loadPlans(bytes: Uint8Array): Plans;
34
+ /** spec/billing.md §5b: how VAT applies to a customer. */
35
+ export type VatTreatment = "standard" | "zero_rated" | "reverse_charge";
34
36
  export interface TaxInvoiceInput {
35
37
  number: string;
36
38
  issued_at: string;
@@ -53,6 +55,8 @@ export interface TaxInvoiceInput {
53
55
  sessions: number;
54
56
  };
55
57
  vat_percent?: number;
58
+ /** §5b: `zero_rated` and `reverse_charge` invoice VAT at 0%, with a note saying why. */
59
+ vat_treatment?: VatTreatment;
56
60
  }
57
61
  /** spec/billing.md §5: the tax invoice document. */
58
62
  export declare function taxInvoice(input: TaxInvoiceInput): {
@@ -90,6 +94,8 @@ export declare function taxInvoice(input: TaxInvoiceInput): {
90
94
  events: number;
91
95
  sessions: number;
92
96
  };
97
+ vat_treatment?: "reverse_charge" | "zero_rated";
98
+ vat_note?: string;
93
99
  total_aed: string;
94
100
  };
95
101
  export type TaxInvoice = ReturnType<typeof taxInvoice>;
@@ -69,6 +69,10 @@ export function loadPlans(bytes) {
69
69
  }
70
70
  return doc;
71
71
  }
72
+ const VAT_NOTES = {
73
+ zero_rated: "Zero-rated supply (export of services): VAT at 0%",
74
+ reverse_charge: "Reverse charge: the customer accounts for the VAT",
75
+ };
72
76
  /** The month's `[from, to)` and its last day (spec/billing.md §5). */
73
77
  function period(month) {
74
78
  const [y, m] = month.split("-").map(Number);
@@ -79,7 +83,8 @@ function period(month) {
79
83
  /** spec/billing.md §5: the tax invoice document. */
80
84
  export function taxInvoice(input) {
81
85
  const p = period(input.month);
82
- const inv = invoice(input.plan, input.usage, input.vat_percent ?? 5);
86
+ const treatment = input.vat_treatment ?? "standard";
87
+ const inv = invoice(input.plan, input.usage, treatment === "standard" ? (input.vat_percent ?? 5) : 0);
83
88
  return {
84
89
  v: 1,
85
90
  title: "Tax Invoice",
@@ -101,6 +106,9 @@ export function taxInvoice(input) {
101
106
  plan: { id: input.plan_id, name: input.plan.name, version: input.plans_version },
102
107
  usage: { events: input.usage.events, sessions: input.usage.sessions },
103
108
  ...inv,
109
+ ...(treatment !== "standard"
110
+ ? { vat_treatment: treatment, vat_note: VAT_NOTES[treatment] }
111
+ : {}),
104
112
  total_aed: formatAed(inv.total_fils),
105
113
  };
106
114
  }
@@ -146,6 +154,7 @@ export function renderTaxInvoice(d) {
146
154
  `| VAT ${d.vat_percent}% | ${aed(d.vat_fils)} |`,
147
155
  `| **Total** | **${aed(d.total_fils)}** |`,
148
156
  "",
157
+ ...(d.vat_note ? [`VAT: ${d.vat_note}.`, ""] : []),
149
158
  `Plan: ${oneLine(d.plan.name)} (${d.plan.id}, plans ${d.plan.version}). Usage: ${grouped(d.usage.events)} events, ${grouped(d.usage.sessions)} sessions.`,
150
159
  "",
151
160
  ];
package/dist/cli.js CHANGED
@@ -43,7 +43,7 @@ const USAGE = `usage (spec/cli.md):
43
43
  blackbox policy-from-zanii <zanii-policy.json> [--out <file>]
44
44
  blackbox policy-to-zanii <policy.json> [--out <file>]
45
45
  blackbox statement --provider anthropic|openai --from <YYYY-MM-DD> --to <YYYY-MM-DD> [--out <file>]
46
- blackbox statement --provider csv --file <f> --model-column <name> --amount-column <name> --unit usd|cents|micro_usd|aed [--delimiter <c>] --from <d> --to <d> [--out <file>]
46
+ blackbox statement --provider csv --file <f> --model-column <name> --amount-column <name> (--unit usd|cents|micro_usd|aed | --currency <CUR> [--rate <usd per unit>] [--rate-date <d>]) [--delimiter <c>] --from <d> --to <d> [--out <file>]
47
47
  blackbox statement --provider custom --connector <file> --from <d> --to <d> [--out <file>]
48
48
  blackbox preflight --require <items> [--optional <items>]
49
49
  blackbox --version
@@ -53,7 +53,9 @@ const USAGE = `usage (spec/cli.md):
53
53
  blackbox reconcile-system --connector <connector.json> --log <export.csv|jsonl> [--pack <pack.json>] <session_id>
54
54
  blackbox workspace-receipt (a Claude Code PreToolUse / PostToolUse hook; reads the hook JSON on stdin)
55
55
  blackbox attest (a Claude Code PostToolUse hook: spec/attestation.md §4)
56
- blackbox policy-hook (a Claude Code / Codex PreToolUse hook: asks the gateway's policy; fails closed)`;
56
+ blackbox policy-hook (a Claude Code / Codex PreToolUse hook: asks the gateway's policy; fails closed)
57
+ blackbox key-split --m <M> --n <N> < secret (M-of-N shares of a secret, one per line: spec/operations.md §4b)
58
+ blackbox key-combine <share> <share> … (the secret back, on stdout)`;
57
59
  async function main(argv) {
58
60
  const [command, ...rest] = argv;
59
61
  if (command === "mcp")
@@ -70,6 +72,10 @@ async function main(argv) {
70
72
  return attestCommand();
71
73
  if (command === "policy-hook")
72
74
  return policyHookCommand();
75
+ if (command === "key-split")
76
+ return keySplitCommand(rest);
77
+ if (command === "key-combine")
78
+ return keyCombineCommand(rest);
73
79
  if (command === "verify")
74
80
  return verifyCommand(rest);
75
81
  if (command === "export")
@@ -218,6 +224,39 @@ async function attestCommand() {
218
224
  }
219
225
  return 0;
220
226
  }
227
+ /** spec/operations.md §4b: the secret on stdin, as M-of-N shares on stdout, one a line. */
228
+ async function keySplitCommand(rest) {
229
+ const { flags } = parse(rest, []);
230
+ const m = Number(flags.m);
231
+ const n = Number(flags.n);
232
+ const parts = [];
233
+ for await (const chunk of process.stdin)
234
+ parts.push(chunk);
235
+ const secret = Buffer.from(Buffer.concat(parts)
236
+ .toString("utf8")
237
+ .replace(/\r?\n$/, ""), "utf8");
238
+ try {
239
+ const { splitSecret } = await import("./shares/index.js");
240
+ process.stdout.write(`${splitSecret(secret, m, n).join("\n")}\n`);
241
+ return 0;
242
+ }
243
+ catch (error) {
244
+ process.stderr.write(`blackbox key-split: ${error instanceof Error ? error.message : error}\n`);
245
+ return 2;
246
+ }
247
+ }
248
+ /** spec/operations.md §4b: M shares (arguments) back to the secret, on stdout. */
249
+ async function keyCombineCommand(rest) {
250
+ try {
251
+ const { combineShares } = await import("./shares/index.js");
252
+ process.stdout.write(`${Buffer.from(combineShares(rest)).toString("utf8")}\n`);
253
+ return 0;
254
+ }
255
+ catch (error) {
256
+ process.stderr.write(`blackbox key-combine: ${error instanceof Error ? error.message : error}\n`);
257
+ return 2;
258
+ }
259
+ }
221
260
  /** spec/policy.md §2a: a PreToolUse hook. Deny: exit 2, the reason on stderr (the agent sees it).
222
261
  * Ask: Claude Code's `permissionDecision: "ask"` on stdout. Allow: exit 0, nothing printed. Any
223
262
  * failure denies, unless BLACKBOX_HOOK_FAIL_OPEN=1. */
@@ -956,8 +995,29 @@ async function statementAny(provider, flags, from, to) {
956
995
  if (provider === "csv") {
957
996
  const model = flags["model-column"];
958
997
  const amount = flags["amount-column"];
959
- const unit = flags.unit;
998
+ let unit = flags.unit;
960
999
  const delimiter = flags.delimiter;
1000
+ // spec/money.md §2c: another currency, at a stated rate or the ECB's for the day
1001
+ let fx;
1002
+ if (flags.currency) {
1003
+ try {
1004
+ fx = flags.rate
1005
+ ? {
1006
+ currency: flags.currency,
1007
+ usd_per_unit: flags.rate,
1008
+ date: flags["rate-date"] ?? to,
1009
+ source: "stated",
1010
+ }
1011
+ : await money.fxRate(flags.currency, {
1012
+ ...(flags["rate-date"] ? { date: flags["rate-date"] } : {}),
1013
+ ...(process.env.BLACKBOX_FX_URL ? { url: process.env.BLACKBOX_FX_URL } : {}),
1014
+ });
1015
+ unit = money.fxUnit(fx.currency, fx.usd_per_unit);
1016
+ }
1017
+ catch (e) {
1018
+ return message(e);
1019
+ }
1020
+ }
961
1021
  if (!flags.file ||
962
1022
  !model ||
963
1023
  !amount ||
@@ -972,7 +1032,7 @@ async function statementAny(provider, flags, from, to) {
972
1032
  unit,
973
1033
  ...(delimiter ? { delimiter } : {}),
974
1034
  });
975
- return { doc: { provider: "csv", from, to, lines: statement.lines } };
1035
+ return { doc: { provider: "csv", from, to, lines: statement.lines, ...(fx ? { fx } : {}) } };
976
1036
  }
977
1037
  catch (e) {
978
1038
  return message(e);
@@ -33,6 +33,19 @@ export interface PriceTable {
33
33
  currency: "USD";
34
34
  unit: "micro_usd_per_mtok";
35
35
  models: Record<string, ModelPrice>;
36
+ /** spec/cost.md §1a: percentages added to a call's table price when it matches. */
37
+ uplifts?: Uplift[];
38
+ }
39
+ /** spec/cost.md §1a: a call that matches every condition given costs `percent` more. */
40
+ export interface Uplift {
41
+ /** The configured provider id the call went to (`bedrock-eu`). */
42
+ provider?: string;
43
+ /** A model id prefix (`claude-opus-5`). */
44
+ model?: string;
45
+ /** Flattened usage fields and the values they must have (`{"inference_geo": "us"}`). */
46
+ usage?: Record<string, string>;
47
+ percent: number;
48
+ note?: string;
36
49
  }
37
50
  export interface Tokens {
38
51
  input: number;
@@ -58,6 +71,8 @@ export interface CallCost {
58
71
  service?: string;
59
72
  context_above?: number;
60
73
  };
74
+ /** spec/cost.md §1a: the uplift applied, when one matched. */
75
+ uplift_percent?: number;
61
76
  source?: "reported";
62
77
  }
63
78
  export interface CostReport {
@@ -126,6 +141,9 @@ export declare function priceCall(call: {
126
141
  }, table: PriceTable): Omit<CallCost, "seq" | "model"> & {
127
142
  key: string | null;
128
143
  };
144
+ /** spec/cost.md §1a: the sum of the uplifts a call matches (0 when none): its provider, a model
145
+ * prefix, and usage fields, each only when the uplift names it. */
146
+ export declare function upliftPercent(table: PriceTable, provider: string | null, model: string, usage: Record<string, unknown>): number;
129
147
  /** fils at the CBUAE peg (3.6725 AED / USD), rounded half up. */
130
148
  export declare function toAedFils(microUsd: number): number;
131
149
  export declare function formatAed(fils: number): string;
@@ -40,6 +40,24 @@ export function loadPrices(bytes) {
40
40
  if (p.status !== undefined && p.status !== "deprecated" && p.status !== "beta")
41
41
  throw new Error(`price table: ${model}.status must be deprecated or beta`);
42
42
  }
43
+ if (table.uplifts !== undefined) {
44
+ if (!Array.isArray(table.uplifts))
45
+ throw new Error("price table: uplifts must be a list");
46
+ for (const [i, u] of table.uplifts.entries()) {
47
+ const at = `price table: uplifts[${i}]`;
48
+ if (!Number.isSafeInteger(u?.percent) || u.percent < 1 || u.percent > 1000)
49
+ throw new Error(`${at}.percent must be an integer 1-1000`);
50
+ if (u.provider !== undefined && typeof u.provider !== "string")
51
+ throw new Error(`${at}.provider must be a string`);
52
+ if (u.model !== undefined && typeof u.model !== "string")
53
+ throw new Error(`${at}.model must be a string`);
54
+ if (u.usage !== undefined &&
55
+ (typeof u.usage !== "object" ||
56
+ u.usage === null ||
57
+ Object.values(u.usage).some((v) => typeof v !== "string")))
58
+ throw new Error(`${at}.usage must map usage fields to strings`);
59
+ }
60
+ }
43
61
  return { table, sha256: `sha256:${createHash("sha256").update(bytes).digest("hex")}` };
44
62
  }
45
63
  /** The exact model, else the longest table key the model starts with; null when there's none. */
@@ -194,12 +212,15 @@ export function priceCall(call, table) {
194
212
  const match = call.model === null ? null : priceFor(call.model, table, call.provider);
195
213
  if (match) {
196
214
  const { price, tier } = effectivePrice(match.price, tokens, call.serviceTier);
215
+ const base = callMicroUsd(tokens, price, requests);
216
+ const uplift = upliftPercent(table, call.provider ?? null, call.model, call.usage);
197
217
  return {
198
218
  key: match.key,
199
219
  tokens,
200
- micro_usd: callMicroUsd(tokens, price, requests),
220
+ micro_usd: uplift ? Math.floor((base * (100 + uplift) + 50) / 100) : base,
201
221
  ...(requests ? { requests } : {}),
202
222
  ...(tier ? { tier } : {}),
223
+ ...(uplift ? { uplift_percent: uplift } : {}),
203
224
  };
204
225
  }
205
226
  if (typeof call.reportedMicroUsd === "number")
@@ -212,6 +233,21 @@ export function priceCall(call, table) {
212
233
  };
213
234
  return { key: null, tokens, micro_usd: null, ...(requests ? { requests } : {}) };
214
235
  }
236
+ /** spec/cost.md §1a: the sum of the uplifts a call matches (0 when none): its provider, a model
237
+ * prefix, and usage fields, each only when the uplift names it. */
238
+ export function upliftPercent(table, provider, model, usage) {
239
+ let percent = 0;
240
+ for (const u of table.uplifts ?? []) {
241
+ if (u.provider !== undefined && u.provider !== provider)
242
+ continue;
243
+ if (u.model !== undefined && !model.startsWith(u.model))
244
+ continue;
245
+ if (u.usage && Object.entries(u.usage).some(([k, v]) => String(usage[k] ?? "") !== v))
246
+ continue;
247
+ percent += u.percent;
248
+ }
249
+ return percent;
250
+ }
215
251
  /** fils at the CBUAE peg (3.6725 AED / USD), rounded half up. */
216
252
  export function toAedFils(microUsd) {
217
253
  return Math.floor((microUsd * 36725 + 50_000_000) / 100_000_000);
package/dist/index.d.ts CHANGED
@@ -43,6 +43,7 @@ export { type AmountUnit, anthropicStatement, type Baseline, baselines, type Con
43
43
  export { deadlinesDue, filingPack, type OccurrenceFacts, type OccurrenceFramework, type OccurrenceReport, occurrenceFacts, occurrenceReport, renderOccurrence, } from "./occurrence/index.ts";
44
44
  export { ingestOcsf, type OcsfEvent, type OcsfVersion, ocsfLine, toOcsf } from "./ocsf/index.ts";
45
45
  export { fromOtlp, type OtlpEvent, toOtlp } from "./otlp/index.ts";
46
+ export { otlpFromProtobuf } from "./otlp/protobuf.ts";
46
47
  export { checkIdentifier, checkPack, corePack, type PackEntry, packData, packFrameworks, } from "./packs/index.ts";
47
48
  export { type PolicyChange, type PolicyDelta, policyDelta } from "./policy/delta.ts";
48
49
  export { type Draft, type DraftGroup, policyDrafts } from "./policy/drafts.ts";
@@ -62,6 +63,7 @@ export { ORPHAN_TEXT, repairToolCalls } from "./replay/repair.ts";
62
63
  export { checkSearch, type SearchMatch, type SearchQuery, searchEvents } from "./search/index.ts";
63
64
  export { type DrainResult, drainSpools } from "./session/drain.ts";
64
65
  export { type AnswerCheck, BlackboxSession, checkEgress, type EgressCheck, type FlightPlan, type LlmCallIds, type SessionError, type SessionOptions, type SessionState, type SessionStats, session, stableEventId, } from "./session/index.ts";
66
+ export { combineShares, splitSecret } from "./shares/index.ts";
65
67
  export { assembleSla, assessCompliance, buildSlaBody, signSla, slaHash, verifySla, } from "./sla/index.ts";
66
68
  export { assembleSuccession, buildSuccessionBody, type Succession, type SuccessionBody, type SuccessionReason, signSuccession, successionHash, verifyLineage, verifySuccession, } from "./succession/index.ts";
67
69
  export { type TimestampReport, timestampRequest, verifyTimestamp, } from "./timestamp/index.ts";
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ export { anthropicStatement, baselines, checkConnector, checkStatement, csvState
45
45
  export { deadlinesDue, filingPack, occurrenceFacts, occurrenceReport, renderOccurrence, } from "./occurrence/index.js";
46
46
  export { ingestOcsf, ocsfLine, toOcsf } from "./ocsf/index.js";
47
47
  export { fromOtlp, toOtlp } from "./otlp/index.js";
48
+ export { otlpFromProtobuf } from "./otlp/protobuf.js";
48
49
  export { checkIdentifier, checkPack, corePack, packData, packFrameworks, } from "./packs/index.js";
49
50
  export { policyDelta } from "./policy/delta.js";
50
51
  export { policyDrafts } from "./policy/drafts.js";
@@ -64,6 +65,7 @@ export { ORPHAN_TEXT, repairToolCalls } from "./replay/repair.js";
64
65
  export { checkSearch, searchEvents } from "./search/index.js";
65
66
  export { drainSpools } from "./session/drain.js";
66
67
  export { BlackboxSession, checkEgress, session, stableEventId, } from "./session/index.js";
68
+ export { combineShares, splitSecret } from "./shares/index.js";
67
69
  export { assembleSla, assessCompliance, buildSlaBody, signSla, slaHash, verifySla, } from "./sla/index.js";
68
70
  export { assembleSuccession, buildSuccessionBody, signSuccession, successionHash, verifyLineage, verifySuccession, } from "./succession/index.js";
69
71
  export { timestampRequest, verifyTimestamp, } from "./timestamp/index.js";
@@ -45,7 +45,23 @@ export declare function fuel(summary: Summary, budgetMicroUsd: number | null, ba
45
45
  p90_total_micro_usd: number | null;
46
46
  verdict: string;
47
47
  };
48
- export type AmountUnit = "usd" | "cents" | "micro_usd" | "aed";
48
+ /** `fx:<ISO code>:<USD per unit>` (`fx:EUR:1.0821`): another currency at a stated rate (§2c). */
49
+ export type AmountUnit = "usd" | "cents" | "micro_usd" | "aed" | `fx:${string}`;
50
+ /** spec/money.md §2c: the unit for amounts in `currency` at `usdPerUnit` (a decimal string). */
51
+ export declare function fxUnit(currency: string, usdPerUnit: string): AmountUnit;
52
+ export interface FxRate {
53
+ currency: string;
54
+ /** USD per one unit of the currency, as the source gave it (a decimal string). */
55
+ usd_per_unit: string;
56
+ /** The rate's own date (a reference rate is set once a working day). */
57
+ date: string;
58
+ source: string;
59
+ }
60
+ /** spec/money.md §2c: the ECB reference rate for a day (or the latest), through Frankfurter. */
61
+ export declare function fxRate(currency: string, opts?: {
62
+ date?: string;
63
+ url?: string;
64
+ }): Promise<FxRate>;
49
65
  /** spec/money.md §2a-b: an exact decimal amount (string, or a JSON number's text) to integer
50
66
  * micro-USD, rounded half up. `unit` is what the amount counts. */
51
67
  export declare function decimalToMicroUsd(value: unknown, unit: AmountUnit): number;
@@ -112,23 +112,66 @@ const UNITS = {
112
112
  micro_usd: [0, 1n],
113
113
  aed: [10, 36725n],
114
114
  };
115
+ const FX_UNIT = /^fx:([A-Z]{3}):(\d{1,9})(?:\.(\d{1,12}))?$/;
116
+ /** A unit as [power of ten, multiplier, divisor]; null for one that isn't. */
117
+ function unitScale(unit) {
118
+ const s = Object.hasOwn(UNITS, unit) ? UNITS[unit] : undefined;
119
+ if (s)
120
+ return [s[0], 1n, s[1]];
121
+ const m = FX_UNIT.exec(unit);
122
+ if (!m)
123
+ return null;
124
+ const frac = m[3] ?? "";
125
+ const rate = BigInt(`${m[2]}${frac}`);
126
+ return rate === 0n ? null : [6 - frac.length, rate, 1n];
127
+ }
128
+ /** spec/money.md §2c: the unit for amounts in `currency` at `usdPerUnit` (a decimal string). */
129
+ export function fxUnit(currency, usdPerUnit) {
130
+ const unit = `fx:${currency}:${usdPerUnit}`;
131
+ if (!unitScale(unit))
132
+ throw new Error(`not a currency and rate: ${currency} ${usdPerUnit}`);
133
+ return unit;
134
+ }
135
+ /** spec/money.md §2c: the ECB reference rate for a day (or the latest), through Frankfurter. */
136
+ export async function fxRate(currency, opts = {}) {
137
+ if (!/^[A-Z]{3}$/.test(currency))
138
+ throw new Error(`not an ISO 4217 code: ${currency}`);
139
+ if (opts.date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(opts.date))
140
+ throw new Error(`not a date: ${opts.date}`);
141
+ const base = (opts.url ?? "https://api.frankfurter.dev").replace(/\/+$/, "");
142
+ const res = await fetch(`${base}/v1/${opts.date ?? "latest"}?base=${currency}&symbols=USD`, {
143
+ signal: AbortSignal.timeout(10_000),
144
+ });
145
+ if (!res.ok)
146
+ throw new Error(`the rate service answered HTTP ${res.status}`);
147
+ const body = (await res.json());
148
+ const usd = body.rates?.USD;
149
+ if (typeof usd !== "number" || !(usd > 0) || typeof body.date !== "string")
150
+ throw new Error("the rate service's answer has no USD rate");
151
+ return {
152
+ currency,
153
+ usd_per_unit: String(usd),
154
+ date: body.date,
155
+ source: "ECB reference rate, via Frankfurter",
156
+ };
157
+ }
115
158
  /** spec/money.md §2a-b: an exact decimal amount (string, or a JSON number's text) to integer
116
159
  * micro-USD, rounded half up. `unit` is what the amount counts. */
117
160
  export function decimalToMicroUsd(value, unit) {
118
161
  const text = typeof value === "number" && Number.isFinite(value) ? String(value) : value;
119
162
  const m = typeof text === "string" ? DECIMAL.exec(text.trim()) : null;
120
- const scale = UNITS[unit];
163
+ const scale = unitScale(unit);
121
164
  if (!m || !scale)
122
165
  throw new Error(`not a non-negative decimal amount: ${JSON.stringify(value)}`);
123
166
  const frac = m[2] ?? "";
124
- const digits = BigInt(`${m[1]}${frac}`);
167
+ const digits = BigInt(`${m[1]}${frac}`) * scale[1];
125
168
  if (digits === 0n)
126
169
  return 0;
127
170
  const k = Number(m[3] ?? 0) - frac.length + scale[0];
128
171
  if (k > 30)
129
172
  throw new Error(`amount too large: ${JSON.stringify(value)}`);
130
173
  const num = k >= 0 ? digits * 10n ** BigInt(k) : digits;
131
- const den = scale[1] * (k >= 0 ? 1n : 10n ** BigInt(-k));
174
+ const den = scale[2] * (k >= 0 ? 1n : 10n ** BigInt(-k));
132
175
  let micro = num / den;
133
176
  if ((num % den) * 2n >= den)
134
177
  micro += 1n;
@@ -312,8 +355,8 @@ export function checkConnector(value) {
312
355
  for (const k of ["results", "model", "amount"])
313
356
  if (typeof c[k] !== "string" || !PATH.test(c[k]))
314
357
  return `${k} must be a path`;
315
- if (!c.unit || !(c.unit in UNITS))
316
- return "unit must be usd, cents, micro_usd or aed";
358
+ if (!c.unit || !unitScale(c.unit))
359
+ return "unit must be usd, cents, micro_usd, aed or fx:<CUR>:<USD per unit>";
317
360
  if (c.currency !== undefined &&
318
361
  (typeof c.currency?.path !== "string" ||
319
362
  !PATH.test(c.currency.path) ||
@@ -0,0 +1,4 @@
1
+ /** spec/otlp.md §4: an ExportTraceServiceRequest's bytes as OTLP/JSON. Throws on bad bytes. */
2
+ export declare function otlpFromProtobuf(buf: Uint8Array): {
3
+ resourceSpans: object[];
4
+ };
@@ -0,0 +1,167 @@
1
+ // OTLP/HTTP protobuf (spec/otlp.md §4): an ExportTraceServiceRequest's bytes as the OTLP/JSON
2
+ // shape `fromOtlp` reads. A reader for just the trace messages, no protobuf library: wire types
3
+ // varint, 64-bit, length-delimited and 32-bit; unknown fields are skipped. Mirrors
4
+ // sdks/python/src/zanii_blackbox/otlp_protobuf.py; pinned by spec/vectors/otlp-protobuf.json.
5
+ function fields(buf) {
6
+ const out = [];
7
+ let i = 0;
8
+ const varint = () => {
9
+ let r = 0n;
10
+ let s = 0n;
11
+ for (;;) {
12
+ if (i >= buf.length)
13
+ throw new Error("protobuf: a varint runs past the end");
14
+ const b = buf[i++];
15
+ r |= BigInt(b & 0x7f) << s;
16
+ if (!(b & 0x80))
17
+ return r;
18
+ s += 7n;
19
+ if (s > 63n)
20
+ throw new Error("protobuf: a varint over 64 bits");
21
+ }
22
+ };
23
+ const fixed = (size) => {
24
+ if (i + size > buf.length)
25
+ throw new Error("protobuf: a fixed field runs past the end");
26
+ let v = 0n;
27
+ for (let k = size - 1; k >= 0; k--)
28
+ v = (v << 8n) | BigInt(buf[i + k]);
29
+ i += size;
30
+ return v;
31
+ };
32
+ while (i < buf.length) {
33
+ const key = Number(varint());
34
+ const n = key >>> 3;
35
+ const wire = key & 7;
36
+ if (wire === 0)
37
+ out.push({ n, wire, v: varint() });
38
+ else if (wire === 1)
39
+ out.push({ n, wire, v: fixed(8) });
40
+ else if (wire === 5)
41
+ out.push({ n, wire, v: fixed(4) });
42
+ else if (wire === 2) {
43
+ const len = Number(varint());
44
+ if (i + len > buf.length)
45
+ throw new Error("protobuf: a field runs past the end");
46
+ out.push({ n, wire, v: buf.subarray(i, i + len) });
47
+ i += len;
48
+ }
49
+ else
50
+ throw new Error(`protobuf: wire type ${wire} isn't supported`);
51
+ }
52
+ return out;
53
+ }
54
+ const text = (b) => new TextDecoder().decode(b);
55
+ const hex = (b) => Buffer.from(b).toString("hex");
56
+ const bytes = (f) => f.v;
57
+ const big = (f) => f.v;
58
+ const float64 = (bits) => {
59
+ const view = new DataView(new ArrayBuffer(8));
60
+ view.setBigUint64(0, bits, true);
61
+ return view.getFloat64(0, true);
62
+ };
63
+ function anyValue(b) {
64
+ for (const f of fields(b)) {
65
+ if (f.n === 1 && f.wire === 2)
66
+ return { stringValue: text(bytes(f)) };
67
+ if (f.n === 2 && f.wire === 0)
68
+ return { boolValue: big(f) !== 0n };
69
+ if (f.n === 3 && f.wire === 0)
70
+ return { intValue: BigInt.asIntN(64, big(f)).toString() };
71
+ if (f.n === 4 && f.wire === 1)
72
+ return { doubleValue: float64(big(f)) };
73
+ if (f.n === 5 && f.wire === 2)
74
+ return {
75
+ arrayValue: {
76
+ values: fields(bytes(f))
77
+ .filter((x) => x.n === 1 && x.wire === 2)
78
+ .map((x) => anyValue(bytes(x))),
79
+ },
80
+ };
81
+ if (f.n === 6 && f.wire === 2)
82
+ return { kvlistValue: { values: keyValues(bytes(f), 1) } };
83
+ if (f.n === 7 && f.wire === 2)
84
+ return { bytesValue: Buffer.from(bytes(f)).toString("base64") };
85
+ }
86
+ return {};
87
+ }
88
+ /** The repeated KeyValue at field `at` of a message. */
89
+ function keyValues(b, at) {
90
+ return fields(b)
91
+ .filter((f) => f.n === at && f.wire === 2)
92
+ .map((f) => {
93
+ let key = "";
94
+ let value = {};
95
+ for (const g of fields(bytes(f))) {
96
+ if (g.n === 1 && g.wire === 2)
97
+ key = text(bytes(g));
98
+ if (g.n === 2 && g.wire === 2)
99
+ value = anyValue(bytes(g));
100
+ }
101
+ return { key, value };
102
+ });
103
+ }
104
+ function span(b) {
105
+ const out = {};
106
+ for (const f of fields(b)) {
107
+ if (f.n === 1 && f.wire === 2)
108
+ out.traceId = hex(bytes(f));
109
+ else if (f.n === 2 && f.wire === 2)
110
+ out.spanId = hex(bytes(f));
111
+ else if (f.n === 4 && f.wire === 2 && bytes(f).length)
112
+ out.parentSpanId = hex(bytes(f));
113
+ else if (f.n === 5 && f.wire === 2)
114
+ out.name = text(bytes(f));
115
+ else if (f.n === 6 && f.wire === 0)
116
+ out.kind = Number(big(f));
117
+ else if (f.n === 7 && f.wire === 1)
118
+ out.startTimeUnixNano = big(f).toString();
119
+ else if (f.n === 8 && f.wire === 1)
120
+ out.endTimeUnixNano = big(f).toString();
121
+ else if (f.n === 15 && f.wire === 2) {
122
+ const status = {};
123
+ for (const g of fields(bytes(f))) {
124
+ if (g.n === 2 && g.wire === 2)
125
+ status.message = text(bytes(g));
126
+ if (g.n === 3 && g.wire === 0)
127
+ status.code = Number(big(g));
128
+ }
129
+ out.status = status;
130
+ }
131
+ }
132
+ out.attributes = keyValues(b, 9);
133
+ return out;
134
+ }
135
+ /** spec/otlp.md §4: an ExportTraceServiceRequest's bytes as OTLP/JSON. Throws on bad bytes. */
136
+ export function otlpFromProtobuf(buf) {
137
+ return {
138
+ resourceSpans: fields(buf)
139
+ .filter((f) => f.n === 1 && f.wire === 2)
140
+ .map((rs) => {
141
+ const out = { scopeSpans: [] };
142
+ for (const f of fields(bytes(rs))) {
143
+ if (f.n === 1 && f.wire === 2)
144
+ out.resource = { attributes: keyValues(bytes(f), 1) };
145
+ if (f.n === 2 && f.wire === 2) {
146
+ const ss = { spans: [] };
147
+ for (const g of fields(bytes(f))) {
148
+ if (g.n === 1 && g.wire === 2) {
149
+ const scope = {};
150
+ for (const h of fields(bytes(g))) {
151
+ if (h.n === 1 && h.wire === 2)
152
+ scope.name = text(bytes(h));
153
+ if (h.n === 2 && h.wire === 2)
154
+ scope.version = text(bytes(h));
155
+ }
156
+ ss.scope = scope;
157
+ }
158
+ if (g.n === 2 && g.wire === 2)
159
+ ss.spans.push(span(bytes(g)));
160
+ }
161
+ out.scopeSpans.push(ss);
162
+ }
163
+ }
164
+ return out;
165
+ }),
166
+ };
167
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Splits `secret` into `n` shares, any `m` of which rebuild it: `zks1-<m>-<x>-<check>-<hex>`,
3
+ * where `check` (8 hex of the secret's SHA-256) catches a share from another secret. `random`
4
+ * gives the polynomial's coefficients (the tests pass fixed ones; never do that for real).
5
+ */
6
+ export declare function splitSecret(secret: Uint8Array, m: number, n: number, random?: (count: number) => Uint8Array): string[];
7
+ /** Rebuilds a secret from at least `m` of its shares; throws if they're too few or don't fit. */
8
+ export declare function combineShares(shares: readonly string[]): Uint8Array;
@@ -0,0 +1,84 @@
1
+ // M-of-N secret shares (spec/operations.md §4b): Shamir's scheme over GF(256), byte by byte, so any
2
+ // M of N custodians can rebuild a secret (the key passphrase) and fewer learn nothing about it.
3
+ // Mirrors sdks/python/src/zanii_blackbox/shares.py; pinned by spec/vectors/shares.json.
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ // GF(256) with the AES polynomial x^8 + x^4 + x^3 + x + 1, generator 3
6
+ const EXP = new Uint8Array(512);
7
+ const LOG = new Uint8Array(256);
8
+ {
9
+ let x = 1;
10
+ for (let i = 0; i < 255; i++) {
11
+ EXP[i] = x;
12
+ LOG[x] = i;
13
+ x ^= (x << 1) ^ (x & 0x80 ? 0x11b : 0);
14
+ x &= 0xff;
15
+ }
16
+ for (let i = 255; i < 512; i++)
17
+ EXP[i] = EXP[i - 255];
18
+ }
19
+ const mul = (a, b) => a === 0 || b === 0 ? 0 : EXP[LOG[a] + LOG[b]];
20
+ const div = (a, b) => a === 0 ? 0 : EXP[LOG[a] + 255 - LOG[b]];
21
+ const check = (secret) => createHash("sha256").update(secret).digest("hex").slice(0, 8);
22
+ /**
23
+ * Splits `secret` into `n` shares, any `m` of which rebuild it: `zks1-<m>-<x>-<check>-<hex>`,
24
+ * where `check` (8 hex of the secret's SHA-256) catches a share from another secret. `random`
25
+ * gives the polynomial's coefficients (the tests pass fixed ones; never do that for real).
26
+ */
27
+ export function splitSecret(secret, m, n, random = (count) => new Uint8Array(randomBytes(count))) {
28
+ if (!Number.isInteger(m) || !Number.isInteger(n) || m < 2 || n < m || n > 255)
29
+ throw new Error("shares need 2 ≤ m ≤ n ≤ 255");
30
+ if (secret.length < 1 || secret.length > 4096)
31
+ throw new Error("a secret is 1-4096 bytes");
32
+ const ys = Array.from({ length: n }, () => new Uint8Array(secret.length));
33
+ for (let i = 0; i < secret.length; i++) {
34
+ const coefficients = [secret[i], ...random(m - 1)];
35
+ for (let x = 1; x <= n; x++) {
36
+ let y = 0;
37
+ for (let k = coefficients.length - 1; k >= 0; k--)
38
+ y = mul(y, x) ^ coefficients[k]; // Horner, in GF(256)
39
+ ys[x - 1][i] = y;
40
+ }
41
+ }
42
+ const c = check(secret);
43
+ return ys.map((y, i) => `zks1-${m}-${i + 1}-${c}-${Buffer.from(y).toString("hex")}`);
44
+ }
45
+ /** Rebuilds a secret from at least `m` of its shares; throws if they're too few or don't fit. */
46
+ export function combineShares(shares) {
47
+ const parsed = shares.map((s) => {
48
+ const p = /^zks1-(\d{1,3})-(\d{1,3})-([0-9a-f]{8})-((?:[0-9a-f]{2})+)$/.exec(s.trim());
49
+ if (!p)
50
+ throw new Error("not a share (zks1-<m>-<x>-<check>-<hex>)");
51
+ return {
52
+ m: Number(p[1]),
53
+ x: Number(p[2]),
54
+ check: p[3],
55
+ y: Buffer.from(p[4], "hex"),
56
+ };
57
+ });
58
+ const first = parsed[0];
59
+ if (!first)
60
+ throw new Error("no shares");
61
+ if (parsed.some((p) => p.m !== first.m || p.check !== first.check || p.y.length !== first.y.length))
62
+ throw new Error("the shares aren't of one secret");
63
+ const unique = [...new Map(parsed.map((p) => [p.x, p])).values()];
64
+ if (unique.some((p) => p.x < 1 || p.x > 255))
65
+ throw new Error("a share's index is out of range");
66
+ if (unique.length < first.m)
67
+ throw new Error(`${first.m} shares are needed, ${unique.length} given`);
68
+ const use = unique.slice(0, first.m);
69
+ const secret = new Uint8Array(first.y.length);
70
+ for (let i = 0; i < secret.length; i++) {
71
+ let s = 0;
72
+ for (const [j, pj] of use.entries()) {
73
+ let l = 1; // the Lagrange basis at 0
74
+ for (const [k, pk] of use.entries())
75
+ if (k !== j)
76
+ l = mul(l, div(pk.x, pk.x ^ pj.x));
77
+ s ^= mul(pj.y[i], l);
78
+ }
79
+ secret[i] = s;
80
+ }
81
+ if (check(secret) !== first.check)
82
+ throw new Error("the shares don't fit together");
83
+ return secret;
84
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.13.0";
1
+ export declare const VERSION = "0.15.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // The SDK version, on its own so the CLI can print it without loading the whole SDK.
2
- export const VERSION = "0.13.0";
2
+ export const VERSION = "0.15.0";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zanii/blackbox",
3
3
  "license": "Apache-2.0",
4
- "version": "0.13.0",
4
+ "version": "0.15.0",
5
5
  "description": "The flight recorder for AI agents: sessions, a zero-loss spool, framework hooks, approvals, and offline verification of the gateway's hash-chained record.",
6
6
  "keywords": [
7
7
  "ai-agents",