@fidacy/mcp 0.3.0 → 0.3.2

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 01f0734: Telemetry now flushes on process exit. The 2s flush timer is unref'd so a short-lived process could exit with a full buffer and silently lose it, which is exactly how plugin-channel installs went uncounted. A beforeExit hook sends the last batch, best-effort as always.
8
+
3
9
  ## 0.3.0
4
10
 
5
11
  ### Minor Changes
package/dist/core.js CHANGED
@@ -85,10 +85,23 @@ function lookalikePayee(payee, allowed) {
85
85
  }
86
86
  return null;
87
87
  }
88
+ function validateMandateCaps(mandate) {
89
+ const bad = (v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0;
90
+ if (!mandate.allow || typeof mandate.allow !== "object")
91
+ return "invalid_mandate:missing_allow";
92
+ if (bad(mandate.allow.perTxMax))
93
+ return `invalid_mandate_cap:perTxMax=${String(mandate.allow.perTxMax)}`;
94
+ if (bad(mandate.allow.maxTotal))
95
+ return `invalid_mandate_cap:maxTotal=${String(mandate.allow.maxTotal)}`;
96
+ return null;
97
+ }
88
98
  function evaluate(mandate, req, spentSoFar) {
89
99
  const now = Date.now();
90
100
  if (mandate.revoked)
91
101
  return "mandate_revoked";
102
+ const badCap = validateMandateCaps(mandate);
103
+ if (badCap)
104
+ return badCap;
92
105
  if (now < Date.parse(mandate.window.notBefore))
93
106
  return "before_mandate_window";
94
107
  if (now > Date.parse(mandate.window.notAfter))
@@ -450,18 +463,29 @@ function readConfig() {
450
463
  function resolveMandateRules(cfg) {
451
464
  const m = cfg?.mandate ?? {};
452
465
  const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
453
- const envNum = (v) => v === void 0 || v === "" ? void 0 : Number(v);
466
+ const envNum = (name, v) => {
467
+ if (v === void 0 || v.trim() === "") return void 0;
468
+ const n = Number(v);
469
+ if (!Number.isFinite(n) || n <= 0) {
470
+ console.error(
471
+ `[fidacy] ${name}="${v}" is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write digits only, with no thousands separator, currency symbol or unit (e.g. ${name}=2500).`
472
+ );
473
+ return void 0;
474
+ }
475
+ return n;
476
+ };
477
+ const fileNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
454
478
  return {
455
479
  payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
456
480
  categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
457
481
  currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
458
- perTxMax: envNum(process.env.FIDACY_PER_TX_MAX) ?? m.perTxMax ?? 2500,
459
- maxTotal: envNum(process.env.FIDACY_MAX_TOTAL) ?? m.maxTotal ?? 1e4
482
+ perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum(m.perTxMax) ?? 2500,
483
+ maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum(m.maxTotal) ?? 1e4
460
484
  };
461
485
  }
462
486
 
463
487
  // src/telemetry.ts
464
- var CLIENT_VERSION = true ? "0.3.0" : "dev";
488
+ var CLIENT_VERSION = true ? "0.3.2" : "dev";
465
489
  function bandOf(amount) {
466
490
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
467
491
  if (amount < 10) return "lt10";
@@ -497,6 +521,14 @@ function anonId() {
497
521
  }
498
522
  var buffer = [];
499
523
  var flushTimer = null;
524
+ var exitHookArmed = false;
525
+ function armExitFlush() {
526
+ if (exitHookArmed || typeof process === "undefined" || typeof process.once !== "function") return;
527
+ exitHookArmed = true;
528
+ process.once("beforeExit", () => {
529
+ if (buffer.length > 0) void flush();
530
+ });
531
+ }
500
532
  function resultOf(status, violatedRule) {
501
533
  if (status === "ALLOW") return "allow";
502
534
  const r = violatedRule ?? "";
@@ -511,6 +543,7 @@ function resultOf(status, violatedRule) {
511
543
  }
512
544
  function record(type, result, band, capRatio, action) {
513
545
  if (!telemetryEnabled()) return;
546
+ armExitFlush();
514
547
  buffer.push({
515
548
  type,
516
549
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7,4 +7,15 @@ import { Mandate, PaymentRequest } from "./types.js";
7
7
  * min length so short, genuinely-distinct payees don't false-positive.
8
8
  */
9
9
  export declare function lookalikePayee(payee: string, allowed: string[]): string | null;
10
+ /**
11
+ * The MANDATE's own caps must be finite positive numbers, for exactly the reason
12
+ * validateRequest() checks the request's amount: `amount > NaN` is false, so a
13
+ * non-numeric cap does not raise an error — it SILENTLY DISABLES the cap and the
14
+ * firewall fails OPEN. The caps arrive from human-authored surfaces (env vars,
15
+ * config.json, FIDACY_MANDATE_JSON), where `2,500`, `$2500` and `2500 USD` are
16
+ * all normal things to type and all coerce to NaN. Verified 2026-07-24: each of
17
+ * those let a 9,999 payment through a mandate whose author believed it was
18
+ * capped at 2,500. A cap we cannot enforce must deny, never allow.
19
+ */
20
+ export declare function validateMandateCaps(mandate: Mandate): string | null;
10
21
  export declare function evaluate(mandate: Mandate, req: PaymentRequest, spentSoFar: number): string | null;
package/dist/index.js CHANGED
@@ -92,10 +92,23 @@ function lookalikePayee(payee, allowed) {
92
92
  }
93
93
  return null;
94
94
  }
95
+ function validateMandateCaps(mandate) {
96
+ const bad = (v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0;
97
+ if (!mandate.allow || typeof mandate.allow !== "object")
98
+ return "invalid_mandate:missing_allow";
99
+ if (bad(mandate.allow.perTxMax))
100
+ return `invalid_mandate_cap:perTxMax=${String(mandate.allow.perTxMax)}`;
101
+ if (bad(mandate.allow.maxTotal))
102
+ return `invalid_mandate_cap:maxTotal=${String(mandate.allow.maxTotal)}`;
103
+ return null;
104
+ }
95
105
  function evaluate(mandate, req, spentSoFar) {
96
106
  const now = Date.now();
97
107
  if (mandate.revoked)
98
108
  return "mandate_revoked";
109
+ const badCap = validateMandateCaps(mandate);
110
+ if (badCap)
111
+ return badCap;
99
112
  if (now < Date.parse(mandate.window.notBefore))
100
113
  return "before_mandate_window";
101
114
  if (now > Date.parse(mandate.window.notAfter))
@@ -479,18 +492,29 @@ function ensureState() {
479
492
  function resolveMandateRules(cfg) {
480
493
  const m = cfg?.mandate ?? {};
481
494
  const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
482
- const envNum = (v) => v === void 0 || v === "" ? void 0 : Number(v);
495
+ const envNum = (name, v) => {
496
+ if (v === void 0 || v.trim() === "") return void 0;
497
+ const n = Number(v);
498
+ if (!Number.isFinite(n) || n <= 0) {
499
+ console.error(
500
+ `[fidacy] ${name}="${v}" is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write digits only, with no thousands separator, currency symbol or unit (e.g. ${name}=2500).`
501
+ );
502
+ return void 0;
503
+ }
504
+ return n;
505
+ };
506
+ const fileNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
483
507
  return {
484
508
  payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
485
509
  categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
486
510
  currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
487
- perTxMax: envNum(process.env.FIDACY_PER_TX_MAX) ?? m.perTxMax ?? 2500,
488
- maxTotal: envNum(process.env.FIDACY_MAX_TOTAL) ?? m.maxTotal ?? 1e4
511
+ perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum(m.perTxMax) ?? 2500,
512
+ maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum(m.maxTotal) ?? 1e4
489
513
  };
490
514
  }
491
515
 
492
516
  // src/telemetry.ts
493
- var CLIENT_VERSION = true ? "0.3.0" : "dev";
517
+ var CLIENT_VERSION = true ? "0.3.2" : "dev";
494
518
  function bandOf(amount) {
495
519
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
496
520
  if (amount < 10) return "lt10";
@@ -526,6 +550,14 @@ function anonId() {
526
550
  }
527
551
  var buffer = [];
528
552
  var flushTimer = null;
553
+ var exitHookArmed = false;
554
+ function armExitFlush() {
555
+ if (exitHookArmed || typeof process === "undefined" || typeof process.once !== "function") return;
556
+ exitHookArmed = true;
557
+ process.once("beforeExit", () => {
558
+ if (buffer.length > 0) void flush();
559
+ });
560
+ }
529
561
  function resultOf(status, violatedRule) {
530
562
  if (status === "ALLOW") return "allow";
531
563
  const r = violatedRule ?? "";
@@ -540,6 +572,7 @@ function resultOf(status, violatedRule) {
540
572
  }
541
573
  function record(type, result, band, capRatio, action) {
542
574
  if (!telemetryEnabled()) return;
575
+ armExitFlush();
543
576
  buffer.push({
544
577
  type,
545
578
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -836,7 +869,7 @@ async function findArtifacts(sha2562, cfg) {
836
869
  }
837
870
 
838
871
  // src/provision.ts
839
- var CLIENT_VERSION2 = true ? "0.3.0" : "dev";
872
+ var CLIENT_VERSION2 = true ? "0.3.2" : "dev";
840
873
  function provisionEnabled() {
841
874
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
842
875
  return !(v === "1" || v === "true" || v === "yes");
@@ -1267,7 +1300,7 @@ function renderAlertLine(alerts) {
1267
1300
  var state = ensureState();
1268
1301
  var core = makeCore();
1269
1302
  var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
1270
- var SERVER_VERSION = true ? "0.3.0" : "dev";
1303
+ var SERVER_VERSION = true ? "0.3.2" : "dev";
1271
1304
  var bootClaim = claimUrl();
1272
1305
  var server = new McpServer(
1273
1306
  { name: "fidacy", version: SERVER_VERSION },
package/dist/lib.d.ts CHANGED
@@ -3,7 +3,7 @@ export { makeCore } from "./core.js";
3
3
  export { ensureState, readConfig } from "./config.js";
4
4
  export { requestUpgrade, upgradeUrl } from "./upgrade.js";
5
5
  export { autoProvision } from "./provision.js";
6
- export { recordInstall, recordAgentActive, setTelemetryShell } from "./telemetry.js";
6
+ export { recordInstall, recordAgentActive, setTelemetryShell, flush as flushTelemetry } from "./telemetry.js";
7
7
  export { decisionNudge, weekOneBootNudge, nudgeOnce, installAgeDays, claimUrl, noKeyCta } from "./nudges.js";
8
8
  export { FREE_DECISIONS, activationGate, activationBootLine, trialCountdownLine, hasEngineKey, remainingFree } from "./activation.js";
9
9
  export { summarize, renderSummary, toRows, renderRows, explainRule, withinDays } from "./reporting.js";
package/dist/lib.js CHANGED
@@ -123,10 +123,23 @@ function lookalikePayee(payee, allowed) {
123
123
  }
124
124
  return null;
125
125
  }
126
+ function validateMandateCaps(mandate) {
127
+ const bad = (v) => typeof v !== "number" || !Number.isFinite(v) || v <= 0;
128
+ if (!mandate.allow || typeof mandate.allow !== "object")
129
+ return "invalid_mandate:missing_allow";
130
+ if (bad(mandate.allow.perTxMax))
131
+ return `invalid_mandate_cap:perTxMax=${String(mandate.allow.perTxMax)}`;
132
+ if (bad(mandate.allow.maxTotal))
133
+ return `invalid_mandate_cap:maxTotal=${String(mandate.allow.maxTotal)}`;
134
+ return null;
135
+ }
126
136
  function evaluate(mandate, req, spentSoFar) {
127
137
  const now = Date.now();
128
138
  if (mandate.revoked)
129
139
  return "mandate_revoked";
140
+ const badCap = validateMandateCaps(mandate);
141
+ if (badCap)
142
+ return badCap;
130
143
  if (now < Date.parse(mandate.window.notBefore))
131
144
  return "before_mandate_window";
132
145
  if (now > Date.parse(mandate.window.notAfter))
@@ -602,18 +615,29 @@ function ensureState() {
602
615
  function resolveMandateRules(cfg) {
603
616
  const m = cfg?.mandate ?? {};
604
617
  const envList = (v) => v === void 0 ? void 0 : v.split(",").map((s) => s.trim()).filter(Boolean);
605
- const envNum = (v) => v === void 0 || v === "" ? void 0 : Number(v);
618
+ const envNum = (name, v) => {
619
+ if (v === void 0 || v.trim() === "") return void 0;
620
+ const n = Number(v);
621
+ if (!Number.isFinite(n) || n <= 0) {
622
+ console.error(
623
+ `[fidacy] ${name}="${v}" is not a positive number, so it cannot be enforced as a cap. Ignoring it and using the safe default. Write digits only, with no thousands separator, currency symbol or unit (e.g. ${name}=2500).`
624
+ );
625
+ return void 0;
626
+ }
627
+ return n;
628
+ };
629
+ const fileNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
606
630
  return {
607
631
  payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
608
632
  categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
609
633
  currency: process.env.FIDACY_CURRENCY ?? m.currency ?? "USD",
610
- perTxMax: envNum(process.env.FIDACY_PER_TX_MAX) ?? m.perTxMax ?? 2500,
611
- maxTotal: envNum(process.env.FIDACY_MAX_TOTAL) ?? m.maxTotal ?? 1e4
634
+ perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum(m.perTxMax) ?? 2500,
635
+ maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum(m.maxTotal) ?? 1e4
612
636
  };
613
637
  }
614
638
 
615
639
  // src/telemetry.ts
616
- var CLIENT_VERSION = true ? "0.3.0" : "dev";
640
+ var CLIENT_VERSION = true ? "0.3.2" : "dev";
617
641
  function bandOf(amount) {
618
642
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
619
643
  if (amount < 10) return "lt10";
@@ -652,6 +676,14 @@ function anonId() {
652
676
  }
653
677
  var buffer = [];
654
678
  var flushTimer = null;
679
+ var exitHookArmed = false;
680
+ function armExitFlush() {
681
+ if (exitHookArmed || typeof process === "undefined" || typeof process.once !== "function") return;
682
+ exitHookArmed = true;
683
+ process.once("beforeExit", () => {
684
+ if (buffer.length > 0) void flush();
685
+ });
686
+ }
655
687
  function resultOf(status, violatedRule) {
656
688
  if (status === "ALLOW") return "allow";
657
689
  const r = violatedRule ?? "";
@@ -666,6 +698,7 @@ function resultOf(status, violatedRule) {
666
698
  }
667
699
  function record(type, result, band, capRatio, action) {
668
700
  if (!telemetryEnabled()) return;
701
+ armExitFlush();
669
702
  buffer.push({
670
703
  type,
671
704
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -792,7 +825,7 @@ function requestUpgrade() {
792
825
  }
793
826
 
794
827
  // src/provision.ts
795
- var CLIENT_VERSION2 = true ? "0.3.0" : "dev";
828
+ var CLIENT_VERSION2 = true ? "0.3.2" : "dev";
796
829
  function provisionEnabled() {
797
830
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
798
831
  return !(v === "1" || v === "true" || v === "yes");
@@ -1156,6 +1189,7 @@ export {
1156
1189
  ensureState,
1157
1190
  evaluate,
1158
1191
  explainRule,
1192
+ flush as flushTelemetry,
1159
1193
  hasEngineKey,
1160
1194
  httpRedeem,
1161
1195
  installAgeDays,
@@ -1183,6 +1217,7 @@ export {
1183
1217
  toRows,
1184
1218
  trialCountdownLine,
1185
1219
  upgradeUrl,
1220
+ validateMandateCaps,
1186
1221
  validateRequest,
1187
1222
  verify,
1188
1223
  verifyGrant,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Fidacy action firewall for AI agents. Mandate-gated payment authorization as an MCP server.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Fidacy (ZeepCode Group Technology LLC) <hello@fidacy.com> (https://fidacy.com)",
@@ -61,6 +61,7 @@
61
61
  "@types/node": "^22.10.0",
62
62
  "tsx": "^4.19.2",
63
63
  "typescript": "^5.7.2",
64
+ "vitest": "^3.2.4",
64
65
  "@fidacy/firewall": "0.1.1"
65
66
  },
66
67
  "mcpName": "com.fidacy/mcp",
@@ -71,6 +72,8 @@
71
72
  "scripts": {
72
73
  "build": "node scripts/bundle.mjs",
73
74
  "dev": "tsx watch src/index.ts",
74
- "start": "node dist/index.js"
75
+ "start": "node dist/index.js",
76
+ "typecheck": "tsc --noEmit",
77
+ "test": "vitest run"
75
78
  }
76
79
  }