@fidacy/mcp 0.1.11 → 0.1.12
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/CHANGELOG.md +17 -0
- package/dist/core.js +123 -62
- package/dist/index.js +124 -63
- package/dist/lib.js +208 -147
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -95,3 +95,20 @@ semantic versioning.
|
|
|
95
95
|
- verify_mandate/get_audit_proof gain one informational closing line each;
|
|
96
96
|
assess_action's missing-key error now offers the free-key path.
|
|
97
97
|
- config.json gains created_at + nudge/decision state (preserved on read).
|
|
98
|
+
|
|
99
|
+
## 0.1.12 — 2026-07-02
|
|
100
|
+
|
|
101
|
+
- **The BEC guarantee now survives restarts.** The local free-tier core kept the
|
|
102
|
+
duplicate-invoice set and the spent counter in memory only, so a re-presented
|
|
103
|
+
invoice was ALLOWED again after every process restart — exactly the short-lived
|
|
104
|
+
shell case (`npx` per session). The core now rehydrates both from the persisted
|
|
105
|
+
audit log at boot: prior ALLOW records rebuild the invoice claims (a paid
|
|
106
|
+
invoice stays paid, regardless of age) and re-sum `spent` within the current
|
|
107
|
+
mandate window and currency. One O(records) pass over the already-loaded chain.
|
|
108
|
+
- Audit records for ALLOWs now carry `amount`, `currency`, and the canonical
|
|
109
|
+
`invoiceRef` (rehydration fields). Additive: old logs stay valid, chain hashes
|
|
110
|
+
unchanged; pre-0.1.12 records simply carry no rehydratable state.
|
|
111
|
+
- Torn-tail recovery: a crash mid-append used to quarantine the WHOLE audit log
|
|
112
|
+
on next boot (losing all dedup state). A torn trailing line is now recognized,
|
|
113
|
+
the intact prefix is salvaged, and the tail truncated. Mid-file damage still
|
|
114
|
+
quarantines the file as before.
|
package/dist/core.js
CHANGED
|
@@ -33,66 +33,9 @@ function sha256(input) {
|
|
|
33
33
|
|
|
34
34
|
// ../firewall/dist/audit-store.js
|
|
35
35
|
import fs from "node:fs";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
66
|
-
}
|
|
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;
|
|
92
|
-
}
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
36
|
+
|
|
37
|
+
// ../firewall/dist/core.js
|
|
38
|
+
import { randomUUID } from "node:crypto";
|
|
96
39
|
|
|
97
40
|
// ../firewall/dist/evaluate.js
|
|
98
41
|
function fold(s) {
|
|
@@ -169,7 +112,6 @@ function evaluate(mandate, req, spentSoFar) {
|
|
|
169
112
|
}
|
|
170
113
|
|
|
171
114
|
// ../firewall/dist/core.js
|
|
172
|
-
import { randomUUID } from "node:crypto";
|
|
173
115
|
function canonInvoice(ref) {
|
|
174
116
|
if (typeof ref !== "string")
|
|
175
117
|
return "";
|
|
@@ -217,6 +159,35 @@ var DevFidacyCore = class {
|
|
|
217
159
|
this.mandate = opts.mandate;
|
|
218
160
|
this.store = new FileAuditStore(opts.auditLogPath ?? "./fidacy-audit.log");
|
|
219
161
|
this.onDecision = opts.onDecision;
|
|
162
|
+
this.rehydrate();
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Rebuild the in-memory decision state from the persisted audit log, so a process
|
|
166
|
+
* restart cannot re-open a claimed invoice (BEC dedup) or reset the maxTotal
|
|
167
|
+
* counter. One O(records) pass at boot over the already-loaded chain:
|
|
168
|
+
* - claimedInvoices: every ALLOW that carried an invoiceRef, regardless of age —
|
|
169
|
+
* a paid invoice stays paid.
|
|
170
|
+
* - spent: ALLOW amounts whose timestamp falls inside the CURRENT mandate window
|
|
171
|
+
* and whose currency matches the mandate (the cap is defined per window/currency).
|
|
172
|
+
* Records from older versions lack the rehydration fields and are skipped.
|
|
173
|
+
*/
|
|
174
|
+
rehydrate() {
|
|
175
|
+
const notBefore = Date.parse(this.mandate.window.notBefore);
|
|
176
|
+
const notAfter = Date.parse(this.mandate.window.notAfter);
|
|
177
|
+
for (const r of this.store.records()) {
|
|
178
|
+
if (r.status !== "ALLOW")
|
|
179
|
+
continue;
|
|
180
|
+
if (r.invoiceRef)
|
|
181
|
+
this.claimedInvoices.add(`${r.subject}|${r.invoiceRef}`);
|
|
182
|
+
if (typeof r.amount !== "number" || !Number.isFinite(r.amount))
|
|
183
|
+
continue;
|
|
184
|
+
if (r.currency !== this.mandate.allow.currency)
|
|
185
|
+
continue;
|
|
186
|
+
const t = Date.parse(r.ts);
|
|
187
|
+
if (!Number.isFinite(t) || t < notBefore || t > notAfter)
|
|
188
|
+
continue;
|
|
189
|
+
this.spent += r.amount;
|
|
190
|
+
}
|
|
220
191
|
}
|
|
221
192
|
async getMandate() {
|
|
222
193
|
return this.mandate;
|
|
@@ -303,6 +274,96 @@ var HttpFidacyCore = class {
|
|
|
303
274
|
}
|
|
304
275
|
};
|
|
305
276
|
|
|
277
|
+
// ../firewall/dist/audit-store.js
|
|
278
|
+
var FileAuditStore = class {
|
|
279
|
+
path;
|
|
280
|
+
chain = [];
|
|
281
|
+
constructor(path) {
|
|
282
|
+
this.path = path;
|
|
283
|
+
this.load();
|
|
284
|
+
}
|
|
285
|
+
load() {
|
|
286
|
+
if (!fs.existsSync(this.path))
|
|
287
|
+
return;
|
|
288
|
+
try {
|
|
289
|
+
const raw = fs.readFileSync(this.path, "utf8");
|
|
290
|
+
const lines = raw.split("\n");
|
|
291
|
+
const parsed = [];
|
|
292
|
+
let torn = false;
|
|
293
|
+
for (let i = 0; i < lines.length; i++) {
|
|
294
|
+
if (!lines[i].trim())
|
|
295
|
+
continue;
|
|
296
|
+
try {
|
|
297
|
+
parsed.push(JSON.parse(lines[i]));
|
|
298
|
+
} catch {
|
|
299
|
+
if (lines.slice(i + 1).some((l) => l.trim()))
|
|
300
|
+
throw new Error(`unparseable record at line ${i + 1}`);
|
|
301
|
+
torn = true;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this.chain = parsed;
|
|
306
|
+
if (!this.intact())
|
|
307
|
+
throw new Error("audit chain integrity broken");
|
|
308
|
+
if (torn) {
|
|
309
|
+
const clean = parsed.map((r) => JSON.stringify(r)).join("\n") + (parsed.length ? "\n" : "");
|
|
310
|
+
fs.writeFileSync(this.path, clean);
|
|
311
|
+
console.error(`[fidacy] audit log at ${this.path} had a torn trailing line (crash during append?); salvaged ${parsed.length} intact records and truncated the tail.`);
|
|
312
|
+
}
|
|
313
|
+
} catch (err) {
|
|
314
|
+
this.chain = [];
|
|
315
|
+
try {
|
|
316
|
+
const quarantine = `${this.path}.corrupt-${Date.now()}`;
|
|
317
|
+
fs.renameSync(this.path, quarantine);
|
|
318
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
|
|
319
|
+
} catch {
|
|
320
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
head() {
|
|
325
|
+
return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
|
|
326
|
+
}
|
|
327
|
+
append(decision) {
|
|
328
|
+
const prevHash = this.head();
|
|
329
|
+
const seq = this.chain.length;
|
|
330
|
+
const ts = decision.ts;
|
|
331
|
+
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
332
|
+
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
|
|
333
|
+
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
334
|
+
if (decision.status === "ALLOW") {
|
|
335
|
+
const req = decision.request;
|
|
336
|
+
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
337
|
+
record2.amount = req.amount;
|
|
338
|
+
if (typeof req?.currency === "string")
|
|
339
|
+
record2.currency = req.currency;
|
|
340
|
+
const invoice = canonInvoice(req?.invoiceRef);
|
|
341
|
+
if (invoice)
|
|
342
|
+
record2.invoiceRef = invoice;
|
|
343
|
+
}
|
|
344
|
+
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
345
|
+
this.chain.push(record2);
|
|
346
|
+
return record2;
|
|
347
|
+
}
|
|
348
|
+
find(decisionId) {
|
|
349
|
+
return this.chain.find((r) => r.decisionId === decisionId);
|
|
350
|
+
}
|
|
351
|
+
/** The full loaded chain, oldest first. Read-only; used to rehydrate core state at boot. */
|
|
352
|
+
records() {
|
|
353
|
+
return this.chain;
|
|
354
|
+
}
|
|
355
|
+
intact() {
|
|
356
|
+
let prev = "GENESIS";
|
|
357
|
+
for (const r of this.chain) {
|
|
358
|
+
const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
|
|
359
|
+
if (expected !== r.hash || r.prevHash !== prev)
|
|
360
|
+
return false;
|
|
361
|
+
prev = r.hash;
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
306
367
|
// src/config.ts
|
|
307
368
|
import { homedir } from "node:os";
|
|
308
369
|
import { join } from "node:path";
|
|
@@ -362,7 +423,7 @@ function resolveMandateRules(cfg) {
|
|
|
362
423
|
}
|
|
363
424
|
|
|
364
425
|
// src/telemetry.ts
|
|
365
|
-
var CLIENT_VERSION = true ? "0.1.
|
|
426
|
+
var CLIENT_VERSION = true ? "0.1.12" : "dev";
|
|
366
427
|
var currentShell = "mcp";
|
|
367
428
|
function telemetryEnabled() {
|
|
368
429
|
const v = (process.env.FIDACY_DISABLE_TELEMETRY ?? "").trim().toLowerCase();
|
package/dist/index.js
CHANGED
|
@@ -40,66 +40,9 @@ function sha256(input) {
|
|
|
40
40
|
|
|
41
41
|
// ../firewall/dist/audit-store.js
|
|
42
42
|
import fs from "node:fs";
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
constructor(path) {
|
|
47
|
-
this.path = path;
|
|
48
|
-
this.load();
|
|
49
|
-
}
|
|
50
|
-
load() {
|
|
51
|
-
if (!fs.existsSync(this.path))
|
|
52
|
-
return;
|
|
53
|
-
try {
|
|
54
|
-
const raw = fs.readFileSync(this.path, "utf8");
|
|
55
|
-
const parsed = [];
|
|
56
|
-
for (const line of raw.split("\n")) {
|
|
57
|
-
if (!line.trim())
|
|
58
|
-
continue;
|
|
59
|
-
parsed.push(JSON.parse(line));
|
|
60
|
-
}
|
|
61
|
-
this.chain = parsed;
|
|
62
|
-
if (!this.intact())
|
|
63
|
-
throw new Error("audit chain integrity broken");
|
|
64
|
-
} catch (err) {
|
|
65
|
-
this.chain = [];
|
|
66
|
-
try {
|
|
67
|
-
const quarantine = `${this.path}.corrupt-${Date.now()}`;
|
|
68
|
-
fs.renameSync(this.path, quarantine);
|
|
69
|
-
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
|
|
70
|
-
} catch {
|
|
71
|
-
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
head() {
|
|
76
|
-
return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
|
|
77
|
-
}
|
|
78
|
-
append(decision) {
|
|
79
|
-
const prevHash = this.head();
|
|
80
|
-
const seq = this.chain.length;
|
|
81
|
-
const ts = decision.ts;
|
|
82
|
-
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
83
|
-
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
|
|
84
|
-
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
85
|
-
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
86
|
-
this.chain.push(record2);
|
|
87
|
-
return record2;
|
|
88
|
-
}
|
|
89
|
-
find(decisionId) {
|
|
90
|
-
return this.chain.find((r) => r.decisionId === decisionId);
|
|
91
|
-
}
|
|
92
|
-
intact() {
|
|
93
|
-
let prev = "GENESIS";
|
|
94
|
-
for (const r of this.chain) {
|
|
95
|
-
const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
|
|
96
|
-
if (expected !== r.hash || r.prevHash !== prev)
|
|
97
|
-
return false;
|
|
98
|
-
prev = r.hash;
|
|
99
|
-
}
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
43
|
+
|
|
44
|
+
// ../firewall/dist/core.js
|
|
45
|
+
import { randomUUID } from "node:crypto";
|
|
103
46
|
|
|
104
47
|
// ../firewall/dist/evaluate.js
|
|
105
48
|
function fold(s) {
|
|
@@ -176,7 +119,6 @@ function evaluate(mandate, req, spentSoFar) {
|
|
|
176
119
|
}
|
|
177
120
|
|
|
178
121
|
// ../firewall/dist/core.js
|
|
179
|
-
import { randomUUID } from "node:crypto";
|
|
180
122
|
function canonInvoice(ref) {
|
|
181
123
|
if (typeof ref !== "string")
|
|
182
124
|
return "";
|
|
@@ -224,6 +166,35 @@ var DevFidacyCore = class {
|
|
|
224
166
|
this.mandate = opts.mandate;
|
|
225
167
|
this.store = new FileAuditStore(opts.auditLogPath ?? "./fidacy-audit.log");
|
|
226
168
|
this.onDecision = opts.onDecision;
|
|
169
|
+
this.rehydrate();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Rebuild the in-memory decision state from the persisted audit log, so a process
|
|
173
|
+
* restart cannot re-open a claimed invoice (BEC dedup) or reset the maxTotal
|
|
174
|
+
* counter. One O(records) pass at boot over the already-loaded chain:
|
|
175
|
+
* - claimedInvoices: every ALLOW that carried an invoiceRef, regardless of age —
|
|
176
|
+
* a paid invoice stays paid.
|
|
177
|
+
* - spent: ALLOW amounts whose timestamp falls inside the CURRENT mandate window
|
|
178
|
+
* and whose currency matches the mandate (the cap is defined per window/currency).
|
|
179
|
+
* Records from older versions lack the rehydration fields and are skipped.
|
|
180
|
+
*/
|
|
181
|
+
rehydrate() {
|
|
182
|
+
const notBefore = Date.parse(this.mandate.window.notBefore);
|
|
183
|
+
const notAfter = Date.parse(this.mandate.window.notAfter);
|
|
184
|
+
for (const r of this.store.records()) {
|
|
185
|
+
if (r.status !== "ALLOW")
|
|
186
|
+
continue;
|
|
187
|
+
if (r.invoiceRef)
|
|
188
|
+
this.claimedInvoices.add(`${r.subject}|${r.invoiceRef}`);
|
|
189
|
+
if (typeof r.amount !== "number" || !Number.isFinite(r.amount))
|
|
190
|
+
continue;
|
|
191
|
+
if (r.currency !== this.mandate.allow.currency)
|
|
192
|
+
continue;
|
|
193
|
+
const t = Date.parse(r.ts);
|
|
194
|
+
if (!Number.isFinite(t) || t < notBefore || t > notAfter)
|
|
195
|
+
continue;
|
|
196
|
+
this.spent += r.amount;
|
|
197
|
+
}
|
|
227
198
|
}
|
|
228
199
|
async getMandate() {
|
|
229
200
|
return this.mandate;
|
|
@@ -310,6 +281,96 @@ var HttpFidacyCore = class {
|
|
|
310
281
|
}
|
|
311
282
|
};
|
|
312
283
|
|
|
284
|
+
// ../firewall/dist/audit-store.js
|
|
285
|
+
var FileAuditStore = class {
|
|
286
|
+
path;
|
|
287
|
+
chain = [];
|
|
288
|
+
constructor(path) {
|
|
289
|
+
this.path = path;
|
|
290
|
+
this.load();
|
|
291
|
+
}
|
|
292
|
+
load() {
|
|
293
|
+
if (!fs.existsSync(this.path))
|
|
294
|
+
return;
|
|
295
|
+
try {
|
|
296
|
+
const raw = fs.readFileSync(this.path, "utf8");
|
|
297
|
+
const lines = raw.split("\n");
|
|
298
|
+
const parsed = [];
|
|
299
|
+
let torn = false;
|
|
300
|
+
for (let i = 0; i < lines.length; i++) {
|
|
301
|
+
if (!lines[i].trim())
|
|
302
|
+
continue;
|
|
303
|
+
try {
|
|
304
|
+
parsed.push(JSON.parse(lines[i]));
|
|
305
|
+
} catch {
|
|
306
|
+
if (lines.slice(i + 1).some((l) => l.trim()))
|
|
307
|
+
throw new Error(`unparseable record at line ${i + 1}`);
|
|
308
|
+
torn = true;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
this.chain = parsed;
|
|
313
|
+
if (!this.intact())
|
|
314
|
+
throw new Error("audit chain integrity broken");
|
|
315
|
+
if (torn) {
|
|
316
|
+
const clean = parsed.map((r) => JSON.stringify(r)).join("\n") + (parsed.length ? "\n" : "");
|
|
317
|
+
fs.writeFileSync(this.path, clean);
|
|
318
|
+
console.error(`[fidacy] audit log at ${this.path} had a torn trailing line (crash during append?); salvaged ${parsed.length} intact records and truncated the tail.`);
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
this.chain = [];
|
|
322
|
+
try {
|
|
323
|
+
const quarantine = `${this.path}.corrupt-${Date.now()}`;
|
|
324
|
+
fs.renameSync(this.path, quarantine);
|
|
325
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
|
|
326
|
+
} catch {
|
|
327
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
head() {
|
|
332
|
+
return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
|
|
333
|
+
}
|
|
334
|
+
append(decision) {
|
|
335
|
+
const prevHash = this.head();
|
|
336
|
+
const seq = this.chain.length;
|
|
337
|
+
const ts = decision.ts;
|
|
338
|
+
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
339
|
+
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
|
|
340
|
+
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
341
|
+
if (decision.status === "ALLOW") {
|
|
342
|
+
const req = decision.request;
|
|
343
|
+
if (typeof req?.amount === "number" && Number.isFinite(req.amount))
|
|
344
|
+
record2.amount = req.amount;
|
|
345
|
+
if (typeof req?.currency === "string")
|
|
346
|
+
record2.currency = req.currency;
|
|
347
|
+
const invoice = canonInvoice(req?.invoiceRef);
|
|
348
|
+
if (invoice)
|
|
349
|
+
record2.invoiceRef = invoice;
|
|
350
|
+
}
|
|
351
|
+
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
352
|
+
this.chain.push(record2);
|
|
353
|
+
return record2;
|
|
354
|
+
}
|
|
355
|
+
find(decisionId) {
|
|
356
|
+
return this.chain.find((r) => r.decisionId === decisionId);
|
|
357
|
+
}
|
|
358
|
+
/** The full loaded chain, oldest first. Read-only; used to rehydrate core state at boot. */
|
|
359
|
+
records() {
|
|
360
|
+
return this.chain;
|
|
361
|
+
}
|
|
362
|
+
intact() {
|
|
363
|
+
let prev = "GENESIS";
|
|
364
|
+
for (const r of this.chain) {
|
|
365
|
+
const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
|
|
366
|
+
if (expected !== r.hash || r.prevHash !== prev)
|
|
367
|
+
return false;
|
|
368
|
+
prev = r.hash;
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
313
374
|
// src/config.ts
|
|
314
375
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
315
376
|
import { homedir } from "node:os";
|
|
@@ -391,7 +452,7 @@ function resolveMandateRules(cfg) {
|
|
|
391
452
|
}
|
|
392
453
|
|
|
393
454
|
// src/telemetry.ts
|
|
394
|
-
var CLIENT_VERSION = true ? "0.1.
|
|
455
|
+
var CLIENT_VERSION = true ? "0.1.12" : "dev";
|
|
395
456
|
var currentShell = "mcp";
|
|
396
457
|
function telemetryEnabled() {
|
|
397
458
|
const v = (process.env.FIDACY_DISABLE_TELEMETRY ?? "").trim().toLowerCase();
|
|
@@ -615,7 +676,7 @@ async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
|
|
|
615
676
|
}
|
|
616
677
|
|
|
617
678
|
// src/provision.ts
|
|
618
|
-
var CLIENT_VERSION2 = true ? "0.1.
|
|
679
|
+
var CLIENT_VERSION2 = true ? "0.1.12" : "dev";
|
|
619
680
|
function provisionEnabled() {
|
|
620
681
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
621
682
|
return !(v === "1" || v === "true" || v === "yes");
|
package/dist/lib.js
CHANGED
|
@@ -74,151 +74,9 @@ function verifyGrant(publicKeyPem2, grant, expected) {
|
|
|
74
74
|
|
|
75
75
|
// ../firewall/dist/audit-store.js
|
|
76
76
|
import fs from "node:fs";
|
|
77
|
-
var FileAuditStore = class {
|
|
78
|
-
path;
|
|
79
|
-
chain = [];
|
|
80
|
-
constructor(path) {
|
|
81
|
-
this.path = path;
|
|
82
|
-
this.load();
|
|
83
|
-
}
|
|
84
|
-
load() {
|
|
85
|
-
if (!fs.existsSync(this.path))
|
|
86
|
-
return;
|
|
87
|
-
try {
|
|
88
|
-
const raw = fs.readFileSync(this.path, "utf8");
|
|
89
|
-
const parsed = [];
|
|
90
|
-
for (const line of raw.split("\n")) {
|
|
91
|
-
if (!line.trim())
|
|
92
|
-
continue;
|
|
93
|
-
parsed.push(JSON.parse(line));
|
|
94
|
-
}
|
|
95
|
-
this.chain = parsed;
|
|
96
|
-
if (!this.intact())
|
|
97
|
-
throw new Error("audit chain integrity broken");
|
|
98
|
-
} catch (err) {
|
|
99
|
-
this.chain = [];
|
|
100
|
-
try {
|
|
101
|
-
const quarantine = `${this.path}.corrupt-${Date.now()}`;
|
|
102
|
-
fs.renameSync(this.path, quarantine);
|
|
103
|
-
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
|
|
104
|
-
} catch {
|
|
105
|
-
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
head() {
|
|
110
|
-
return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
|
|
111
|
-
}
|
|
112
|
-
append(decision) {
|
|
113
|
-
const prevHash = this.head();
|
|
114
|
-
const seq = this.chain.length;
|
|
115
|
-
const ts = decision.ts;
|
|
116
|
-
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
117
|
-
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
|
|
118
|
-
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
119
|
-
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
120
|
-
this.chain.push(record2);
|
|
121
|
-
return record2;
|
|
122
|
-
}
|
|
123
|
-
find(decisionId) {
|
|
124
|
-
return this.chain.find((r) => r.decisionId === decisionId);
|
|
125
|
-
}
|
|
126
|
-
intact() {
|
|
127
|
-
let prev = "GENESIS";
|
|
128
|
-
for (const r of this.chain) {
|
|
129
|
-
const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
|
|
130
|
-
if (expected !== r.hash || r.prevHash !== prev)
|
|
131
|
-
return false;
|
|
132
|
-
prev = r.hash;
|
|
133
|
-
}
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
77
|
|
|
138
|
-
// ../firewall/dist/
|
|
139
|
-
import { createPublicKey } from "node:crypto";
|
|
78
|
+
// ../firewall/dist/core.js
|
|
140
79
|
import { randomUUID } from "node:crypto";
|
|
141
|
-
var ReferenceRail = class {
|
|
142
|
-
settlements = [];
|
|
143
|
-
async execute(req) {
|
|
144
|
-
const railRef = "ref_" + randomUUID();
|
|
145
|
-
this.settlements.push({ ...req, railRef, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
146
|
-
return { railRef };
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
function httpRedeem(coreUrl, apiKey) {
|
|
150
|
-
const base = String(coreUrl ?? "").replace(/\/+$/, "");
|
|
151
|
-
const u = new URL(base);
|
|
152
|
-
if (u.protocol !== "https:" && !(u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1"))) {
|
|
153
|
-
throw new Error("core URL must be https:// (the API key is sent to it)");
|
|
154
|
-
}
|
|
155
|
-
return async (grant) => {
|
|
156
|
-
const res = await fetch(`${base}/v1/grant/redeem`, {
|
|
157
|
-
method: "POST",
|
|
158
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
159
|
-
body: JSON.stringify({ grant })
|
|
160
|
-
});
|
|
161
|
-
if (res.status === 200)
|
|
162
|
-
return true;
|
|
163
|
-
if (res.status === 409)
|
|
164
|
-
return false;
|
|
165
|
-
throw new Error(`redeem -> ${res.status}`);
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
var GrantEnforcingExecutor = class {
|
|
169
|
-
rail;
|
|
170
|
-
used = /* @__PURE__ */ new Set();
|
|
171
|
-
pub;
|
|
172
|
-
redeem;
|
|
173
|
-
constructor(publicKeyPem2, rail, opts) {
|
|
174
|
-
this.rail = rail;
|
|
175
|
-
this.pub = createPublicKey(publicKeyPem2);
|
|
176
|
-
this.redeem = opts?.redeem;
|
|
177
|
-
}
|
|
178
|
-
async execute(req, grant) {
|
|
179
|
-
if (!grant)
|
|
180
|
-
return { status: "REFUSED", reason: "missing_grant" };
|
|
181
|
-
const dot = grant.indexOf(".");
|
|
182
|
-
if (dot < 0)
|
|
183
|
-
return { status: "REFUSED", reason: "malformed_grant" };
|
|
184
|
-
const body = grant.slice(0, dot);
|
|
185
|
-
const sig = grant.slice(dot + 1);
|
|
186
|
-
if (!verify(this.pub, body, sig))
|
|
187
|
-
return { status: "REFUSED", reason: "invalid_signature" };
|
|
188
|
-
let p;
|
|
189
|
-
try {
|
|
190
|
-
p = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
191
|
-
} catch {
|
|
192
|
-
return { status: "REFUSED", reason: "undecodable_grant" };
|
|
193
|
-
}
|
|
194
|
-
if (Date.now() > p.exp)
|
|
195
|
-
return { status: "REFUSED", reason: "grant_expired" };
|
|
196
|
-
if (this.used.has(p.decisionId))
|
|
197
|
-
return { status: "REFUSED", reason: "grant_replayed" };
|
|
198
|
-
if (p.payee !== req.payee)
|
|
199
|
-
return { status: "REFUSED", reason: "payee_mismatch" };
|
|
200
|
-
if (p.amount !== req.amount)
|
|
201
|
-
return { status: "REFUSED", reason: "amount_mismatch" };
|
|
202
|
-
if (p.currency !== req.currency)
|
|
203
|
-
return { status: "REFUSED", reason: "currency_mismatch" };
|
|
204
|
-
if (req.invoiceRef != null && req.invoiceRef !== (p.invoiceRef ?? null)) {
|
|
205
|
-
return { status: "REFUSED", reason: "invoice_mismatch" };
|
|
206
|
-
}
|
|
207
|
-
if (this.redeem) {
|
|
208
|
-
let first;
|
|
209
|
-
try {
|
|
210
|
-
first = await this.redeem(grant);
|
|
211
|
-
} catch {
|
|
212
|
-
return { status: "REFUSED", reason: "redeem_unavailable" };
|
|
213
|
-
}
|
|
214
|
-
if (!first)
|
|
215
|
-
return { status: "REFUSED", reason: "grant_replayed" };
|
|
216
|
-
}
|
|
217
|
-
this.used.add(p.decisionId);
|
|
218
|
-
const { railRef } = await this.rail.execute(req);
|
|
219
|
-
return { status: "EXECUTED", railRef, decisionId: p.decisionId };
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
80
|
|
|
223
81
|
// ../firewall/dist/evaluate.js
|
|
224
82
|
function fold(s) {
|
|
@@ -295,7 +153,6 @@ function evaluate(mandate, req, spentSoFar) {
|
|
|
295
153
|
}
|
|
296
154
|
|
|
297
155
|
// ../firewall/dist/core.js
|
|
298
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
299
156
|
function canonInvoice(ref) {
|
|
300
157
|
if (typeof ref !== "string")
|
|
301
158
|
return "";
|
|
@@ -343,12 +200,41 @@ var DevFidacyCore = class {
|
|
|
343
200
|
this.mandate = opts.mandate;
|
|
344
201
|
this.store = new FileAuditStore(opts.auditLogPath ?? "./fidacy-audit.log");
|
|
345
202
|
this.onDecision = opts.onDecision;
|
|
203
|
+
this.rehydrate();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Rebuild the in-memory decision state from the persisted audit log, so a process
|
|
207
|
+
* restart cannot re-open a claimed invoice (BEC dedup) or reset the maxTotal
|
|
208
|
+
* counter. One O(records) pass at boot over the already-loaded chain:
|
|
209
|
+
* - claimedInvoices: every ALLOW that carried an invoiceRef, regardless of age —
|
|
210
|
+
* a paid invoice stays paid.
|
|
211
|
+
* - spent: ALLOW amounts whose timestamp falls inside the CURRENT mandate window
|
|
212
|
+
* and whose currency matches the mandate (the cap is defined per window/currency).
|
|
213
|
+
* Records from older versions lack the rehydration fields and are skipped.
|
|
214
|
+
*/
|
|
215
|
+
rehydrate() {
|
|
216
|
+
const notBefore = Date.parse(this.mandate.window.notBefore);
|
|
217
|
+
const notAfter = Date.parse(this.mandate.window.notAfter);
|
|
218
|
+
for (const r of this.store.records()) {
|
|
219
|
+
if (r.status !== "ALLOW")
|
|
220
|
+
continue;
|
|
221
|
+
if (r.invoiceRef)
|
|
222
|
+
this.claimedInvoices.add(`${r.subject}|${r.invoiceRef}`);
|
|
223
|
+
if (typeof r.amount !== "number" || !Number.isFinite(r.amount))
|
|
224
|
+
continue;
|
|
225
|
+
if (r.currency !== this.mandate.allow.currency)
|
|
226
|
+
continue;
|
|
227
|
+
const t = Date.parse(r.ts);
|
|
228
|
+
if (!Number.isFinite(t) || t < notBefore || t > notAfter)
|
|
229
|
+
continue;
|
|
230
|
+
this.spent += r.amount;
|
|
231
|
+
}
|
|
346
232
|
}
|
|
347
233
|
async getMandate() {
|
|
348
234
|
return this.mandate;
|
|
349
235
|
}
|
|
350
236
|
async decide(req, subject) {
|
|
351
|
-
const decisionId =
|
|
237
|
+
const decisionId = randomUUID();
|
|
352
238
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
353
239
|
const invalid = validateRequest(req);
|
|
354
240
|
if (invalid) {
|
|
@@ -429,6 +315,181 @@ var HttpFidacyCore = class {
|
|
|
429
315
|
}
|
|
430
316
|
};
|
|
431
317
|
|
|
318
|
+
// ../firewall/dist/audit-store.js
|
|
319
|
+
var FileAuditStore = class {
|
|
320
|
+
path;
|
|
321
|
+
chain = [];
|
|
322
|
+
constructor(path) {
|
|
323
|
+
this.path = path;
|
|
324
|
+
this.load();
|
|
325
|
+
}
|
|
326
|
+
load() {
|
|
327
|
+
if (!fs.existsSync(this.path))
|
|
328
|
+
return;
|
|
329
|
+
try {
|
|
330
|
+
const raw = fs.readFileSync(this.path, "utf8");
|
|
331
|
+
const lines = raw.split("\n");
|
|
332
|
+
const parsed = [];
|
|
333
|
+
let torn = false;
|
|
334
|
+
for (let i = 0; i < lines.length; i++) {
|
|
335
|
+
if (!lines[i].trim())
|
|
336
|
+
continue;
|
|
337
|
+
try {
|
|
338
|
+
parsed.push(JSON.parse(lines[i]));
|
|
339
|
+
} catch {
|
|
340
|
+
if (lines.slice(i + 1).some((l) => l.trim()))
|
|
341
|
+
throw new Error(`unparseable record at line ${i + 1}`);
|
|
342
|
+
torn = true;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
this.chain = parsed;
|
|
347
|
+
if (!this.intact())
|
|
348
|
+
throw new Error("audit chain integrity broken");
|
|
349
|
+
if (torn) {
|
|
350
|
+
const clean = parsed.map((r) => JSON.stringify(r)).join("\n") + (parsed.length ? "\n" : "");
|
|
351
|
+
fs.writeFileSync(this.path, clean);
|
|
352
|
+
console.error(`[fidacy] audit log at ${this.path} had a torn trailing line (crash during append?); salvaged ${parsed.length} intact records and truncated the tail.`);
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
this.chain = [];
|
|
356
|
+
try {
|
|
357
|
+
const quarantine = `${this.path}.corrupt-${Date.now()}`;
|
|
358
|
+
fs.renameSync(this.path, quarantine);
|
|
359
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered (${err.message}); quarantined to ${quarantine}, starting a fresh chain.`);
|
|
360
|
+
} catch {
|
|
361
|
+
console.error(`[fidacy] audit log at ${this.path} is unreadable/tampered and could not be quarantined; starting a fresh in-memory chain.`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
head() {
|
|
366
|
+
return this.chain.length ? this.chain[this.chain.length - 1].hash : "GENESIS";
|
|
367
|
+
}
|
|
368
|
+
append(decision) {
|
|
369
|
+
const prevHash = this.head();
|
|
370
|
+
const seq = this.chain.length;
|
|
371
|
+
const ts = decision.ts;
|
|
372
|
+
const digest = sha256(stableStringify({ decisionId: decision.decisionId, status: decision.status, request: decision.request, violatedRule: decision.violatedRule ?? null }));
|
|
373
|
+
const hash = sha256(`${prevHash}|${digest}|${seq}|${ts}`);
|
|
374
|
+
const record2 = { seq, decisionId: decision.decisionId, status: decision.status, subject: decision.subject, digest, prevHash, hash, ts };
|
|
375
|
+
if (decision.status === "ALLOW") {
|
|
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;
|
|
381
|
+
const invoice = canonInvoice(req?.invoiceRef);
|
|
382
|
+
if (invoice)
|
|
383
|
+
record2.invoiceRef = invoice;
|
|
384
|
+
}
|
|
385
|
+
fs.appendFileSync(this.path, JSON.stringify(record2) + "\n");
|
|
386
|
+
this.chain.push(record2);
|
|
387
|
+
return record2;
|
|
388
|
+
}
|
|
389
|
+
find(decisionId) {
|
|
390
|
+
return this.chain.find((r) => r.decisionId === decisionId);
|
|
391
|
+
}
|
|
392
|
+
/** The full loaded chain, oldest first. Read-only; used to rehydrate core state at boot. */
|
|
393
|
+
records() {
|
|
394
|
+
return this.chain;
|
|
395
|
+
}
|
|
396
|
+
intact() {
|
|
397
|
+
let prev = "GENESIS";
|
|
398
|
+
for (const r of this.chain) {
|
|
399
|
+
const expected = sha256(`${prev}|${r.digest}|${r.seq}|${r.ts}`);
|
|
400
|
+
if (expected !== r.hash || r.prevHash !== prev)
|
|
401
|
+
return false;
|
|
402
|
+
prev = r.hash;
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// ../firewall/dist/executor.js
|
|
409
|
+
import { createPublicKey } from "node:crypto";
|
|
410
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
411
|
+
var ReferenceRail = class {
|
|
412
|
+
settlements = [];
|
|
413
|
+
async execute(req) {
|
|
414
|
+
const railRef = "ref_" + randomUUID2();
|
|
415
|
+
this.settlements.push({ ...req, railRef, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
416
|
+
return { railRef };
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
function httpRedeem(coreUrl, apiKey) {
|
|
420
|
+
const base = String(coreUrl ?? "").replace(/\/+$/, "");
|
|
421
|
+
const u = new URL(base);
|
|
422
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1"))) {
|
|
423
|
+
throw new Error("core URL must be https:// (the API key is sent to it)");
|
|
424
|
+
}
|
|
425
|
+
return async (grant) => {
|
|
426
|
+
const res = await fetch(`${base}/v1/grant/redeem`, {
|
|
427
|
+
method: "POST",
|
|
428
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
429
|
+
body: JSON.stringify({ grant })
|
|
430
|
+
});
|
|
431
|
+
if (res.status === 200)
|
|
432
|
+
return true;
|
|
433
|
+
if (res.status === 409)
|
|
434
|
+
return false;
|
|
435
|
+
throw new Error(`redeem -> ${res.status}`);
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
var GrantEnforcingExecutor = class {
|
|
439
|
+
rail;
|
|
440
|
+
used = /* @__PURE__ */ new Set();
|
|
441
|
+
pub;
|
|
442
|
+
redeem;
|
|
443
|
+
constructor(publicKeyPem2, rail, opts) {
|
|
444
|
+
this.rail = rail;
|
|
445
|
+
this.pub = createPublicKey(publicKeyPem2);
|
|
446
|
+
this.redeem = opts?.redeem;
|
|
447
|
+
}
|
|
448
|
+
async execute(req, grant) {
|
|
449
|
+
if (!grant)
|
|
450
|
+
return { status: "REFUSED", reason: "missing_grant" };
|
|
451
|
+
const dot = grant.indexOf(".");
|
|
452
|
+
if (dot < 0)
|
|
453
|
+
return { status: "REFUSED", reason: "malformed_grant" };
|
|
454
|
+
const body = grant.slice(0, dot);
|
|
455
|
+
const sig = grant.slice(dot + 1);
|
|
456
|
+
if (!verify(this.pub, body, sig))
|
|
457
|
+
return { status: "REFUSED", reason: "invalid_signature" };
|
|
458
|
+
let p;
|
|
459
|
+
try {
|
|
460
|
+
p = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
461
|
+
} catch {
|
|
462
|
+
return { status: "REFUSED", reason: "undecodable_grant" };
|
|
463
|
+
}
|
|
464
|
+
if (Date.now() > p.exp)
|
|
465
|
+
return { status: "REFUSED", reason: "grant_expired" };
|
|
466
|
+
if (this.used.has(p.decisionId))
|
|
467
|
+
return { status: "REFUSED", reason: "grant_replayed" };
|
|
468
|
+
if (p.payee !== req.payee)
|
|
469
|
+
return { status: "REFUSED", reason: "payee_mismatch" };
|
|
470
|
+
if (p.amount !== req.amount)
|
|
471
|
+
return { status: "REFUSED", reason: "amount_mismatch" };
|
|
472
|
+
if (p.currency !== req.currency)
|
|
473
|
+
return { status: "REFUSED", reason: "currency_mismatch" };
|
|
474
|
+
if (req.invoiceRef != null && req.invoiceRef !== (p.invoiceRef ?? null)) {
|
|
475
|
+
return { status: "REFUSED", reason: "invoice_mismatch" };
|
|
476
|
+
}
|
|
477
|
+
if (this.redeem) {
|
|
478
|
+
let first;
|
|
479
|
+
try {
|
|
480
|
+
first = await this.redeem(grant);
|
|
481
|
+
} catch {
|
|
482
|
+
return { status: "REFUSED", reason: "redeem_unavailable" };
|
|
483
|
+
}
|
|
484
|
+
if (!first)
|
|
485
|
+
return { status: "REFUSED", reason: "grant_replayed" };
|
|
486
|
+
}
|
|
487
|
+
this.used.add(p.decisionId);
|
|
488
|
+
const { railRef } = await this.rail.execute(req);
|
|
489
|
+
return { status: "EXECUTED", railRef, decisionId: p.decisionId };
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
432
493
|
// src/config.ts
|
|
433
494
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
434
495
|
import { homedir } from "node:os";
|
|
@@ -510,7 +571,7 @@ function resolveMandateRules(cfg) {
|
|
|
510
571
|
}
|
|
511
572
|
|
|
512
573
|
// src/telemetry.ts
|
|
513
|
-
var CLIENT_VERSION = true ? "0.1.
|
|
574
|
+
var CLIENT_VERSION = true ? "0.1.12" : "dev";
|
|
514
575
|
var currentShell = "mcp";
|
|
515
576
|
function setTelemetryShell(shell) {
|
|
516
577
|
currentShell = shell;
|
|
@@ -633,7 +694,7 @@ function requestUpgrade() {
|
|
|
633
694
|
}
|
|
634
695
|
|
|
635
696
|
// src/provision.ts
|
|
636
|
-
var CLIENT_VERSION2 = true ? "0.1.
|
|
697
|
+
var CLIENT_VERSION2 = true ? "0.1.12" : "dev";
|
|
637
698
|
function provisionEnabled() {
|
|
638
699
|
const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
|
|
639
700
|
return !(v === "1" || v === "true" || v === "yes");
|
package/package.json
CHANGED