@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/core.js CHANGED
@@ -1,157 +1,416 @@
1
- import { randomUUID } from "node:crypto";
2
- import { loadOrGenerateKeyPair, publicKeyPem, sign } from "./signing.js";
3
- import { stableStringify } from "./util.js";
4
- import { FileAuditStore } from "./audit-store.js";
5
- // Pure mandate evaluation. Returns the first violated rule, or null if allowed.
6
- export function evaluate(mandate, req, spentSoFar) {
7
- const now = Date.now();
8
- if (mandate.revoked)
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
- // Reference core: real Ed25519 grants + durable hash-chained audit. Ships a demo
31
- // mandate so the firewall runs out of the box. Swap for HttpFidacyCore in prod.
32
- export class DevFidacyCore {
33
- priv;
34
- pubPem;
35
- mandate;
36
- store;
37
- spent = 0;
38
- claimedInvoices = new Set();
39
- constructor() {
40
- const kp = loadOrGenerateKeyPair();
41
- this.priv = kp.privateKey;
42
- this.pubPem = publicKeyPem(kp.publicKey);
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
- // Production core: delegates to your backend over HTTP. Implement three
114
- // endpoints in the Fidacy core and the MCP layer needs zero changes.
115
- export class HttpFidacyCore {
116
- baseUrl;
117
- apiKey;
118
- subjectPub;
119
- constructor(baseUrl, apiKey, subjectPub = "") {
120
- this.baseUrl = baseUrl;
121
- this.apiKey = apiKey;
122
- this.subjectPub = subjectPub;
123
- }
124
- async call(path, body) {
125
- const res = await fetch(`${this.baseUrl}${path}`, {
126
- method: "POST",
127
- headers: { "content-type": "application/json", authorization: `Bearer ${this.apiKey}` },
128
- body: JSON.stringify(body),
129
- });
130
- if (!res.ok)
131
- throw new Error(`fidacy core ${path} -> ${res.status}`);
132
- return (await res.json());
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
- async getMandate(subject) {
135
- return this.call("/v1/mandate/get", { subject });
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
- async decide(req, subject) {
138
- return this.call("/v1/decide", { req, subject });
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
- async getProof(decisionId) {
141
- return this.call("/v1/audit/proof", { decisionId });
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
- publicKey() {
144
- return this.subjectPub;
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
- export function makeCore() {
148
- if ((process.env.FIDACY_MODE ?? "dev") === "http") {
149
- const url = process.env.FIDACY_API_URL;
150
- const key = process.env.FIDACY_API_KEY;
151
- if (!url || !key)
152
- throw new Error("FIDACY_MODE=http requires FIDACY_API_URL and FIDACY_API_KEY");
153
- return new HttpFidacyCore(url, key, process.env.FIDACY_PUBLIC_KEY_PEM ?? "");
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
- return new DevFidacyCore();
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
- //# sourceMappingURL=core.js.map
414
+ export {
415
+ makeCore
416
+ };