@fidacy/mcp 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.js +21 -1
- package/dist/index.js +213 -3
- package/dist/lib.js +142 -3
- package/package.json +1 -1
package/dist/core.js
CHANGED
|
@@ -251,6 +251,10 @@ var DevFidacyCore = class {
|
|
|
251
251
|
return null;
|
|
252
252
|
return { record: record2, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
|
|
253
253
|
}
|
|
254
|
+
async history(limit = 50) {
|
|
255
|
+
const all = this.store.records();
|
|
256
|
+
return limit > 0 && all.length > limit ? all.slice(-limit) : all;
|
|
257
|
+
}
|
|
254
258
|
publicKey() {
|
|
255
259
|
return this.pubPem;
|
|
256
260
|
}
|
|
@@ -285,6 +289,10 @@ var HttpFidacyCore = class {
|
|
|
285
289
|
async getProof(decisionId) {
|
|
286
290
|
return this.call("/v1/audit/proof", { decisionId });
|
|
287
291
|
}
|
|
292
|
+
async history(limit = 50) {
|
|
293
|
+
const res = await this.call("/v1/audit/list", { limit });
|
|
294
|
+
return res.records ?? [];
|
|
295
|
+
}
|
|
288
296
|
publicKey() {
|
|
289
297
|
return this.subjectPub;
|
|
290
298
|
}
|
|
@@ -350,6 +358,9 @@ var FileAuditStore = class {
|
|
|
350
358
|
if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
|
|
351
359
|
record2.purpose = decision.request.purpose.slice(0, 500);
|
|
352
360
|
}
|
|
361
|
+
if (typeof decision.request?.payee === "string" && decision.request.payee.trim()) {
|
|
362
|
+
record2.payee = decision.request.payee.slice(0, 200);
|
|
363
|
+
}
|
|
353
364
|
if (decision.status === "ALLOW") {
|
|
354
365
|
const req = decision.request;
|
|
355
366
|
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
@@ -359,6 +370,14 @@ var FileAuditStore = class {
|
|
|
359
370
|
const invoice = canonInvoice(req?.invoiceRef);
|
|
360
371
|
if (invoice)
|
|
361
372
|
record2.invoiceRef = invoice;
|
|
373
|
+
} else {
|
|
374
|
+
if (decision.violatedRule)
|
|
375
|
+
record2.violatedRule = decision.violatedRule;
|
|
376
|
+
const req = decision.request;
|
|
377
|
+
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
378
|
+
record2.amount = req.amount;
|
|
379
|
+
if (typeof req?.currency === "string")
|
|
380
|
+
record2.currency = req.currency;
|
|
362
381
|
}
|
|
363
382
|
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
364
383
|
this.chain.push(record2);
|
|
@@ -442,7 +461,7 @@ function resolveMandateRules(cfg) {
|
|
|
442
461
|
}
|
|
443
462
|
|
|
444
463
|
// src/telemetry.ts
|
|
445
|
-
var CLIENT_VERSION = true ? "0.2.
|
|
464
|
+
var CLIENT_VERSION = true ? "0.2.3" : "dev";
|
|
446
465
|
function bandOf(amount) {
|
|
447
466
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
448
467
|
if (amount < 10) return "lt10";
|
|
@@ -596,6 +615,7 @@ function makeCore() {
|
|
|
596
615
|
return dev.decide(req, subject);
|
|
597
616
|
},
|
|
598
617
|
getProof: (decisionId) => dev.getProof(decisionId),
|
|
618
|
+
history: (limit) => dev.history(limit),
|
|
599
619
|
publicKey: () => dev.publicKey()
|
|
600
620
|
};
|
|
601
621
|
}
|
package/dist/index.js
CHANGED
|
@@ -258,6 +258,10 @@ var DevFidacyCore = class {
|
|
|
258
258
|
return null;
|
|
259
259
|
return { record: record2, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
|
|
260
260
|
}
|
|
261
|
+
async history(limit = 50) {
|
|
262
|
+
const all = this.store.records();
|
|
263
|
+
return limit > 0 && all.length > limit ? all.slice(-limit) : all;
|
|
264
|
+
}
|
|
261
265
|
publicKey() {
|
|
262
266
|
return this.pubPem;
|
|
263
267
|
}
|
|
@@ -292,6 +296,10 @@ var HttpFidacyCore = class {
|
|
|
292
296
|
async getProof(decisionId) {
|
|
293
297
|
return this.call("/v1/audit/proof", { decisionId });
|
|
294
298
|
}
|
|
299
|
+
async history(limit = 50) {
|
|
300
|
+
const res = await this.call("/v1/audit/list", { limit });
|
|
301
|
+
return res.records ?? [];
|
|
302
|
+
}
|
|
295
303
|
publicKey() {
|
|
296
304
|
return this.subjectPub;
|
|
297
305
|
}
|
|
@@ -357,6 +365,9 @@ var FileAuditStore = class {
|
|
|
357
365
|
if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
|
|
358
366
|
record2.purpose = decision.request.purpose.slice(0, 500);
|
|
359
367
|
}
|
|
368
|
+
if (typeof decision.request?.payee === "string" && decision.request.payee.trim()) {
|
|
369
|
+
record2.payee = decision.request.payee.slice(0, 200);
|
|
370
|
+
}
|
|
360
371
|
if (decision.status === "ALLOW") {
|
|
361
372
|
const req = decision.request;
|
|
362
373
|
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
@@ -366,6 +377,14 @@ var FileAuditStore = class {
|
|
|
366
377
|
const invoice = canonInvoice(req?.invoiceRef);
|
|
367
378
|
if (invoice)
|
|
368
379
|
record2.invoiceRef = invoice;
|
|
380
|
+
} else {
|
|
381
|
+
if (decision.violatedRule)
|
|
382
|
+
record2.violatedRule = decision.violatedRule;
|
|
383
|
+
const req = decision.request;
|
|
384
|
+
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
385
|
+
record2.amount = req.amount;
|
|
386
|
+
if (typeof req?.currency === "string")
|
|
387
|
+
record2.currency = req.currency;
|
|
369
388
|
}
|
|
370
389
|
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
371
390
|
this.chain.push(record2);
|
|
@@ -471,7 +490,7 @@ function resolveMandateRules(cfg) {
|
|
|
471
490
|
}
|
|
472
491
|
|
|
473
492
|
// src/telemetry.ts
|
|
474
|
-
var CLIENT_VERSION = true ? "0.2.
|
|
493
|
+
var CLIENT_VERSION = true ? "0.2.3" : "dev";
|
|
475
494
|
function bandOf(amount) {
|
|
476
495
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
477
496
|
if (amount < 10) return "lt10";
|
|
@@ -628,6 +647,7 @@ function makeCore() {
|
|
|
628
647
|
return dev.decide(req, subject2);
|
|
629
648
|
},
|
|
630
649
|
getProof: (decisionId) => dev.getProof(decisionId),
|
|
650
|
+
history: (limit) => dev.history(limit),
|
|
631
651
|
publicKey: () => dev.publicKey()
|
|
632
652
|
};
|
|
633
653
|
}
|
|
@@ -814,7 +834,7 @@ async function findArtifacts(sha2562, cfg) {
|
|
|
814
834
|
}
|
|
815
835
|
|
|
816
836
|
// src/provision.ts
|
|
817
|
-
var CLIENT_VERSION2 = true ? "0.2.
|
|
837
|
+
var CLIENT_VERSION2 = true ? "0.2.3" : "dev";
|
|
818
838
|
function provisionEnabled() {
|
|
819
839
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
820
840
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -985,11 +1005,124 @@ function activationBootLine() {
|
|
|
985
1005
|
return ` Anonymous trial: ${used}/${FREE_DECISIONS} free decisions used; after that the firewall requires a free API key.`;
|
|
986
1006
|
}
|
|
987
1007
|
|
|
1008
|
+
// src/reporting.ts
|
|
1009
|
+
function withinDays(records, days, now = Date.now()) {
|
|
1010
|
+
if (!Number.isFinite(days) || days <= 0) return [...records];
|
|
1011
|
+
const cutoff = now - days * 864e5;
|
|
1012
|
+
return records.filter((r) => {
|
|
1013
|
+
const t = Date.parse(r.ts);
|
|
1014
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
var addTo = (bucket, key, value) => {
|
|
1018
|
+
bucket[key] = (bucket[key] ?? 0) + value;
|
|
1019
|
+
};
|
|
1020
|
+
function summarize(records, days, now = Date.now()) {
|
|
1021
|
+
const rows = withinDays(records, days, now);
|
|
1022
|
+
const out = {
|
|
1023
|
+
window_days: days,
|
|
1024
|
+
decisions: rows.length,
|
|
1025
|
+
allowed: 0,
|
|
1026
|
+
denied: 0,
|
|
1027
|
+
allowed_totals: {},
|
|
1028
|
+
blocked_totals: {},
|
|
1029
|
+
denied_by_rule: {},
|
|
1030
|
+
payees_paid: [],
|
|
1031
|
+
amount_unknown: 0,
|
|
1032
|
+
first_ts: rows.length ? rows[0].ts : null,
|
|
1033
|
+
last_ts: rows.length ? rows[rows.length - 1].ts : null
|
|
1034
|
+
};
|
|
1035
|
+
const paid = /* @__PURE__ */ new Set();
|
|
1036
|
+
for (const r of rows) {
|
|
1037
|
+
const known = typeof r.amount === "number" && Number.isFinite(r.amount);
|
|
1038
|
+
if (!known) out.amount_unknown += 1;
|
|
1039
|
+
const ccy = r.currency ?? "unknown";
|
|
1040
|
+
if (r.status === "ALLOW") {
|
|
1041
|
+
out.allowed += 1;
|
|
1042
|
+
if (known) addTo(out.allowed_totals, ccy, r.amount);
|
|
1043
|
+
if (r.payee) paid.add(r.payee);
|
|
1044
|
+
} else {
|
|
1045
|
+
out.denied += 1;
|
|
1046
|
+
if (known) addTo(out.blocked_totals, ccy, r.amount);
|
|
1047
|
+
addTo(out.denied_by_rule, r.violatedRule ?? "not_recorded", 1);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
out.payees_paid = [...paid].sort();
|
|
1051
|
+
return out;
|
|
1052
|
+
}
|
|
1053
|
+
var money = (totals) => {
|
|
1054
|
+
const parts = Object.entries(totals).map(([ccy, v]) => `${v.toLocaleString("en-US")} ${ccy}`);
|
|
1055
|
+
return parts.length ? parts.join(", ") : "0";
|
|
1056
|
+
};
|
|
1057
|
+
function renderSummary(s) {
|
|
1058
|
+
if (s.decisions === 0) {
|
|
1059
|
+
return `No agent payment decisions in the last ${s.window_days} day(s). The firewall is active and has nothing to report, which is the expected state until an agent attempts a payment.`;
|
|
1060
|
+
}
|
|
1061
|
+
const lines = [
|
|
1062
|
+
`Last ${s.window_days} day(s): ${s.decisions} payment decision(s), ${s.allowed} allowed and ${s.denied} blocked before execution.`,
|
|
1063
|
+
`Paid: ${money(s.allowed_totals)}${s.payees_paid.length ? ` across ${s.payees_paid.length} payee(s): ${s.payees_paid.slice(0, 8).join(", ")}${s.payees_paid.length > 8 ? ", \u2026" : ""}` : ""}.`
|
|
1064
|
+
];
|
|
1065
|
+
if (s.denied > 0) {
|
|
1066
|
+
const ranked = Object.entries(s.denied_by_rule).sort((a, b) => b[1] - a[1]);
|
|
1067
|
+
lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By rule: ${ranked.map(([rule, n]) => `${rule} (${n})`).join(", ")}.`);
|
|
1068
|
+
}
|
|
1069
|
+
if (s.amount_unknown > 0) {
|
|
1070
|
+
lines.push(`${s.amount_unknown} record(s) in this window predate amount logging, so the totals above are a floor, not the exact figure.`);
|
|
1071
|
+
}
|
|
1072
|
+
return lines.join("\n");
|
|
1073
|
+
}
|
|
1074
|
+
function toRows(records, status, limit) {
|
|
1075
|
+
const filtered = status === "all" ? [...records] : records.filter((r) => r.status === status);
|
|
1076
|
+
return filtered.slice(-Math.max(1, limit)).reverse().map((r) => ({
|
|
1077
|
+
seq: r.seq,
|
|
1078
|
+
decisionId: r.decisionId,
|
|
1079
|
+
status: r.status,
|
|
1080
|
+
ts: r.ts,
|
|
1081
|
+
payee: r.payee,
|
|
1082
|
+
amount: r.amount,
|
|
1083
|
+
currency: r.currency,
|
|
1084
|
+
purpose: r.purpose,
|
|
1085
|
+
violatedRule: r.violatedRule,
|
|
1086
|
+
invoiceRef: r.invoiceRef
|
|
1087
|
+
}));
|
|
1088
|
+
}
|
|
1089
|
+
function renderRows(rows) {
|
|
1090
|
+
if (!rows.length) return "No decisions match that filter yet.";
|
|
1091
|
+
return rows.map((r) => {
|
|
1092
|
+
const amount = typeof r.amount === "number" ? `${r.amount.toLocaleString("en-US")} ${r.currency ?? ""}`.trim() : "amount not recorded";
|
|
1093
|
+
const who = r.payee ?? "payee not recorded";
|
|
1094
|
+
const why = r.status === "DENY" ? ` blocked by ${r.violatedRule ?? "a rule not recorded on this older entry"}.` : "";
|
|
1095
|
+
const said = r.purpose ? ` Agent's stated reason: "${r.purpose}".` : "";
|
|
1096
|
+
return `#${r.seq} ${r.ts} ${r.status} ${amount} to ${who}.${why}${said} (decision ${r.decisionId})`;
|
|
1097
|
+
}).join("\n");
|
|
1098
|
+
}
|
|
1099
|
+
function explainRule(rule, payee) {
|
|
1100
|
+
if (!rule) return "This entry predates readable rule logging, so the specific rule is not on the record. The decision payload is still covered by the chain digest.";
|
|
1101
|
+
if (rule.startsWith("payee_not_in_allowlist"))
|
|
1102
|
+
return `The payee${payee ? ` "${payee}"` : ""} is not on the mandate's allowlist. This is the deny-by-default rule doing its job: an agent can only pay counterparties a human put on the list, which is what stops a swapped or lookalike vendor.`;
|
|
1103
|
+
if (rule.startsWith("category_not_allowed"))
|
|
1104
|
+
return "The stated purpose category is not one the mandate permits for this agent.";
|
|
1105
|
+
if (rule.startsWith("currency_not_allowed"))
|
|
1106
|
+
return "The payment currency is not the mandate's currency.";
|
|
1107
|
+
if (rule.startsWith("per_tx") || rule.includes("perTxMax"))
|
|
1108
|
+
return "The amount exceeds the per-transaction ceiling in the mandate.";
|
|
1109
|
+
if (rule.includes("maxTotal") || rule.startsWith("budget"))
|
|
1110
|
+
return "The payment would push cumulative spend past the mandate's total budget for the window.";
|
|
1111
|
+
if (rule.startsWith("invoice") || rule.includes("duplicate"))
|
|
1112
|
+
return "This invoice was already paid once. One payment per invoice is enforced regardless of amount, which is the control that stops a re-presented invoice at a higher figure.";
|
|
1113
|
+
if (rule.startsWith("window") || rule.includes("expired"))
|
|
1114
|
+
return "The mandate was outside its validity window at the moment of the request.";
|
|
1115
|
+
if (rule.startsWith("revoked")) return "The mandate had been revoked.";
|
|
1116
|
+
if (rule.startsWith("activation_required"))
|
|
1117
|
+
return "The anonymous trial had been used up, so the firewall failed closed. No money can move until the install is activated with a free API key.";
|
|
1118
|
+
return `The mandate rule "${rule}" was violated.`;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
988
1121
|
// src/index.ts
|
|
989
1122
|
var state = ensureState();
|
|
990
1123
|
var core = makeCore();
|
|
991
1124
|
var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
|
|
992
|
-
var SERVER_VERSION = true ? "0.2.
|
|
1125
|
+
var SERVER_VERSION = true ? "0.2.3" : "dev";
|
|
993
1126
|
var bootClaim = claimUrl();
|
|
994
1127
|
var server = new McpServer(
|
|
995
1128
|
{ name: "fidacy", version: SERVER_VERSION },
|
|
@@ -1074,6 +1207,83 @@ server.registerTool(
|
|
|
1074
1207
|
return { content: [{ type: "text", text }], structuredContent: proof };
|
|
1075
1208
|
}
|
|
1076
1209
|
);
|
|
1210
|
+
server.registerTool(
|
|
1211
|
+
"spend_summary",
|
|
1212
|
+
{
|
|
1213
|
+
title: "Spend Summary (what my agents spent and what was blocked)",
|
|
1214
|
+
description: "Operator report over a time window: how many payment decisions were made, how much was paid and to whom, how much was blocked before executing, and which mandate rules did the blocking. Read-only, works offline against the local audit chain. Use this to answer questions like 'what did my agents spend this week' or 'has anything been blocked'.",
|
|
1215
|
+
inputSchema: {
|
|
1216
|
+
days: z.number().int().positive().max(365).optional().describe("Window in days, counting back from now. Default 7.")
|
|
1217
|
+
},
|
|
1218
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1219
|
+
},
|
|
1220
|
+
async ({ days }) => {
|
|
1221
|
+
const window = days ?? 7;
|
|
1222
|
+
const records = await core.history(5e3);
|
|
1223
|
+
const summary = summarize(records, window);
|
|
1224
|
+
const claim = process.env.FIDACY_ENGINE_API_KEY ? null : claimUrl();
|
|
1225
|
+
const claimLine = claim ? `
|
|
1226
|
+
|
|
1227
|
+
For your operator: this report is built from the local chain on this machine. A free account keeps it across machines and anchors it to Bitcoin, and this install's history migrates: ${claim}` : "";
|
|
1228
|
+
return {
|
|
1229
|
+
content: [{ type: "text", text: renderSummary(summary) + claimLine }],
|
|
1230
|
+
structuredContent: summary
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
);
|
|
1234
|
+
server.registerTool(
|
|
1235
|
+
"list_decisions",
|
|
1236
|
+
{
|
|
1237
|
+
title: "List Recent Payment Decisions",
|
|
1238
|
+
description: "List recent agent payment decisions, newest first, with payee, amount, the agent's stated reason, and for blocked ones the rule that stopped it. Filter by status to answer 'what got blocked' directly. Read-only, works offline.",
|
|
1239
|
+
inputSchema: {
|
|
1240
|
+
limit: z.number().int().positive().max(200).optional().describe("How many decisions to return. Default 20."),
|
|
1241
|
+
status: z.enum(["ALLOW", "DENY", "all"]).optional().describe("Filter by outcome. Default all.")
|
|
1242
|
+
},
|
|
1243
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1244
|
+
},
|
|
1245
|
+
async ({ limit, status }) => {
|
|
1246
|
+
const records = await core.history(5e3);
|
|
1247
|
+
const rows = toRows(records, status ?? "all", limit ?? 20);
|
|
1248
|
+
return {
|
|
1249
|
+
content: [{ type: "text", text: renderRows(rows) }],
|
|
1250
|
+
structuredContent: { count: rows.length, decisions: rows }
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
);
|
|
1254
|
+
server.registerTool(
|
|
1255
|
+
"explain_decision",
|
|
1256
|
+
{
|
|
1257
|
+
title: "Explain a Decision (why was this allowed or blocked)",
|
|
1258
|
+
description: "Explain one decision in plain language: what was requested, what the mandate did about it, why, and the tamper-evident proof for it. Use when an operator asks why a specific payment was blocked.",
|
|
1259
|
+
inputSchema: { decisionId: z.string().describe("Decision id from request_payment, list_decisions or spend_summary") },
|
|
1260
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1261
|
+
},
|
|
1262
|
+
async ({ decisionId }) => {
|
|
1263
|
+
const records = await core.history(5e3);
|
|
1264
|
+
const record2 = records.find((r) => r.decisionId === decisionId);
|
|
1265
|
+
if (!record2) {
|
|
1266
|
+
return {
|
|
1267
|
+
content: [{ type: "text", text: `No decision ${decisionId} on this chain. Run list_decisions to see the ids that exist here.` }],
|
|
1268
|
+
isError: true
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
const proof = await core.getProof(decisionId);
|
|
1272
|
+
const amount = typeof record2.amount === "number" ? `${record2.amount.toLocaleString("en-US")} ${record2.currency ?? ""}`.trim() : "an amount that was not recorded";
|
|
1273
|
+
const head = record2.status === "ALLOW" ? `Decision ${decisionId} ALLOWED a payment of ${amount}${record2.payee ? ` to ${record2.payee}` : ""} at ${record2.ts}. It was inside every rule of the active mandate, and a signed grant was issued so a downstream executor could settle it.` : `Decision ${decisionId} BLOCKED a payment of ${amount}${record2.payee ? ` to ${record2.payee}` : ""} at ${record2.ts}. No money moved and no grant was issued.`;
|
|
1274
|
+
const why = record2.status === "DENY" ? `
|
|
1275
|
+
|
|
1276
|
+
Why: ${explainRule(record2.violatedRule, record2.payee)}` : "";
|
|
1277
|
+
const said = record2.purpose ? `
|
|
1278
|
+
|
|
1279
|
+
The agent's stated reason at the time: "${record2.purpose}".` : "";
|
|
1280
|
+
const integrity = proof ? `
|
|
1281
|
+
|
|
1282
|
+
Proof: entry #${record2.seq} in the hash-chained audit, chain currently ${proof.chainIntact ? "intact" : "BROKEN (records were altered or removed)"}. Call get_audit_proof for the full portable proof.` : "";
|
|
1283
|
+
const payload = { record: record2, explanation: explainRule(record2.violatedRule, record2.payee), chainIntact: proof?.chainIntact ?? null };
|
|
1284
|
+
return { content: [{ type: "text", text: head + why + said + integrity }], structuredContent: payload };
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1077
1287
|
server.registerTool(
|
|
1078
1288
|
"assess_action",
|
|
1079
1289
|
{
|
package/dist/lib.js
CHANGED
|
@@ -289,6 +289,10 @@ var DevFidacyCore = class {
|
|
|
289
289
|
return null;
|
|
290
290
|
return { record: record2, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
|
|
291
291
|
}
|
|
292
|
+
async history(limit = 50) {
|
|
293
|
+
const all = this.store.records();
|
|
294
|
+
return limit > 0 && all.length > limit ? all.slice(-limit) : all;
|
|
295
|
+
}
|
|
292
296
|
publicKey() {
|
|
293
297
|
return this.pubPem;
|
|
294
298
|
}
|
|
@@ -323,6 +327,10 @@ var HttpFidacyCore = class {
|
|
|
323
327
|
async getProof(decisionId) {
|
|
324
328
|
return this.call("/v1/audit/proof", { decisionId });
|
|
325
329
|
}
|
|
330
|
+
async history(limit = 50) {
|
|
331
|
+
const res = await this.call("/v1/audit/list", { limit });
|
|
332
|
+
return res.records ?? [];
|
|
333
|
+
}
|
|
326
334
|
publicKey() {
|
|
327
335
|
return this.subjectPub;
|
|
328
336
|
}
|
|
@@ -388,6 +396,9 @@ var FileAuditStore = class {
|
|
|
388
396
|
if (typeof decision.request?.purpose === "string" && decision.request.purpose.trim()) {
|
|
389
397
|
record2.purpose = decision.request.purpose.slice(0, 500);
|
|
390
398
|
}
|
|
399
|
+
if (typeof decision.request?.payee === "string" && decision.request.payee.trim()) {
|
|
400
|
+
record2.payee = decision.request.payee.slice(0, 200);
|
|
401
|
+
}
|
|
391
402
|
if (decision.status === "ALLOW") {
|
|
392
403
|
const req = decision.request;
|
|
393
404
|
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
@@ -397,6 +408,14 @@ var FileAuditStore = class {
|
|
|
397
408
|
const invoice = canonInvoice(req?.invoiceRef);
|
|
398
409
|
if (invoice)
|
|
399
410
|
record2.invoiceRef = invoice;
|
|
411
|
+
} else {
|
|
412
|
+
if (decision.violatedRule)
|
|
413
|
+
record2.violatedRule = decision.violatedRule;
|
|
414
|
+
const req = decision.request;
|
|
415
|
+
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
416
|
+
record2.amount = req.amount;
|
|
417
|
+
if (typeof req?.currency === "string")
|
|
418
|
+
record2.currency = req.currency;
|
|
400
419
|
}
|
|
401
420
|
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
402
421
|
this.chain.push(record2);
|
|
@@ -594,7 +613,7 @@ function resolveMandateRules(cfg) {
|
|
|
594
613
|
}
|
|
595
614
|
|
|
596
615
|
// src/telemetry.ts
|
|
597
|
-
var CLIENT_VERSION = true ? "0.2.
|
|
616
|
+
var CLIENT_VERSION = true ? "0.2.3" : "dev";
|
|
598
617
|
function bandOf(amount) {
|
|
599
618
|
if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
|
|
600
619
|
if (amount < 10) return "lt10";
|
|
@@ -754,6 +773,7 @@ function makeCore() {
|
|
|
754
773
|
return dev.decide(req, subject);
|
|
755
774
|
},
|
|
756
775
|
getProof: (decisionId) => dev.getProof(decisionId),
|
|
776
|
+
history: (limit) => dev.history(limit),
|
|
757
777
|
publicKey: () => dev.publicKey()
|
|
758
778
|
};
|
|
759
779
|
}
|
|
@@ -771,7 +791,7 @@ function requestUpgrade() {
|
|
|
771
791
|
}
|
|
772
792
|
|
|
773
793
|
// src/provision.ts
|
|
774
|
-
var CLIENT_VERSION2 = true ? "0.2.
|
|
794
|
+
var CLIENT_VERSION2 = true ? "0.2.3" : "dev";
|
|
775
795
|
function provisionEnabled() {
|
|
776
796
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
777
797
|
return !(v === "1" || v === "true" || v === "yes");
|
|
@@ -920,6 +940,119 @@ function activationBootLine() {
|
|
|
920
940
|
}
|
|
921
941
|
return ` Anonymous trial: ${used}/${FREE_DECISIONS} free decisions used; after that the firewall requires a free API key.`;
|
|
922
942
|
}
|
|
943
|
+
|
|
944
|
+
// src/reporting.ts
|
|
945
|
+
function withinDays(records, days, now = Date.now()) {
|
|
946
|
+
if (!Number.isFinite(days) || days <= 0) return [...records];
|
|
947
|
+
const cutoff = now - days * 864e5;
|
|
948
|
+
return records.filter((r) => {
|
|
949
|
+
const t = Date.parse(r.ts);
|
|
950
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
var addTo = (bucket, key, value) => {
|
|
954
|
+
bucket[key] = (bucket[key] ?? 0) + value;
|
|
955
|
+
};
|
|
956
|
+
function summarize(records, days, now = Date.now()) {
|
|
957
|
+
const rows = withinDays(records, days, now);
|
|
958
|
+
const out = {
|
|
959
|
+
window_days: days,
|
|
960
|
+
decisions: rows.length,
|
|
961
|
+
allowed: 0,
|
|
962
|
+
denied: 0,
|
|
963
|
+
allowed_totals: {},
|
|
964
|
+
blocked_totals: {},
|
|
965
|
+
denied_by_rule: {},
|
|
966
|
+
payees_paid: [],
|
|
967
|
+
amount_unknown: 0,
|
|
968
|
+
first_ts: rows.length ? rows[0].ts : null,
|
|
969
|
+
last_ts: rows.length ? rows[rows.length - 1].ts : null
|
|
970
|
+
};
|
|
971
|
+
const paid = /* @__PURE__ */ new Set();
|
|
972
|
+
for (const r of rows) {
|
|
973
|
+
const known = typeof r.amount === "number" && Number.isFinite(r.amount);
|
|
974
|
+
if (!known) out.amount_unknown += 1;
|
|
975
|
+
const ccy = r.currency ?? "unknown";
|
|
976
|
+
if (r.status === "ALLOW") {
|
|
977
|
+
out.allowed += 1;
|
|
978
|
+
if (known) addTo(out.allowed_totals, ccy, r.amount);
|
|
979
|
+
if (r.payee) paid.add(r.payee);
|
|
980
|
+
} else {
|
|
981
|
+
out.denied += 1;
|
|
982
|
+
if (known) addTo(out.blocked_totals, ccy, r.amount);
|
|
983
|
+
addTo(out.denied_by_rule, r.violatedRule ?? "not_recorded", 1);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
out.payees_paid = [...paid].sort();
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
989
|
+
var money = (totals) => {
|
|
990
|
+
const parts = Object.entries(totals).map(([ccy, v]) => `${v.toLocaleString("en-US")} ${ccy}`);
|
|
991
|
+
return parts.length ? parts.join(", ") : "0";
|
|
992
|
+
};
|
|
993
|
+
function renderSummary(s) {
|
|
994
|
+
if (s.decisions === 0) {
|
|
995
|
+
return `No agent payment decisions in the last ${s.window_days} day(s). The firewall is active and has nothing to report, which is the expected state until an agent attempts a payment.`;
|
|
996
|
+
}
|
|
997
|
+
const lines = [
|
|
998
|
+
`Last ${s.window_days} day(s): ${s.decisions} payment decision(s), ${s.allowed} allowed and ${s.denied} blocked before execution.`,
|
|
999
|
+
`Paid: ${money(s.allowed_totals)}${s.payees_paid.length ? ` across ${s.payees_paid.length} payee(s): ${s.payees_paid.slice(0, 8).join(", ")}${s.payees_paid.length > 8 ? ", \u2026" : ""}` : ""}.`
|
|
1000
|
+
];
|
|
1001
|
+
if (s.denied > 0) {
|
|
1002
|
+
const ranked = Object.entries(s.denied_by_rule).sort((a, b) => b[1] - a[1]);
|
|
1003
|
+
lines.push(`Blocked: ${money(s.blocked_totals)} in attempted value. By rule: ${ranked.map(([rule, n]) => `${rule} (${n})`).join(", ")}.`);
|
|
1004
|
+
}
|
|
1005
|
+
if (s.amount_unknown > 0) {
|
|
1006
|
+
lines.push(`${s.amount_unknown} record(s) in this window predate amount logging, so the totals above are a floor, not the exact figure.`);
|
|
1007
|
+
}
|
|
1008
|
+
return lines.join("\n");
|
|
1009
|
+
}
|
|
1010
|
+
function toRows(records, status, limit) {
|
|
1011
|
+
const filtered = status === "all" ? [...records] : records.filter((r) => r.status === status);
|
|
1012
|
+
return filtered.slice(-Math.max(1, limit)).reverse().map((r) => ({
|
|
1013
|
+
seq: r.seq,
|
|
1014
|
+
decisionId: r.decisionId,
|
|
1015
|
+
status: r.status,
|
|
1016
|
+
ts: r.ts,
|
|
1017
|
+
payee: r.payee,
|
|
1018
|
+
amount: r.amount,
|
|
1019
|
+
currency: r.currency,
|
|
1020
|
+
purpose: r.purpose,
|
|
1021
|
+
violatedRule: r.violatedRule,
|
|
1022
|
+
invoiceRef: r.invoiceRef
|
|
1023
|
+
}));
|
|
1024
|
+
}
|
|
1025
|
+
function renderRows(rows) {
|
|
1026
|
+
if (!rows.length) return "No decisions match that filter yet.";
|
|
1027
|
+
return rows.map((r) => {
|
|
1028
|
+
const amount = typeof r.amount === "number" ? `${r.amount.toLocaleString("en-US")} ${r.currency ?? ""}`.trim() : "amount not recorded";
|
|
1029
|
+
const who = r.payee ?? "payee not recorded";
|
|
1030
|
+
const why = r.status === "DENY" ? ` blocked by ${r.violatedRule ?? "a rule not recorded on this older entry"}.` : "";
|
|
1031
|
+
const said = r.purpose ? ` Agent's stated reason: "${r.purpose}".` : "";
|
|
1032
|
+
return `#${r.seq} ${r.ts} ${r.status} ${amount} to ${who}.${why}${said} (decision ${r.decisionId})`;
|
|
1033
|
+
}).join("\n");
|
|
1034
|
+
}
|
|
1035
|
+
function explainRule(rule, payee) {
|
|
1036
|
+
if (!rule) return "This entry predates readable rule logging, so the specific rule is not on the record. The decision payload is still covered by the chain digest.";
|
|
1037
|
+
if (rule.startsWith("payee_not_in_allowlist"))
|
|
1038
|
+
return `The payee${payee ? ` "${payee}"` : ""} is not on the mandate's allowlist. This is the deny-by-default rule doing its job: an agent can only pay counterparties a human put on the list, which is what stops a swapped or lookalike vendor.`;
|
|
1039
|
+
if (rule.startsWith("category_not_allowed"))
|
|
1040
|
+
return "The stated purpose category is not one the mandate permits for this agent.";
|
|
1041
|
+
if (rule.startsWith("currency_not_allowed"))
|
|
1042
|
+
return "The payment currency is not the mandate's currency.";
|
|
1043
|
+
if (rule.startsWith("per_tx") || rule.includes("perTxMax"))
|
|
1044
|
+
return "The amount exceeds the per-transaction ceiling in the mandate.";
|
|
1045
|
+
if (rule.includes("maxTotal") || rule.startsWith("budget"))
|
|
1046
|
+
return "The payment would push cumulative spend past the mandate's total budget for the window.";
|
|
1047
|
+
if (rule.startsWith("invoice") || rule.includes("duplicate"))
|
|
1048
|
+
return "This invoice was already paid once. One payment per invoice is enforced regardless of amount, which is the control that stops a re-presented invoice at a higher figure.";
|
|
1049
|
+
if (rule.startsWith("window") || rule.includes("expired"))
|
|
1050
|
+
return "The mandate was outside its validity window at the moment of the request.";
|
|
1051
|
+
if (rule.startsWith("revoked")) return "The mandate had been revoked.";
|
|
1052
|
+
if (rule.startsWith("activation_required"))
|
|
1053
|
+
return "The anonymous trial had been used up, so the firewall failed closed. No money can move until the install is activated with a free API key.";
|
|
1054
|
+
return `The mandate rule "${rule}" was violated.`;
|
|
1055
|
+
}
|
|
923
1056
|
export {
|
|
924
1057
|
DevFidacyCore,
|
|
925
1058
|
FREE_DECISIONS,
|
|
@@ -935,6 +1068,7 @@ export {
|
|
|
935
1068
|
decisionNudge,
|
|
936
1069
|
ensureState,
|
|
937
1070
|
evaluate,
|
|
1071
|
+
explainRule,
|
|
938
1072
|
hasEngineKey,
|
|
939
1073
|
httpRedeem,
|
|
940
1074
|
installAgeDays,
|
|
@@ -948,16 +1082,21 @@ export {
|
|
|
948
1082
|
recordAgentActive,
|
|
949
1083
|
recordInstall,
|
|
950
1084
|
remainingFree,
|
|
1085
|
+
renderRows,
|
|
1086
|
+
renderSummary,
|
|
951
1087
|
requestUpgrade,
|
|
952
1088
|
requireHttpsBase,
|
|
953
1089
|
setTelemetryShell,
|
|
954
1090
|
sha256,
|
|
955
1091
|
sign,
|
|
956
1092
|
stableStringify,
|
|
1093
|
+
summarize,
|
|
1094
|
+
toRows,
|
|
957
1095
|
trialCountdownLine,
|
|
958
1096
|
upgradeUrl,
|
|
959
1097
|
validateRequest,
|
|
960
1098
|
verify,
|
|
961
1099
|
verifyGrant,
|
|
962
|
-
weekOneBootNudge
|
|
1100
|
+
weekOneBootNudge,
|
|
1101
|
+
withinDays
|
|
963
1102
|
};
|
package/package.json
CHANGED