@fidacy/mcp 0.1.5 → 0.1.6
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/assess.js +110 -129
- package/dist/core.js +406 -147
- package/dist/index.js +718 -105
- package/dist/lib.js +527 -5
- package/package.json +4 -7
- package/dist/assess.d.ts +0 -78
- package/dist/assess.js.map +0 -1
- package/dist/audit-store.d.ts +0 -11
- package/dist/audit-store.js +0 -51
- package/dist/audit-store.js.map +0 -1
- package/dist/core.d.ts +0 -34
- package/dist/core.js.map +0 -1
- package/dist/executor.d.ts +0 -30
- package/dist/executor.js +0 -64
- package/dist/executor.js.map +0 -1
- package/dist/grant.d.ts +0 -20
- package/dist/grant.js +0 -41
- package/dist/grant.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
- package/dist/lib.d.ts +0 -4
- package/dist/lib.js.map +0 -1
- package/dist/signing.d.ts +0 -10
- package/dist/signing.js +0 -32
- package/dist/signing.js.map +0 -1
- package/dist/types.d.ts +0 -52
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/util.d.ts +0 -1
- package/dist/util.js +0 -9
- package/dist/util.js.map +0 -1
package/dist/core.js
CHANGED
|
@@ -1,157 +1,416 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return "mandate_revoked";
|
|
10
|
-
if (now < Date.parse(mandate.window.notBefore))
|
|
11
|
-
return "before_mandate_window";
|
|
12
|
-
if (now > Date.parse(mandate.window.notAfter))
|
|
13
|
-
return "after_mandate_window";
|
|
14
|
-
if (req.currency !== mandate.allow.currency)
|
|
15
|
-
return `currency_not_allowed:${req.currency}`;
|
|
16
|
-
if (req.amount <= 0)
|
|
17
|
-
return "non_positive_amount";
|
|
18
|
-
if (req.amount > mandate.allow.perTxMax)
|
|
19
|
-
return `per_tx_cap_exceeded:${req.amount}>${mandate.allow.perTxMax}`;
|
|
20
|
-
if (spentSoFar + req.amount > mandate.allow.maxTotal)
|
|
21
|
-
return `total_cap_exceeded:${spentSoFar + req.amount}>${mandate.allow.maxTotal}`;
|
|
22
|
-
const payeeOk = mandate.allow.payees.includes("*") || mandate.allow.payees.includes(req.payee);
|
|
23
|
-
if (!payeeOk)
|
|
24
|
-
return `payee_not_in_allowlist:${req.payee}`;
|
|
25
|
-
const catOk = mandate.allow.categories.includes("*") || mandate.allow.categories.includes(req.category);
|
|
26
|
-
if (!catOk)
|
|
27
|
-
return `category_not_allowed:${req.category}`;
|
|
28
|
-
return null;
|
|
1
|
+
// ../firewall/dist/util.js
|
|
2
|
+
function stableStringify(obj) {
|
|
3
|
+
if (obj === null || typeof obj !== "object")
|
|
4
|
+
return JSON.stringify(obj);
|
|
5
|
+
if (Array.isArray(obj))
|
|
6
|
+
return "[" + obj.map(stableStringify).join(",") + "]";
|
|
7
|
+
const keys = Object.keys(obj).sort();
|
|
8
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
29
9
|
}
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (kp.ephemeral)
|
|
44
|
-
console.error("[fidacy] dev mode: ephemeral signing key. Production: set FIDACY_SIGNING_KEY_B64, or FIDACY_MODE=http + FIDACY_API_URL + FIDACY_API_KEY to use the live core.");
|
|
45
|
-
this.mandate = this.loadMandate();
|
|
46
|
-
this.store = new FileAuditStore(process.env.FIDACY_AUDIT_PATH ?? "./fidacy-audit.log");
|
|
47
|
-
}
|
|
48
|
-
loadMandate() {
|
|
49
|
-
if (process.env.FIDACY_MANDATE_JSON)
|
|
50
|
-
return JSON.parse(process.env.FIDACY_MANDATE_JSON);
|
|
51
|
-
const subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
|
|
52
|
-
return {
|
|
53
|
-
id: "mandate:demo",
|
|
54
|
-
subject,
|
|
55
|
-
version: "ap2.v0.2.0",
|
|
56
|
-
allow: {
|
|
57
|
-
payees: (process.env.FIDACY_ALLOW_PAYEES ?? "supplier:acme,supplier:globex").split(","),
|
|
58
|
-
categories: (process.env.FIDACY_ALLOW_CATEGORIES ?? "invoice,subscription").split(","),
|
|
59
|
-
currency: process.env.FIDACY_CURRENCY ?? "USD",
|
|
60
|
-
maxTotal: Number(process.env.FIDACY_MAX_TOTAL ?? 10000),
|
|
61
|
-
perTxMax: Number(process.env.FIDACY_PER_TX_MAX ?? 2500),
|
|
62
|
-
},
|
|
63
|
-
window: {
|
|
64
|
-
notBefore: process.env.FIDACY_NOT_BEFORE ?? new Date(Date.now() - 3600_000).toISOString(),
|
|
65
|
-
notAfter: process.env.FIDACY_NOT_AFTER ?? new Date(Date.now() + 30 * 86400_000).toISOString(),
|
|
66
|
-
},
|
|
67
|
-
revoked: false,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
async getMandate() {
|
|
71
|
-
return this.mandate;
|
|
72
|
-
}
|
|
73
|
-
async decide(req, subject) {
|
|
74
|
-
const decisionId = randomUUID();
|
|
75
|
-
const ts = new Date().toISOString();
|
|
76
|
-
const violated = evaluate(this.mandate, req, this.spent);
|
|
77
|
-
if (violated) {
|
|
78
|
-
const decision = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: violated, ts };
|
|
79
|
-
this.store.append(decision);
|
|
80
|
-
return decision;
|
|
81
|
-
}
|
|
82
|
-
// Invoice-anchored dedup. When the request carries an invoiceRef, the firewall
|
|
83
|
-
// enforces one ALLOW per (subject, invoiceRef): a second authorization for the
|
|
84
|
-
// same invoice, at ANY amount and with ANY idempotency key, is DENIED here.
|
|
85
|
-
// This is stateful, so it runs after the pure mandate evaluation, not inside it.
|
|
86
|
-
if (req.invoiceRef) {
|
|
87
|
-
const k = `${subject}|${req.invoiceRef}`;
|
|
88
|
-
if (this.claimedInvoices.has(k)) {
|
|
89
|
-
const decision = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${req.invoiceRef}`, ts };
|
|
90
|
-
this.store.append(decision);
|
|
91
|
-
return decision;
|
|
92
|
-
}
|
|
93
|
-
this.claimedInvoices.add(k);
|
|
94
|
-
}
|
|
95
|
-
const grantPayload = { decisionId, subject, payee: req.payee, amount: req.amount, currency: req.currency, exp: Date.now() + 120_000, ...(req.invoiceRef ? { invoiceRef: req.invoiceRef } : {}) };
|
|
96
|
-
const grantBody = Buffer.from(stableStringify(grantPayload), "utf8").toString("base64url");
|
|
97
|
-
const grant = `${grantBody}.${sign(this.priv, grantBody)}`;
|
|
98
|
-
const decision = { decisionId, status: "ALLOW", subject, mandateId: this.mandate.id, request: req, grant, ts };
|
|
99
|
-
this.spent += req.amount;
|
|
100
|
-
this.store.append(decision);
|
|
101
|
-
return decision;
|
|
102
|
-
}
|
|
103
|
-
async getProof(decisionId) {
|
|
104
|
-
const record = this.store.find(decisionId);
|
|
105
|
-
if (!record)
|
|
106
|
-
return null;
|
|
107
|
-
return { record, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
|
|
108
|
-
}
|
|
109
|
-
publicKey() {
|
|
110
|
-
return this.pubPem;
|
|
111
|
-
}
|
|
10
|
+
|
|
11
|
+
// ../firewall/dist/signing.js
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
function loadOrGenerateKeyPair() {
|
|
14
|
+
const b64 = process.env.FIDACY_SIGNING_KEY_B64;
|
|
15
|
+
if (b64) {
|
|
16
|
+
const pem = Buffer.from(b64, "base64").toString("utf8");
|
|
17
|
+
const privateKey2 = crypto.createPrivateKey(pem);
|
|
18
|
+
const publicKey2 = crypto.createPublicKey(privateKey2);
|
|
19
|
+
return { privateKey: privateKey2, publicKey: publicKey2, ephemeral: false };
|
|
20
|
+
}
|
|
21
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
|
|
22
|
+
return { privateKey, publicKey, ephemeral: true };
|
|
112
23
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
24
|
+
function publicKeyPem(publicKey) {
|
|
25
|
+
return publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
26
|
+
}
|
|
27
|
+
function sign(privateKey, message) {
|
|
28
|
+
return crypto.sign(null, Buffer.from(message, "utf8"), privateKey).toString("base64url");
|
|
29
|
+
}
|
|
30
|
+
function sha256(input) {
|
|
31
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ../firewall/dist/audit-store.js
|
|
35
|
+
import fs from "node:fs";
|
|
36
|
+
var FileAuditStore = class {
|
|
37
|
+
path;
|
|
38
|
+
chain = [];
|
|
39
|
+
constructor(path) {
|
|
40
|
+
this.path = path;
|
|
41
|
+
this.load();
|
|
42
|
+
}
|
|
43
|
+
load() {
|
|
44
|
+
if (!fs.existsSync(this.path))
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
const raw = fs.readFileSync(this.path, "utf8");
|
|
48
|
+
const parsed = [];
|
|
49
|
+
for (const line of raw.split("\n")) {
|
|
50
|
+
if (!line.trim())
|
|
51
|
+
continue;
|
|
52
|
+
parsed.push(JSON.parse(line));
|
|
53
|
+
}
|
|
54
|
+
this.chain = parsed;
|
|
55
|
+
if (!this.intact())
|
|
56
|
+
throw new Error("audit chain integrity broken");
|
|
57
|
+
} catch (err) {
|
|
58
|
+
this.chain = [];
|
|
59
|
+
try {
|
|
60
|
+
const quarantine = `${this.path}.corrupt-${Date.now()}`;
|
|
61
|
+
fs.renameSync(this.path, quarantine);
|
|
62
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
|
|
63
|
+
} catch {
|
|
64
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
|
|
65
|
+
}
|
|
133
66
|
}
|
|
134
|
-
|
|
135
|
-
|
|
67
|
+
}
|
|
68
|
+
head() {
|
|
69
|
+
return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
|
|
70
|
+
}
|
|
71
|
+
append(decision) {
|
|
72
|
+
const prevHash = this.head();
|
|
73
|
+
const seq = this.chain.length;
|
|
74
|
+
const ts = decision.ts;
|
|
75
|
+
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
76
|
+
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
|
|
77
|
+
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
78
|
+
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
79
|
+
this.chain.push(record2);
|
|
80
|
+
return record2;
|
|
81
|
+
}
|
|
82
|
+
find(decisionId) {
|
|
83
|
+
return this.chain.find((r) => r.decisionId === decisionId);
|
|
84
|
+
}
|
|
85
|
+
intact() {
|
|
86
|
+
let prev = "GENESIS";
|
|
87
|
+
for (const r of this.chain) {
|
|
88
|
+
const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
|
|
89
|
+
if (expected !== r.hash || r.prevHash !== prev)
|
|
90
|
+
return false;
|
|
91
|
+
prev = r.hash;
|
|
136
92
|
}
|
|
137
|
-
|
|
138
|
-
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// ../firewall/dist/evaluate.js
|
|
98
|
+
function evaluate(mandate, req, spentSoFar) {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
if (mandate.revoked)
|
|
101
|
+
return "mandate_revoked";
|
|
102
|
+
if (now < Date.parse(mandate.window.notBefore))
|
|
103
|
+
return "before_mandate_window";
|
|
104
|
+
if (now > Date.parse(mandate.window.notAfter))
|
|
105
|
+
return "after_mandate_window";
|
|
106
|
+
if (req.currency !== mandate.allow.currency)
|
|
107
|
+
return `currency_not_allowed:${req.currency}`;
|
|
108
|
+
if (req.amount <= 0)
|
|
109
|
+
return "non_positive_amount";
|
|
110
|
+
if (req.amount > mandate.allow.perTxMax)
|
|
111
|
+
return `per_tx_cap_exceeded:${req.amount}>${mandate.allow.perTxMax}`;
|
|
112
|
+
if (spentSoFar + req.amount > mandate.allow.maxTotal)
|
|
113
|
+
return `total_cap_exceeded:${spentSoFar + req.amount}>${mandate.allow.maxTotal}`;
|
|
114
|
+
const payeeOk = mandate.allow.payees.includes("*") || mandate.allow.payees.includes(req.payee);
|
|
115
|
+
if (!payeeOk)
|
|
116
|
+
return `payee_not_in_allowlist:${req.payee}`;
|
|
117
|
+
const catOk = mandate.allow.categories.includes("*") || mandate.allow.categories.includes(req.category);
|
|
118
|
+
if (!catOk)
|
|
119
|
+
return `category_not_allowed:${req.category}`;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ../firewall/dist/core.js
|
|
124
|
+
import { randomUUID } from "node:crypto";
|
|
125
|
+
function canonInvoice(ref) {
|
|
126
|
+
if (typeof ref !== "string")
|
|
127
|
+
return "";
|
|
128
|
+
return ref.normalize("NFC").replace(/[\s\u200B\u200C\u200D\uFEFF]+/g, "").toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
function validateRequest(req) {
|
|
131
|
+
if (!req || typeof req !== "object")
|
|
132
|
+
return "invalid_request";
|
|
133
|
+
if (typeof req.amount !== "number" || !Number.isFinite(req.amount) || req.amount <= 0)
|
|
134
|
+
return "invalid_amount";
|
|
135
|
+
if (typeof req.payee !== "string" || req.payee.length === 0)
|
|
136
|
+
return "invalid_payee";
|
|
137
|
+
if (typeof req.currency !== "string" || req.currency.length === 0)
|
|
138
|
+
return "invalid_currency";
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function requireHttpsBase(raw) {
|
|
142
|
+
const trimmed = String(raw ?? "").replace(/\/+$/, "");
|
|
143
|
+
let u;
|
|
144
|
+
try {
|
|
145
|
+
u = new URL(trimmed);
|
|
146
|
+
} catch {
|
|
147
|
+
throw new Error("FIDACY_API_URL is not a valid URL");
|
|
148
|
+
}
|
|
149
|
+
const localHttp = u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]");
|
|
150
|
+
if (u.protocol !== "https:" && !localHttp) {
|
|
151
|
+
throw new Error("FIDACY_API_URL must be https:// (the API key is sent to it)");
|
|
152
|
+
}
|
|
153
|
+
return trimmed;
|
|
154
|
+
}
|
|
155
|
+
var DevFidacyCore = class {
|
|
156
|
+
priv;
|
|
157
|
+
pubPem;
|
|
158
|
+
mandate;
|
|
159
|
+
store;
|
|
160
|
+
spent = 0;
|
|
161
|
+
claimedInvoices = /* @__PURE__ */ new Set();
|
|
162
|
+
onDecision;
|
|
163
|
+
constructor(opts) {
|
|
164
|
+
const kp = loadOrGenerateKeyPair();
|
|
165
|
+
this.priv = kp.privateKey;
|
|
166
|
+
this.pubPem = publicKeyPem(kp.publicKey);
|
|
167
|
+
if (kp.ephemeral)
|
|
168
|
+
console.error("[fidacy] dev mode: ephemeral signing key. Production: set FIDACY_SIGNING_KEY_B64, or FIDACY_MODE=http + FIDACY_API_URL + FIDACY_API_KEY to use the live core.");
|
|
169
|
+
this.mandate = opts.mandate;
|
|
170
|
+
this.store = new FileAuditStore(opts.auditLogPath ?? "./fidacy-audit.log");
|
|
171
|
+
this.onDecision = opts.onDecision;
|
|
172
|
+
}
|
|
173
|
+
async getMandate() {
|
|
174
|
+
return this.mandate;
|
|
175
|
+
}
|
|
176
|
+
async decide(req, subject) {
|
|
177
|
+
const decisionId = randomUUID();
|
|
178
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
179
|
+
const invalid = validateRequest(req);
|
|
180
|
+
if (invalid) {
|
|
181
|
+
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: invalid, ts };
|
|
182
|
+
this.store.append(decision2);
|
|
183
|
+
this.onDecision?.(decision2);
|
|
184
|
+
return decision2;
|
|
139
185
|
}
|
|
140
|
-
|
|
141
|
-
|
|
186
|
+
const violated = evaluate(this.mandate, req, this.spent);
|
|
187
|
+
if (violated) {
|
|
188
|
+
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: violated, ts };
|
|
189
|
+
this.store.append(decision2);
|
|
190
|
+
this.onDecision?.(decision2);
|
|
191
|
+
return decision2;
|
|
142
192
|
}
|
|
143
|
-
|
|
144
|
-
|
|
193
|
+
const invoice = canonInvoice(req.invoiceRef);
|
|
194
|
+
if (invoice) {
|
|
195
|
+
const k = `${subject}|${invoice}`;
|
|
196
|
+
if (this.claimedInvoices.has(k)) {
|
|
197
|
+
const decision2 = { decisionId, status: "DENY", subject, mandateId: this.mandate.id, request: req, violatedRule: `duplicate_invoice:${invoice}`, ts };
|
|
198
|
+
this.store.append(decision2);
|
|
199
|
+
this.onDecision?.(decision2);
|
|
200
|
+
return decision2;
|
|
201
|
+
}
|
|
202
|
+
this.claimedInvoices.add(k);
|
|
145
203
|
}
|
|
204
|
+
const grantPayload = { decisionId, subject, payee: req.payee, amount: req.amount, currency: req.currency, exp: Date.now() + 12e4, ...req.invoiceRef ? { invoiceRef: req.invoiceRef } : {} };
|
|
205
|
+
const grantBody = Buffer.from(stableStringify(grantPayload), "utf8").toString("base64url");
|
|
206
|
+
const grant = `${grantBody}.${sign(this.priv, grantBody)}`;
|
|
207
|
+
const decision = { decisionId, status: "ALLOW", subject, mandateId: this.mandate.id, request: req, grant, ts };
|
|
208
|
+
this.spent += req.amount;
|
|
209
|
+
this.store.append(decision);
|
|
210
|
+
this.onDecision?.(decision);
|
|
211
|
+
return decision;
|
|
212
|
+
}
|
|
213
|
+
async getProof(decisionId) {
|
|
214
|
+
const record2 = this.store.find(decisionId);
|
|
215
|
+
if (!record2)
|
|
216
|
+
return null;
|
|
217
|
+
return { record: record2, chainIntact: this.store.intact(), verifiedAgainstPublicKey: this.pubPem };
|
|
218
|
+
}
|
|
219
|
+
publicKey() {
|
|
220
|
+
return this.pubPem;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var HttpFidacyCore = class {
|
|
224
|
+
apiKey;
|
|
225
|
+
subjectPub;
|
|
226
|
+
baseUrl;
|
|
227
|
+
// Enforce https before any key-bearing request — a hostile FIDACY_API_URL would
|
|
228
|
+
// otherwise exfiltrate the API key in the Authorization header.
|
|
229
|
+
constructor(baseUrl, apiKey, subjectPub = "") {
|
|
230
|
+
this.apiKey = apiKey;
|
|
231
|
+
this.subjectPub = subjectPub;
|
|
232
|
+
this.baseUrl = requireHttpsBase(baseUrl);
|
|
233
|
+
}
|
|
234
|
+
async call(path, body) {
|
|
235
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.apiKey}` },
|
|
238
|
+
body: JSON.stringify(body)
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok)
|
|
241
|
+
throw new Error(`fidacy core ${path} -> ${res.status}`);
|
|
242
|
+
return await res.json();
|
|
243
|
+
}
|
|
244
|
+
async getMandate(subject) {
|
|
245
|
+
return this.call("/v1/mandate/get", { subject });
|
|
246
|
+
}
|
|
247
|
+
async decide(req, subject) {
|
|
248
|
+
return this.call("/v1/decide", { req, subject });
|
|
249
|
+
}
|
|
250
|
+
async getProof(decisionId) {
|
|
251
|
+
return this.call("/v1/audit/proof", { decisionId });
|
|
252
|
+
}
|
|
253
|
+
publicKey() {
|
|
254
|
+
return this.subjectPub;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/config.ts
|
|
259
|
+
import { homedir } from "node:os";
|
|
260
|
+
import { join } from "node:path";
|
|
261
|
+
import {
|
|
262
|
+
existsSync,
|
|
263
|
+
mkdirSync,
|
|
264
|
+
readFileSync,
|
|
265
|
+
writeFileSync
|
|
266
|
+
} from "node:fs";
|
|
267
|
+
function configDir() {
|
|
268
|
+
return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
|
|
269
|
+
}
|
|
270
|
+
function configPath() {
|
|
271
|
+
return join(configDir(), "config.json");
|
|
272
|
+
}
|
|
273
|
+
function auditLogPath() {
|
|
274
|
+
if (process.env.FIDACY_AUDIT_PATH) return process.env.FIDACY_AUDIT_PATH;
|
|
275
|
+
const dir = join(configDir(), "audit");
|
|
276
|
+
try {
|
|
277
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
return join(dir, "audit.log");
|
|
281
|
+
}
|
|
282
|
+
function readConfig() {
|
|
283
|
+
const p = configPath();
|
|
284
|
+
if (!existsSync(p)) return null;
|
|
285
|
+
try {
|
|
286
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
287
|
+
if (!raw || typeof raw.anon_id !== "string") return null;
|
|
288
|
+
return {
|
|
289
|
+
anon_id: raw.anon_id,
|
|
290
|
+
tier: raw.tier === "paid" ? "paid" : "free",
|
|
291
|
+
api_key: typeof raw.api_key === "string" ? raw.api_key : null,
|
|
292
|
+
mandate: raw.mandate
|
|
293
|
+
};
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function resolveMandateRules(cfg) {
|
|
299
|
+
const m = cfg?.mandate ?? {};
|
|
300
|
+
const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
301
|
+
const envNum = (v) => v === void 0 || v === "" ? void 0 : Number(v);
|
|
302
|
+
return {
|
|
303
|
+
payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
|
|
304
|
+
categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
|
|
305
|
+
currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
|
|
306
|
+
perTxMax: envNum(process.env.FIDACY_PER_TX_MAX) ?? m.perTxMax ?? 2500,
|
|
307
|
+
maxTotal: envNum(process.env.FIDACY_MAX_TOTAL) ?? m.maxTotal ?? 1e4
|
|
308
|
+
};
|
|
146
309
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
310
|
+
|
|
311
|
+
// src/telemetry.ts
|
|
312
|
+
var CLIENT_VERSION = "0.1.5";
|
|
313
|
+
function telemetryEnabled() {
|
|
314
|
+
const v = (process.env.FIDACY_DISABLE_TELEMETRY ?? "").trim().toLowerCase();
|
|
315
|
+
return !(v === "1" || v === "true" || v === "yes");
|
|
316
|
+
}
|
|
317
|
+
function endpoint() {
|
|
318
|
+
const base = (process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com").replace(/\/$/, "");
|
|
319
|
+
return `${base}/v1/telemetry`;
|
|
320
|
+
}
|
|
321
|
+
var anonIdCache = null;
|
|
322
|
+
function anonId() {
|
|
323
|
+
if (anonIdCache) return anonIdCache;
|
|
324
|
+
anonIdCache = readConfig()?.anon_id ?? null;
|
|
325
|
+
return anonIdCache;
|
|
326
|
+
}
|
|
327
|
+
var buffer = [];
|
|
328
|
+
var flushTimer = null;
|
|
329
|
+
function resultOf(status, violatedRule) {
|
|
330
|
+
if (status === "ALLOW") return "allow";
|
|
331
|
+
const r = violatedRule ?? "";
|
|
332
|
+
if (r.startsWith("per_tx_cap") || r.startsWith("total_cap")) return "deny_cap";
|
|
333
|
+
if (r.startsWith("payee_not_in_allowlist")) return "deny_payee";
|
|
334
|
+
return "deny_scope";
|
|
335
|
+
}
|
|
336
|
+
function record(type, result) {
|
|
337
|
+
if (!telemetryEnabled()) return;
|
|
338
|
+
buffer.push({ type, ts: (/* @__PURE__ */ new Date()).toISOString(), client_version: CLIENT_VERSION, ...result ? { result } : {} });
|
|
339
|
+
if (!flushTimer) {
|
|
340
|
+
flushTimer = setTimeout(() => {
|
|
341
|
+
void flush();
|
|
342
|
+
}, 2e3);
|
|
343
|
+
if (typeof flushTimer.unref === "function") flushTimer.unref();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
var recordDecision = (status, violatedRule) => record("decision", resultOf(status, violatedRule));
|
|
347
|
+
async function flush() {
|
|
348
|
+
if (flushTimer) {
|
|
349
|
+
clearTimeout(flushTimer);
|
|
350
|
+
flushTimer = null;
|
|
351
|
+
}
|
|
352
|
+
if (!telemetryEnabled() || buffer.length === 0) return;
|
|
353
|
+
const id = anonId();
|
|
354
|
+
if (!id) {
|
|
355
|
+
buffer = [];
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const events = buffer;
|
|
359
|
+
buffer = [];
|
|
360
|
+
try {
|
|
361
|
+
await fetch(endpoint(), {
|
|
362
|
+
method: "POST",
|
|
363
|
+
headers: { "content-type": "application/json" },
|
|
364
|
+
body: JSON.stringify({ anon_id: id, events })
|
|
365
|
+
});
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/core.ts
|
|
371
|
+
function buildMandate() {
|
|
372
|
+
if (process.env.FIDACY_MANDATE_JSON) {
|
|
373
|
+
try {
|
|
374
|
+
const parsed = JSON.parse(process.env.FIDACY_MANDATE_JSON);
|
|
375
|
+
if (parsed && typeof parsed === "object" && parsed.allow && parsed.window) return parsed;
|
|
376
|
+
console.error("[fidacy] FIDACY_MANDATE_JSON is missing allow/window; ignoring it and using the safe default mandate.");
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.error(`[fidacy] FIDACY_MANDATE_JSON is not valid JSON (${err.message}); ignoring it and using the safe default mandate.`);
|
|
154
379
|
}
|
|
155
|
-
|
|
380
|
+
}
|
|
381
|
+
const subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
|
|
382
|
+
const rules = resolveMandateRules(readConfig());
|
|
383
|
+
return {
|
|
384
|
+
id: "mandate:local",
|
|
385
|
+
subject,
|
|
386
|
+
version: "ap2.v0.2.0",
|
|
387
|
+
allow: {
|
|
388
|
+
payees: rules.payees,
|
|
389
|
+
categories: rules.categories,
|
|
390
|
+
currency: rules.currency,
|
|
391
|
+
maxTotal: rules.maxTotal,
|
|
392
|
+
perTxMax: rules.perTxMax
|
|
393
|
+
},
|
|
394
|
+
window: {
|
|
395
|
+
notBefore: process.env.FIDACY_NOT_BEFORE ?? new Date(Date.now() - 36e5).toISOString(),
|
|
396
|
+
notAfter: process.env.FIDACY_NOT_AFTER ?? new Date(Date.now() + 30 * 864e5).toISOString()
|
|
397
|
+
},
|
|
398
|
+
revoked: false
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function makeCore() {
|
|
402
|
+
if ((process.env.FIDACY_MODE ?? "dev") === "http") {
|
|
403
|
+
const url = process.env.FIDACY_API_URL;
|
|
404
|
+
const key = process.env.FIDACY_API_KEY;
|
|
405
|
+
if (!url || !key) throw new Error("FIDACY_MODE=http requires FIDACY_API_URL and FIDACY_API_KEY");
|
|
406
|
+
return new HttpFidacyCore(url, key, process.env.FIDACY_PUBLIC_KEY_PEM ?? "");
|
|
407
|
+
}
|
|
408
|
+
return new DevFidacyCore({
|
|
409
|
+
mandate: buildMandate(),
|
|
410
|
+
auditLogPath: auditLogPath(),
|
|
411
|
+
onDecision: (d) => recordDecision(d.status, d.violatedRule)
|
|
412
|
+
});
|
|
156
413
|
}
|
|
157
|
-
|
|
414
|
+
export {
|
|
415
|
+
makeCore
|
|
416
|
+
};
|