@zanii/blackbox 0.2.0 → 0.4.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 +26 -1
- package/dist/a2a/index.d.ts +77 -0
- package/dist/a2a/index.js +305 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/analysis/accuracy.d.ts +24 -0
- package/dist/analysis/accuracy.js +45 -0
- package/dist/analysis/credential.d.ts +101 -0
- package/dist/analysis/credential.js +142 -0
- package/dist/analysis/faults.js +115 -0
- package/dist/analysis/grounding.d.ts +122 -0
- package/dist/analysis/grounding.js +445 -0
- package/dist/analysis/hallucination.d.ts +32 -0
- package/dist/analysis/hallucination.js +357 -0
- package/dist/analysis/index.d.ts +23 -0
- package/dist/analysis/index.js +93 -0
- package/dist/analysis/memory.d.ts +8 -0
- package/dist/analysis/memory.js +35 -8
- package/dist/analysis/reference.d.ts +49 -0
- package/dist/analysis/reference.js +164 -0
- package/dist/analysis/taxonomy.d.ts +18 -0
- package/dist/analysis/taxonomy.js +66 -0
- package/dist/approvals/index.d.ts +23 -0
- package/dist/approvals/index.js +48 -0
- package/dist/archive/index.d.ts +39 -0
- package/dist/archive/index.js +96 -0
- package/dist/archive/parquet.d.ts +2 -0
- package/dist/archive/parquet.js +185 -0
- package/dist/badge/index.d.ts +16 -0
- package/dist/badge/index.js +48 -0
- package/dist/bom/index.d.ts +14 -0
- package/dist/bom/index.js +152 -0
- package/dist/cli.js +114 -10
- package/dist/compliance/art12.d.ts +35 -0
- package/dist/compliance/art12.js +190 -0
- package/dist/compliance/index.d.ts +36 -2
- package/dist/compliance/index.js +78 -11
- package/dist/compliance/zanii.d.ts +29 -0
- package/dist/compliance/zanii.js +84 -0
- package/dist/constitution/index.d.ts +57 -0
- package/dist/constitution/index.js +131 -0
- package/dist/cv/index.d.ts +39 -0
- package/dist/cv/index.js +108 -0
- package/dist/disclosure/index.d.ts +31 -0
- package/dist/disclosure/index.js +113 -0
- package/dist/encryption/index.d.ts +9 -0
- package/dist/encryption/index.js +31 -0
- package/dist/evidence/index.d.ts +60 -0
- package/dist/evidence/index.js +151 -0
- package/dist/federation/index.d.ts +35 -0
- package/dist/federation/index.js +102 -0
- package/dist/finance/index.d.ts +126 -0
- package/dist/finance/index.js +320 -0
- package/dist/fleet/index.js +9 -0
- package/dist/gov/index.d.ts +108 -0
- package/dist/gov/index.js +225 -0
- package/dist/health/index.d.ts +120 -0
- package/dist/health/index.js +233 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +34 -5
- package/dist/memory/index.d.ts +36 -0
- package/dist/memory/index.js +85 -0
- package/dist/occurrence/index.d.ts +11 -0
- package/dist/occurrence/index.js +18 -0
- package/dist/ocsf/index.d.ts +1 -1
- package/dist/ocsf/index.js +36 -3
- package/dist/otlp/index.d.ts +8 -1
- package/dist/otlp/index.js +258 -1
- package/dist/packs/index.js +44 -4
- package/dist/policy/delta.js +7 -1
- package/dist/policy/index.d.ts +40 -6
- package/dist/policy/index.js +186 -8
- package/dist/policy/zanii.d.ts +31 -0
- package/dist/policy/zanii.js +87 -0
- package/dist/pq/index.d.ts +23 -0
- package/dist/pq/index.js +104 -0
- package/dist/search/index.d.ts +23 -0
- package/dist/search/index.js +69 -0
- package/dist/session/index.d.ts +107 -1
- package/dist/session/index.js +189 -11
- package/dist/sla/index.d.ts +61 -0
- package/dist/sla/index.js +197 -0
- package/dist/succession/index.d.ts +50 -0
- package/dist/succession/index.js +123 -0
- package/dist/timestamp/index.d.ts +24 -0
- package/dist/timestamp/index.js +274 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +46 -0
- package/dist/transparency/index.d.ts +188 -0
- package/dist/transparency/index.js +712 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.d.ts +31 -0
- package/dist/walls/index.js +119 -0
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Witnessing and federation (spec/anchoring.md §12): the gateway as an independent witness that
|
|
2
|
+
// co-signs the ledger's signed tree heads it checked, and gossip that turns a split view into an
|
|
3
|
+
// equivocation proof (@zanii/witness, @zanii/federation). Pure; mirrors
|
|
4
|
+
// sdks/python/src/zanii_blackbox/federation.py; pinned by spec/vectors/federation.json.
|
|
5
|
+
import { createSTH, verifySTH } from "@zanii/core";
|
|
6
|
+
const valid = (sth) => {
|
|
7
|
+
try {
|
|
8
|
+
return typeof sth === "object" && sth !== null && verifySTH(sth);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/** Zanii's witness co-signature over a signed tree head the caller has checked (signed, append-only). */
|
|
15
|
+
export function cosignLedgerSth(sth, o) {
|
|
16
|
+
if (!valid(sth))
|
|
17
|
+
throw new Error("refusing to co-sign: the signed tree head doesn't verify");
|
|
18
|
+
const cosignature = createSTH({ logId: o.did, size: sth.size, root: sth.root, ts: sth.ts }, o.privateKey);
|
|
19
|
+
return { v: 1, witnessed_log_id: sth.log_id, cosignature };
|
|
20
|
+
}
|
|
21
|
+
/** Zanii's check: a valid co-signature over the same (size, root) of a valid tree head. */
|
|
22
|
+
export function verifyLedgerCosignature(c, sth) {
|
|
23
|
+
const p = (c ?? {});
|
|
24
|
+
const cs = p.cosignature;
|
|
25
|
+
if (p.v !== 1 || !cs || typeof cs !== "object" || p.witnessed_log_id !== sth.log_id)
|
|
26
|
+
return false;
|
|
27
|
+
return valid(sth) && valid(cs) && cs.size === sth.size && cs.root === sth.root;
|
|
28
|
+
}
|
|
29
|
+
/** Zanii's equivocation proof: one log key, one size, two roots. Throws unless it really is one. */
|
|
30
|
+
export function buildEquivocationProof(a, b) {
|
|
31
|
+
if (a.log_id !== b.log_id)
|
|
32
|
+
throw new Error("not equivocation: different logs");
|
|
33
|
+
if (a.size !== b.size)
|
|
34
|
+
throw new Error("not equivocation: different tree sizes (could be honest growth)");
|
|
35
|
+
if (a.root === b.root)
|
|
36
|
+
throw new Error("not equivocation: same root — the views agree");
|
|
37
|
+
if (!valid(a) || !valid(b))
|
|
38
|
+
throw new Error("not equivocation: an STH signature is invalid (a forgery proves nothing)");
|
|
39
|
+
const [x, y] = a.root < b.root ? [a, b] : [b, a];
|
|
40
|
+
return { v: 1, type: "federation.equivocation", log_id: a.log_id, size: a.size, sths: [x, y] };
|
|
41
|
+
}
|
|
42
|
+
/** Re-checks a proof from scratch: both signatures, same log, same size, different roots. */
|
|
43
|
+
export function verifyEquivocationProof(proof) {
|
|
44
|
+
const p = (proof ?? {});
|
|
45
|
+
const sths = p.sths;
|
|
46
|
+
if (p.v !== 1 ||
|
|
47
|
+
p.type !== "federation.equivocation" ||
|
|
48
|
+
!Array.isArray(sths) ||
|
|
49
|
+
sths.length !== 2)
|
|
50
|
+
return false;
|
|
51
|
+
const [a, b] = sths;
|
|
52
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object")
|
|
53
|
+
return false;
|
|
54
|
+
return (a.log_id === p.log_id &&
|
|
55
|
+
b.log_id === p.log_id &&
|
|
56
|
+
a.size === p.size &&
|
|
57
|
+
b.size === p.size &&
|
|
58
|
+
a.root !== b.root &&
|
|
59
|
+
valid(a) &&
|
|
60
|
+
valid(b));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Zanii's gossip comparison over views already fetched: views of other logs and bad signatures
|
|
64
|
+
* are set aside, and two valid views at one size with different roots are an equivocation proof.
|
|
65
|
+
*/
|
|
66
|
+
export function compareViews(logId, views) {
|
|
67
|
+
const reasons = [];
|
|
68
|
+
const bySize = new Map();
|
|
69
|
+
let usable = 0;
|
|
70
|
+
for (const v of views) {
|
|
71
|
+
const sth = v.sth;
|
|
72
|
+
if (sth === undefined || sth === null) {
|
|
73
|
+
reasons.push(`${v.source}: unreachable (${v.error})`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (typeof sth !== "object") {
|
|
77
|
+
reasons.push(`${v.source}: not a signed tree head`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const s = sth;
|
|
81
|
+
if (s.log_id !== logId) {
|
|
82
|
+
reasons.push(`${v.source}: serves a different log (${String(s.log_id)})`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!valid(s)) {
|
|
86
|
+
reasons.push(`${v.source}: the signature doesn't verify`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
usable++;
|
|
90
|
+
const seen = bySize.get(s.size);
|
|
91
|
+
if (seen && seen.sth.root !== s.root) {
|
|
92
|
+
const equivocation = buildEquivocationProof(seen.sth, s);
|
|
93
|
+
reasons.push(`split view: ${seen.source} and ${v.source} show different roots at size ${s.size}`);
|
|
94
|
+
return { ok: false, reasons, equivocation };
|
|
95
|
+
}
|
|
96
|
+
if (!seen)
|
|
97
|
+
bySize.set(s.size, { source: v.source, sth: s });
|
|
98
|
+
}
|
|
99
|
+
if (usable === 0)
|
|
100
|
+
reasons.push("no usable views");
|
|
101
|
+
return { ok: usable > 0, reasons };
|
|
102
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
type Obj = Record<string, unknown>;
|
|
2
|
+
/** Minor-unit precision: Zanii's table, plus the Gulf's currencies. */
|
|
3
|
+
export declare const CURRENCIES: Readonly<Record<string, number>>;
|
|
4
|
+
/** Zanii's exact parse: a decimal string to minor units (a string), never a float. */
|
|
5
|
+
export declare function parseMoney(amount: string | number, currency: string): {
|
|
6
|
+
currency: string;
|
|
7
|
+
minor: string;
|
|
8
|
+
display: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const formatMoney: (minor: string | number | bigint, currency: string) => string;
|
|
11
|
+
/** Zanii's payment receipt payload. No settlement reference, no payment. */
|
|
12
|
+
export declare function buildPayment(input: {
|
|
13
|
+
rail: string;
|
|
14
|
+
reference: string;
|
|
15
|
+
amount: string | number;
|
|
16
|
+
currency: string;
|
|
17
|
+
from?: string;
|
|
18
|
+
to?: string;
|
|
19
|
+
memo?: string;
|
|
20
|
+
extra?: Obj;
|
|
21
|
+
}): {
|
|
22
|
+
target: string;
|
|
23
|
+
rail: string;
|
|
24
|
+
reference: string;
|
|
25
|
+
money: {
|
|
26
|
+
currency: string;
|
|
27
|
+
minor: string;
|
|
28
|
+
display: string;
|
|
29
|
+
};
|
|
30
|
+
from: string | null;
|
|
31
|
+
to: string | null;
|
|
32
|
+
memo: string | null;
|
|
33
|
+
extra: Obj | null;
|
|
34
|
+
};
|
|
35
|
+
/** Zanii's KYA screen, offline: the DID or its owner on a deny list is a hit. */
|
|
36
|
+
export declare function screenCounterparty(did: string, opts?: {
|
|
37
|
+
denyList?: readonly string[];
|
|
38
|
+
owner?: string;
|
|
39
|
+
}): {
|
|
40
|
+
did: string;
|
|
41
|
+
owner: string | null;
|
|
42
|
+
ok: boolean;
|
|
43
|
+
hits: {
|
|
44
|
+
source: string;
|
|
45
|
+
detail: string;
|
|
46
|
+
}[];
|
|
47
|
+
history: null;
|
|
48
|
+
};
|
|
49
|
+
/** Zanii's kya.screening payload for a screening result. */
|
|
50
|
+
export declare function screeningPayload(result: ReturnType<typeof screenCounterparty>, ts: string, screenedBy?: string | null): {
|
|
51
|
+
_zr_kind: string;
|
|
52
|
+
v: number;
|
|
53
|
+
did: string;
|
|
54
|
+
owner: string | null;
|
|
55
|
+
ok: boolean;
|
|
56
|
+
hits: {
|
|
57
|
+
source: string;
|
|
58
|
+
detail: string;
|
|
59
|
+
}[];
|
|
60
|
+
sources: string[];
|
|
61
|
+
screened_by: string | null;
|
|
62
|
+
ts: string;
|
|
63
|
+
};
|
|
64
|
+
export declare const FTA_WALL_ID = "fta-prepares-never-files";
|
|
65
|
+
/** Zanii's FTA payloads: a filing prepared (never filed), and its hand-off to a Tax Agent. */
|
|
66
|
+
export declare function ftaPayload(input: {
|
|
67
|
+
action: "prepared" | "handoff";
|
|
68
|
+
filingType: string;
|
|
69
|
+
period: string;
|
|
70
|
+
by: string;
|
|
71
|
+
toTaxAgent?: string;
|
|
72
|
+
filingRef?: string;
|
|
73
|
+
manifestHash?: string;
|
|
74
|
+
}): Obj;
|
|
75
|
+
/** Zanii's kyb.attestation: an owner DID signs which domain and legal entity it is. */
|
|
76
|
+
export declare function buildOwnerAttestation(input: {
|
|
77
|
+
did: string;
|
|
78
|
+
domain: string;
|
|
79
|
+
entity: {
|
|
80
|
+
name: string;
|
|
81
|
+
jurisdiction?: string;
|
|
82
|
+
registration?: string;
|
|
83
|
+
};
|
|
84
|
+
ts: string;
|
|
85
|
+
ownerPrivateKey: Uint8Array;
|
|
86
|
+
}): {
|
|
87
|
+
v: number;
|
|
88
|
+
type: string;
|
|
89
|
+
did: string;
|
|
90
|
+
domain: string;
|
|
91
|
+
entity: {
|
|
92
|
+
name: string;
|
|
93
|
+
jurisdiction?: string;
|
|
94
|
+
registration?: string;
|
|
95
|
+
};
|
|
96
|
+
ts: string;
|
|
97
|
+
sig: string;
|
|
98
|
+
};
|
|
99
|
+
export declare function verifyOwnerAttestation(a: unknown): boolean;
|
|
100
|
+
/** Zanii's two-way check: the owner signed it, and the domain's own doc lists that owner. */
|
|
101
|
+
export declare function verifyOwnerBinding(attestation: unknown, doc: unknown): {
|
|
102
|
+
ok: boolean;
|
|
103
|
+
reasons: string[];
|
|
104
|
+
entity: Obj | null;
|
|
105
|
+
};
|
|
106
|
+
export declare function financeFindings(lines: readonly string[], bodies: (hash: string) => Uint8Array | undefined, closing?: boolean): {
|
|
107
|
+
code: string;
|
|
108
|
+
source: string;
|
|
109
|
+
severity: "warning" | "advisory";
|
|
110
|
+
ref: Record<string, string | number>;
|
|
111
|
+
detail: string;
|
|
112
|
+
}[];
|
|
113
|
+
/** spec/finance.md §2: the session's payments, and exact totals per currency (minor units). */
|
|
114
|
+
export declare function paymentsOf(lines: readonly string[], bodies: (hash: string) => Uint8Array | undefined): {
|
|
115
|
+
payments: {
|
|
116
|
+
seq: number;
|
|
117
|
+
rail: unknown;
|
|
118
|
+
reference: unknown;
|
|
119
|
+
money: Obj;
|
|
120
|
+
to: unknown;
|
|
121
|
+
}[];
|
|
122
|
+
totals: {
|
|
123
|
+
[k: string]: string;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
export {};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// Finance (spec/finance.md): exact money and payments (@zanii/payments), counterparty screening
|
|
2
|
+
// (@zanii/kya), tax filings prepared, never filed (@zanii/fta), the owner behind a counterparty
|
|
3
|
+
// (@zanii/kyb), and the findings over them. Pure; mirrors sdks/python/src/zanii_blackbox/finance.py;
|
|
4
|
+
// pinned by spec/vectors/finance.json.
|
|
5
|
+
import { canonicalBytes, publicKeyFromDid } from "@zanii/core";
|
|
6
|
+
import { ed25519Sign, ed25519Verify } from "../transparency/index.js";
|
|
7
|
+
/** Minor-unit precision: Zanii's table, plus the Gulf's currencies. */
|
|
8
|
+
export const CURRENCIES = {
|
|
9
|
+
USD: 2,
|
|
10
|
+
EUR: 2,
|
|
11
|
+
GBP: 2,
|
|
12
|
+
CHF: 2,
|
|
13
|
+
CAD: 2,
|
|
14
|
+
AUD: 2,
|
|
15
|
+
CNY: 2,
|
|
16
|
+
INR: 2,
|
|
17
|
+
JPY: 0,
|
|
18
|
+
KRW: 0,
|
|
19
|
+
USDC: 6,
|
|
20
|
+
USDT: 6,
|
|
21
|
+
BTC: 8,
|
|
22
|
+
ZAN: 9,
|
|
23
|
+
ETH: 18,
|
|
24
|
+
AED: 2,
|
|
25
|
+
SAR: 2,
|
|
26
|
+
QAR: 2,
|
|
27
|
+
KWD: 3,
|
|
28
|
+
BHD: 3,
|
|
29
|
+
OMR: 3,
|
|
30
|
+
};
|
|
31
|
+
function decimalsOf(currency) {
|
|
32
|
+
const d = CURRENCIES[currency.toUpperCase()];
|
|
33
|
+
if (d === undefined)
|
|
34
|
+
throw new Error(`unknown currency '${currency}'`);
|
|
35
|
+
return d;
|
|
36
|
+
}
|
|
37
|
+
function formatMinorUnits(minor, decimals) {
|
|
38
|
+
const neg = minor < 0n;
|
|
39
|
+
const m = neg ? -minor : minor;
|
|
40
|
+
const base = 10n ** BigInt(decimals);
|
|
41
|
+
const whole = m / base;
|
|
42
|
+
if (decimals === 0)
|
|
43
|
+
return `${neg ? "-" : ""}${whole}`;
|
|
44
|
+
return `${neg ? "-" : ""}${whole}.${(m % base).toString().padStart(decimals, "0")}`;
|
|
45
|
+
}
|
|
46
|
+
/** Zanii's exact parse: a decimal string to minor units (a string), never a float. */
|
|
47
|
+
export function parseMoney(amount, currency) {
|
|
48
|
+
const decimals = decimalsOf(currency);
|
|
49
|
+
const s = String(amount).trim();
|
|
50
|
+
if (!/^\d+(\.\d+)?$/.test(s))
|
|
51
|
+
throw new Error(`invalid amount '${s}' (expected a non-negative decimal)`);
|
|
52
|
+
const [whole = "0", frac = ""] = s.split(".");
|
|
53
|
+
if (frac.length > decimals)
|
|
54
|
+
throw new Error(`amount '${s}' has more precision than ${currency} allows (${decimals} dp)`);
|
|
55
|
+
const minor = BigInt(whole) * 10n ** BigInt(decimals) + BigInt(frac.padEnd(decimals, "0") || "0");
|
|
56
|
+
if (minor <= 0n)
|
|
57
|
+
throw new Error("amount must be > 0");
|
|
58
|
+
const cur = currency.toUpperCase();
|
|
59
|
+
return {
|
|
60
|
+
currency: cur,
|
|
61
|
+
minor: minor.toString(),
|
|
62
|
+
display: `${formatMinorUnits(minor, decimals)} ${cur}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export const formatMoney = (minor, currency) => formatMinorUnits(BigInt(minor), decimalsOf(currency));
|
|
66
|
+
/** Zanii's payment receipt payload. No settlement reference, no payment. */
|
|
67
|
+
export function buildPayment(input) {
|
|
68
|
+
if (!input.rail)
|
|
69
|
+
throw new Error("rail is required");
|
|
70
|
+
if (!input.reference)
|
|
71
|
+
throw new Error("reference is required (a payment with no settlement reference is unprovable)");
|
|
72
|
+
return {
|
|
73
|
+
target: `pay.${input.rail.toLowerCase()}`,
|
|
74
|
+
rail: input.rail,
|
|
75
|
+
reference: input.reference,
|
|
76
|
+
money: parseMoney(input.amount, input.currency),
|
|
77
|
+
from: input.from ?? null,
|
|
78
|
+
to: input.to ?? null,
|
|
79
|
+
memo: input.memo ?? null,
|
|
80
|
+
extra: input.extra ?? null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Zanii's KYA screen, offline: the DID or its owner on a deny list is a hit. */
|
|
84
|
+
export function screenCounterparty(did, opts = {}) {
|
|
85
|
+
if (!did)
|
|
86
|
+
throw new Error("did is required");
|
|
87
|
+
const deny = new Set(opts.denyList ?? []);
|
|
88
|
+
const hits = [];
|
|
89
|
+
if (deny.has(did))
|
|
90
|
+
hits.push({ source: "deny-list", detail: did });
|
|
91
|
+
if (opts.owner && deny.has(opts.owner))
|
|
92
|
+
hits.push({ source: "deny-list", detail: opts.owner });
|
|
93
|
+
return { did, owner: opts.owner ?? null, ok: hits.length === 0, hits, history: null };
|
|
94
|
+
}
|
|
95
|
+
/** Zanii's kya.screening payload for a screening result. */
|
|
96
|
+
export function screeningPayload(result, ts, screenedBy = null) {
|
|
97
|
+
const sources = [...new Set(result.hits.map((h) => h.source))].sort();
|
|
98
|
+
return {
|
|
99
|
+
_zr_kind: "kya",
|
|
100
|
+
v: 1,
|
|
101
|
+
did: result.did,
|
|
102
|
+
owner: result.owner,
|
|
103
|
+
ok: result.ok,
|
|
104
|
+
hits: result.hits,
|
|
105
|
+
sources,
|
|
106
|
+
screened_by: screenedBy,
|
|
107
|
+
ts,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export const FTA_WALL_ID = "fta-prepares-never-files";
|
|
111
|
+
/** Zanii's FTA payloads: a filing prepared (never filed), and its hand-off to a Tax Agent. */
|
|
112
|
+
export function ftaPayload(input) {
|
|
113
|
+
if (!input.filingType)
|
|
114
|
+
throw new Error("filing_type is required");
|
|
115
|
+
if (!input.period)
|
|
116
|
+
throw new Error("period is required");
|
|
117
|
+
if (input.action === "prepared")
|
|
118
|
+
return {
|
|
119
|
+
_zr_kind: "fta",
|
|
120
|
+
v: 1,
|
|
121
|
+
action: "prepared",
|
|
122
|
+
filing_type: input.filingType,
|
|
123
|
+
period: input.period,
|
|
124
|
+
prepared_by: input.by,
|
|
125
|
+
filing_ref: input.filingRef ?? null,
|
|
126
|
+
manifest_hash: input.manifestHash ?? null,
|
|
127
|
+
wall: FTA_WALL_ID,
|
|
128
|
+
};
|
|
129
|
+
if (!input.toTaxAgent)
|
|
130
|
+
throw new Error("to_tax_agent (the licensed Tax Agent) is required");
|
|
131
|
+
return {
|
|
132
|
+
_zr_kind: "fta",
|
|
133
|
+
v: 1,
|
|
134
|
+
action: "handoff",
|
|
135
|
+
filing_type: input.filingType,
|
|
136
|
+
period: input.period,
|
|
137
|
+
from_agent: input.by,
|
|
138
|
+
to_tax_agent: input.toTaxAgent,
|
|
139
|
+
filing_ref: input.filingRef ?? null,
|
|
140
|
+
wall: FTA_WALL_ID,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// ---------------------------------------------------------------- KYB: the owner behind a party
|
|
144
|
+
/** Zanii's kyb.attestation: an owner DID signs which domain and legal entity it is. */
|
|
145
|
+
export function buildOwnerAttestation(input) {
|
|
146
|
+
if (!input.did)
|
|
147
|
+
throw new Error("did is required");
|
|
148
|
+
if (!input.domain)
|
|
149
|
+
throw new Error("domain is required");
|
|
150
|
+
if (!input.entity?.name)
|
|
151
|
+
throw new Error("entity.name is required");
|
|
152
|
+
if (Number.isNaN(Date.parse(input.ts)))
|
|
153
|
+
throw new Error("ts must be an ISO timestamp");
|
|
154
|
+
const unsigned = {
|
|
155
|
+
v: 1,
|
|
156
|
+
type: "kyb.attestation",
|
|
157
|
+
did: input.did,
|
|
158
|
+
domain: input.domain.toLowerCase(),
|
|
159
|
+
entity: input.entity,
|
|
160
|
+
ts: input.ts,
|
|
161
|
+
};
|
|
162
|
+
const sig = Buffer.from(ed25519Sign(input.ownerPrivateKey, canonicalBytes(unsigned))).toString("hex");
|
|
163
|
+
return { ...unsigned, sig: `ed25519:${sig}` };
|
|
164
|
+
}
|
|
165
|
+
export function verifyOwnerAttestation(a) {
|
|
166
|
+
const p = (a ?? {});
|
|
167
|
+
if (p.v !== 1 || p.type !== "kyb.attestation" || !p.did || !p.domain)
|
|
168
|
+
return false;
|
|
169
|
+
const entity = p.entity;
|
|
170
|
+
if (!entity || typeof entity !== "object" || !entity.name)
|
|
171
|
+
return false;
|
|
172
|
+
if (typeof p.ts !== "string" || Number.isNaN(Date.parse(p.ts)))
|
|
173
|
+
return false;
|
|
174
|
+
const pub = publicKeyFromDid(String(p.did));
|
|
175
|
+
const m = typeof p.sig === "string" ? /^ed25519:([0-9a-f]{128})$/.exec(p.sig) : null;
|
|
176
|
+
if (!pub || !m)
|
|
177
|
+
return false;
|
|
178
|
+
const { sig: _sig, ...unsigned } = p;
|
|
179
|
+
return ed25519Verify(pub, canonicalBytes(unsigned), Buffer.from(m[1], "hex"));
|
|
180
|
+
}
|
|
181
|
+
/** Zanii's two-way check: the owner signed it, and the domain's own doc lists that owner. */
|
|
182
|
+
export function verifyOwnerBinding(attestation, doc) {
|
|
183
|
+
const a = (attestation ?? {});
|
|
184
|
+
if (!verifyOwnerAttestation(attestation))
|
|
185
|
+
return { ok: false, reasons: ["attestation is not validly signed by its DID"], entity: null };
|
|
186
|
+
const d = (doc ?? {});
|
|
187
|
+
if (d.v !== 1 ||
|
|
188
|
+
typeof d.domain !== "string" ||
|
|
189
|
+
typeof d.entity !== "object" ||
|
|
190
|
+
d.entity === null ||
|
|
191
|
+
!d.entity.name ||
|
|
192
|
+
!Array.isArray(d.owner_dids))
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
reasons: [
|
|
196
|
+
`${a.domain} publishes no valid owner doc — the domain side of the binding is missing`,
|
|
197
|
+
],
|
|
198
|
+
entity: null,
|
|
199
|
+
};
|
|
200
|
+
const reasons = [];
|
|
201
|
+
if (d.domain.toLowerCase() !== a.domain.toLowerCase())
|
|
202
|
+
reasons.push(`attestation names ${a.domain} but the doc is for ${d.domain}`);
|
|
203
|
+
if (!d.owner_dids.includes(a.did))
|
|
204
|
+
reasons.push(`the domain does not list ${a.did} — anyone can sign a claim; only the domain's list makes it mutual`);
|
|
205
|
+
if (d.entity.name !== a.entity.name)
|
|
206
|
+
reasons.push(`entity name mismatch: domain says '${String(d.entity.name)}', owner claims '${String(a.entity.name)}'`);
|
|
207
|
+
for (const field of ["jurisdiction", "registration"]) {
|
|
208
|
+
const x = a.entity[field];
|
|
209
|
+
const y = d.entity[field];
|
|
210
|
+
if (x !== undefined && y !== undefined && x !== y)
|
|
211
|
+
reasons.push(`entity ${field} mismatch: domain says '${String(y)}', owner claims '${String(x)}'`);
|
|
212
|
+
}
|
|
213
|
+
return { ok: reasons.length === 0, reasons, entity: reasons.length === 0 ? d.entity : null };
|
|
214
|
+
}
|
|
215
|
+
const decoder = new TextDecoder();
|
|
216
|
+
export function financeFindings(lines, bodies, closing = false) {
|
|
217
|
+
let closed = closing;
|
|
218
|
+
const out = [];
|
|
219
|
+
const screened = new Map();
|
|
220
|
+
const prepared = [];
|
|
221
|
+
const handedOff = new Set();
|
|
222
|
+
for (const l of lines) {
|
|
223
|
+
const e = JSON.parse(l);
|
|
224
|
+
if (e.kind === "session.close")
|
|
225
|
+
closed = true;
|
|
226
|
+
if (e.kind !== "sdk.event")
|
|
227
|
+
continue;
|
|
228
|
+
const type = e.meta.type;
|
|
229
|
+
if (type !== "payment" &&
|
|
230
|
+
type !== "kya.screening" &&
|
|
231
|
+
type !== "fta.prepared" &&
|
|
232
|
+
type !== "fta.handoff")
|
|
233
|
+
continue;
|
|
234
|
+
const body = bodies(e.body_hash);
|
|
235
|
+
let data = {};
|
|
236
|
+
try {
|
|
237
|
+
data = body ? JSON.parse(decoder.decode(body)) : {};
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
if (type === "kya.screening" && typeof data.did === "string")
|
|
241
|
+
screened.set(data.did, { ok: data.ok === true, seq: e.seq });
|
|
242
|
+
else if (type === "payment" && typeof data.to === "string" && data.to) {
|
|
243
|
+
const s = screened.get(data.to);
|
|
244
|
+
if (!s)
|
|
245
|
+
out.push({
|
|
246
|
+
code: "UNSCREENED_COUNTERPARTY",
|
|
247
|
+
source: "detectors",
|
|
248
|
+
severity: "warning",
|
|
249
|
+
ref: { seq: e.seq, to: data.to },
|
|
250
|
+
detail: "A payment to a counterparty this session never screened first.",
|
|
251
|
+
});
|
|
252
|
+
else if (!s.ok)
|
|
253
|
+
out.push({
|
|
254
|
+
code: "SCREENING_HIT_PAID",
|
|
255
|
+
source: "detectors",
|
|
256
|
+
severity: "warning",
|
|
257
|
+
ref: { seq: e.seq, to: data.to, screening_seq: s.seq },
|
|
258
|
+
detail: "A payment to a counterparty whose screening had hits.",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else if (type === "fta.prepared" || type === "fta.handoff") {
|
|
262
|
+
const key = `${String(data.filing_type)}\u0000${String(data.period)}`;
|
|
263
|
+
if (type === "fta.handoff")
|
|
264
|
+
handedOff.add(key);
|
|
265
|
+
else
|
|
266
|
+
prepared.push({
|
|
267
|
+
seq: e.seq,
|
|
268
|
+
key,
|
|
269
|
+
filing_type: String(data.filing_type),
|
|
270
|
+
period: String(data.period),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (closed)
|
|
275
|
+
for (const p of prepared)
|
|
276
|
+
if (!handedOff.has(p.key))
|
|
277
|
+
out.push({
|
|
278
|
+
code: "FTA_NO_HANDOFF",
|
|
279
|
+
source: "detectors",
|
|
280
|
+
severity: "advisory",
|
|
281
|
+
ref: { seq: p.seq, filing_type: p.filing_type, period: p.period },
|
|
282
|
+
detail: "A tax filing was prepared and never handed to a licensed Tax Agent.",
|
|
283
|
+
});
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/** spec/finance.md §2: the session's payments, and exact totals per currency (minor units). */
|
|
287
|
+
export function paymentsOf(lines, bodies) {
|
|
288
|
+
const payments = [];
|
|
289
|
+
const totals = new Map();
|
|
290
|
+
for (const l of lines) {
|
|
291
|
+
const e = JSON.parse(l);
|
|
292
|
+
if (e.kind !== "sdk.event" || e.meta.type !== "payment")
|
|
293
|
+
continue;
|
|
294
|
+
const body = bodies(e.body_hash);
|
|
295
|
+
let p;
|
|
296
|
+
try {
|
|
297
|
+
p = body ? JSON.parse(decoder.decode(body)) : {};
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const m = p.money;
|
|
303
|
+
if (!m || typeof m !== "object")
|
|
304
|
+
continue;
|
|
305
|
+
payments.push({
|
|
306
|
+
seq: e.seq,
|
|
307
|
+
rail: p.rail ?? null,
|
|
308
|
+
reference: p.reference ?? null,
|
|
309
|
+
money: m,
|
|
310
|
+
to: p.to ?? null,
|
|
311
|
+
});
|
|
312
|
+
const cur = String(m.currency);
|
|
313
|
+
totals.set(cur, (totals.get(cur) ?? 0n) + BigInt(String(m.minor)));
|
|
314
|
+
}
|
|
315
|
+
const sorted = [...totals.keys()].sort();
|
|
316
|
+
return {
|
|
317
|
+
payments,
|
|
318
|
+
totals: Object.fromEntries(sorted.map((k) => [k, String(totals.get(k))])),
|
|
319
|
+
};
|
|
320
|
+
}
|
package/dist/fleet/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { toolCallsOf } from "../analysis/detectors.js";
|
|
|
6
6
|
import { FAULTS } from "../analysis/faults.js";
|
|
7
7
|
import { eventsOf } from "../analysis/index.js";
|
|
8
8
|
import { landing } from "../analysis/landing.js";
|
|
9
|
+
import { authorityTimeline } from "../authority/index.js";
|
|
9
10
|
import { formatAed, sessionCost, toAedFils } from "../cost/index.js";
|
|
10
11
|
import { callsOf } from "../reconcile/record.js";
|
|
11
12
|
import { canonical } from "../reconcile/shared.js";
|
|
@@ -60,10 +61,18 @@ export function sessionSummary(lines, bodies, prices) {
|
|
|
60
61
|
const claims = { satisfied: 0, failed: 0, unverified: 0 };
|
|
61
62
|
for (const c of landing(eventsOf(lines, bodies), lines, bodies).claims)
|
|
62
63
|
claims[c.verdict]++;
|
|
64
|
+
const inControl = authorityTimeline(lines).at(-1)?.mode ?? "agent";
|
|
63
65
|
return {
|
|
64
66
|
session_id: first.session_id,
|
|
65
67
|
label: typeof first.meta.label === "string" ? first.meta.label : null,
|
|
66
68
|
tenant: typeof first.meta.tenant === "string" ? first.meta.tenant : null,
|
|
69
|
+
// spec/api.md: only when the session has one, so older summaries read the same
|
|
70
|
+
...(typeof first.meta.environment === "string" ? { environment: first.meta.environment } : {}),
|
|
71
|
+
// spec/fleet.md §2: the budget, when there is one; who flies, when it isn't the agent
|
|
72
|
+
...(typeof first.meta.budget_micro_usd === "number"
|
|
73
|
+
? { budget_micro_usd: first.meta.budget_micro_usd }
|
|
74
|
+
: {}),
|
|
75
|
+
...(inControl !== "agent" ? { in_control: inControl } : {}),
|
|
67
76
|
objective: typeof plan?.objective === "string" ? plan.objective : null,
|
|
68
77
|
started_at: first.ts,
|
|
69
78
|
last_event_at: last.ts,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
type Text = {
|
|
2
|
+
en: string;
|
|
3
|
+
ar: string;
|
|
4
|
+
};
|
|
5
|
+
type Obj = Record<string, unknown>;
|
|
6
|
+
export declare const GOV_DECISION_KINDS: readonly ["benefit.grant", "benefit.deny", "visa.decide", "fine.issue", "licence.decide"];
|
|
7
|
+
/** Zanii's pseudonymous, authority-scoped tag for a person's did:key. */
|
|
8
|
+
export declare function subjectTag(subjectDid: string, authority: string): string;
|
|
9
|
+
/** Zanii's subject claim: "this is my slice", signed with the person's own key. */
|
|
10
|
+
export declare function signSubjectClaim(input: {
|
|
11
|
+
platform: string;
|
|
12
|
+
subjectDid: string;
|
|
13
|
+
ts: string;
|
|
14
|
+
subjectPrivateKey: Uint8Array;
|
|
15
|
+
}): {
|
|
16
|
+
v: number;
|
|
17
|
+
type: string;
|
|
18
|
+
platform: string;
|
|
19
|
+
subject: string;
|
|
20
|
+
tag: string;
|
|
21
|
+
ts: string;
|
|
22
|
+
sig: string;
|
|
23
|
+
};
|
|
24
|
+
/** The subject signed it, and the tag derives from that subject at that platform. */
|
|
25
|
+
export declare function verifySubjectClaim(claim: unknown): boolean;
|
|
26
|
+
/** Zanii's decision payload, v1, for a government decision. No rulebook, no decision. */
|
|
27
|
+
export declare function decisionPayload(input: {
|
|
28
|
+
kind: string;
|
|
29
|
+
manifestHash: string;
|
|
30
|
+
outcome: Obj;
|
|
31
|
+
ts: string;
|
|
32
|
+
factors?: Obj;
|
|
33
|
+
appealBy?: string;
|
|
34
|
+
nonce?: string;
|
|
35
|
+
}): {
|
|
36
|
+
payload: Obj;
|
|
37
|
+
nonce: string;
|
|
38
|
+
};
|
|
39
|
+
/** The disclosed factors and nonce match the commitment made at decision time. */
|
|
40
|
+
export declare const verifyFactors: (factors: unknown, nonce: string, commitment: string) => boolean;
|
|
41
|
+
/** Zanii's structural check of a decision payload. */
|
|
42
|
+
export declare function verifyDecision(payload: unknown): {
|
|
43
|
+
ok: boolean;
|
|
44
|
+
reasons: string[];
|
|
45
|
+
};
|
|
46
|
+
/** Zanii's rulebook windows over time; interleaving is the red flag. */
|
|
47
|
+
export declare function ruleConsistency(payloads: readonly unknown[]): {
|
|
48
|
+
rulebooks: {
|
|
49
|
+
manifest_hash: string;
|
|
50
|
+
count: number;
|
|
51
|
+
first_ts: string;
|
|
52
|
+
last_ts: string;
|
|
53
|
+
}[];
|
|
54
|
+
consistent: boolean;
|
|
55
|
+
reasons: string[];
|
|
56
|
+
};
|
|
57
|
+
export interface AppealItem {
|
|
58
|
+
session_id: string;
|
|
59
|
+
seq: number;
|
|
60
|
+
line: string;
|
|
61
|
+
event_hash: string;
|
|
62
|
+
anchored: boolean;
|
|
63
|
+
decision: unknown;
|
|
64
|
+
}
|
|
65
|
+
/** spec/gov.md §3: the citizen's appeal pack over the decisions recorded about their tag. */
|
|
66
|
+
export declare function appealPack(items: readonly AppealItem[], opts: {
|
|
67
|
+
tag: string;
|
|
68
|
+
authority: string;
|
|
69
|
+
generated_at: string;
|
|
70
|
+
}): {
|
|
71
|
+
v: 1;
|
|
72
|
+
legal_basis: Text;
|
|
73
|
+
subject_tag: string;
|
|
74
|
+
authority: string;
|
|
75
|
+
decisions: number;
|
|
76
|
+
rules: {
|
|
77
|
+
rulebooks: {
|
|
78
|
+
manifest_hash: string;
|
|
79
|
+
count: number;
|
|
80
|
+
first_ts: string;
|
|
81
|
+
last_ts: string;
|
|
82
|
+
}[];
|
|
83
|
+
consistent: boolean;
|
|
84
|
+
reasons: string[];
|
|
85
|
+
};
|
|
86
|
+
unruled: number;
|
|
87
|
+
period: {
|
|
88
|
+
first: string | null;
|
|
89
|
+
last: string | null;
|
|
90
|
+
};
|
|
91
|
+
items: {
|
|
92
|
+
session_id: string;
|
|
93
|
+
seq: number;
|
|
94
|
+
line: string;
|
|
95
|
+
event_hash: string;
|
|
96
|
+
anchored: boolean;
|
|
97
|
+
}[];
|
|
98
|
+
steps: {
|
|
99
|
+
en: string;
|
|
100
|
+
ar: string;
|
|
101
|
+
n: number;
|
|
102
|
+
}[];
|
|
103
|
+
generated_at: string;
|
|
104
|
+
};
|
|
105
|
+
export type AppealPack = ReturnType<typeof appealPack>;
|
|
106
|
+
/** The reading copy for an administrative judge: `en`, `ar` or `both` (headers always bilingual). */
|
|
107
|
+
export declare function renderAppealPack(pack: AppealPack, lang?: "en" | "ar" | "both"): string;
|
|
108
|
+
export {};
|