@fidacy/mcp 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3
4
+
5
+ ### Minor Changes
6
+
7
+ - The server now presents itself as **Fidacy AI Agent Firewall** (`fidacy-ai-agent-firewall`, title "Fidacy AI Agent Firewall") instead of the bare `fidacy`. The npm package is unchanged — `npx -y @fidacy/mcp` keeps working exactly as before, and no config needs editing. This is the name the product is sold under, so it is the name your client should show.
8
+
9
+ ### Patch Changes
10
+
11
+ - Fix the wall an existing customer hits. When an install that already carries an API key exhausted its monthly allowance (or reached the spending cap it set), the server answered with an invitation to create a free account — the account they already had — and threw away everything the engine said about it. It now reports what actually happened, names the pay-as-you-go price and links straight to billing, and makes explicit that the local firewall keeps deciding and blocking throughout: only server-signed verdicts and anchored proofs pause, never the protection.
12
+
13
+ ## 0.3.2
14
+
15
+ ### Patch Changes
16
+
17
+ - Security: a spending cap that failed to parse silently stopped capping. Cap values read from `FIDACY_PER_TX_MAX` / `FIDACY_MAX_TOTAL` or from `config.json` went through a bare `Number()`, so ordinary ways of writing an amount — `2,500`, `$2500`, `2500 USD` — became `NaN`. Because `amount > NaN` is false, the comparison did not raise: the cap simply stopped existing, and a firewall whose author believed it was capped at 2,500 would authorize 9,999. Two fixes: the decision engine now DENIES any mandate whose caps are not finite positive numbers (`invalid_mandate_cap`), so an unenforceable cap can never allow a payment on any path, and a cap that does not parse is now refused with an explicit message telling you what to write instead, falling back to the safe default rather than to no cap. Upgrade if you set either cap from an environment variable.
18
+
3
19
  ## 0.3.1
4
20
 
5
21
  ### Patch Changes
@@ -1,3 +1,5 @@
1
+ import { hasEngineKey } from "./config.js";
2
+ export { hasEngineKey };
1
3
  /**
2
4
  * ACTIVATION GATE — the anonymous trial that ends in a free API key.
3
5
  *
@@ -16,8 +18,6 @@
16
18
  * real client needs anyway (it powers assess_action / anchor_artifact).
17
19
  */
18
20
  export declare const FREE_DECISIONS = 20;
19
- /** True when the install carries an operator-set engine key (env or shell config). */
20
- export declare function hasEngineKey(keyOverride?: string): boolean;
21
21
  /**
22
22
  * Payment-gate decisions taken so far on this install: the on-disk counter OR
23
23
  * the in-process one, whichever is higher. The in-process floor is what keeps
package/dist/assess.d.ts CHANGED
@@ -42,16 +42,30 @@ export interface RejectionReason {
42
42
  * request body. `type` is the stable engine error code (or a client code), and
43
43
  * `status` is the HTTP status (0 for network/timeout).
44
44
  */
45
+ /**
46
+ * What a 402 actually says. The engine returns `reason` (quota grace expired vs
47
+ * spending cap reached), a human-readable `detail` and the `billing_url` that
48
+ * resolves it. All of it used to be dropped on the floor, so an operator who had
49
+ * ALREADY signed up was told to go sign up — at the one moment they were asking
50
+ * to buy more. Carry it through and say the true thing instead.
51
+ */
52
+ export interface BillingBlock {
53
+ reason?: string;
54
+ detail?: string;
55
+ billingUrl?: string;
56
+ }
45
57
  export declare class AssessError extends Error {
46
58
  readonly type: string;
47
59
  readonly status: number;
48
60
  readonly details?: unknown;
49
61
  readonly rejection_reasons?: RejectionReason[];
62
+ readonly billing?: BillingBlock;
50
63
  constructor(opts: {
51
64
  type: string;
52
65
  status: number;
53
66
  details?: unknown;
54
67
  rejection_reasons?: RejectionReason[];
68
+ billing?: BillingBlock;
55
69
  });
56
70
  }
57
71
  export interface AssessConfig {
package/dist/assess.js CHANGED
@@ -4,6 +4,7 @@ var AssessError = class extends Error {
4
4
  status;
5
5
  details;
6
6
  rejection_reasons;
7
+ billing;
7
8
  constructor(opts) {
8
9
  super(`Fidacy assess error (${opts.type}, HTTP ${opts.status})`);
9
10
  this.name = "AssessError";
@@ -11,6 +12,7 @@ var AssessError = class extends Error {
11
12
  this.status = opts.status;
12
13
  this.details = opts.details;
13
14
  this.rejection_reasons = opts.rejection_reasons;
15
+ this.billing = opts.billing;
14
16
  }
15
17
  };
16
18
  var DEFAULT_TIMEOUT = 1e4;
@@ -106,7 +108,12 @@ async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
106
108
  const type = isRecord(parsed) && typeof parsed.error === "string" && parsed.error || `http_${res.status}`;
107
109
  const details = isRecord(parsed) ? parsed.details : void 0;
108
110
  const rejection_reasons = isRecord(parsed) && Array.isArray(parsed.rejection_reasons) ? parsed.rejection_reasons : void 0;
109
- throw new AssessError({ type, status: res.status, details, rejection_reasons });
111
+ const billing = res.status === 402 && isRecord(parsed) ? {
112
+ reason: typeof parsed.reason === "string" ? parsed.reason : void 0,
113
+ detail: typeof parsed.detail === "string" ? parsed.detail : void 0,
114
+ billingUrl: typeof parsed.billing_url === "string" ? parsed.billing_url : void 0
115
+ } : void 0;
116
+ throw new AssessError({ type, status: res.status, details, rejection_reasons, billing });
110
117
  }
111
118
  if (!isRecord(parsed)) {
112
119
  throw new AssessError({ type: "invalid_response", status: res.status });
package/dist/config.d.ts CHANGED
@@ -37,6 +37,15 @@ export interface FidacyConfig {
37
37
  /** Payment-gate decisions taken on this install (milestone nudges). */
38
38
  decisions_count?: number;
39
39
  }
40
+ /**
41
+ * True when the install carries an operator-set engine key (env, or the plugin's
42
+ * engineApiKey config passed as an override).
43
+ *
44
+ * Lives here rather than in activation.ts because nudges.ts needs it too, and
45
+ * activation.ts already imports nudges.ts — importing back would be a cycle.
46
+ * Reading the environment is this module's job anyway.
47
+ */
48
+ export declare function hasEngineKey(keyOverride?: string): boolean;
40
49
  /** Config dir: FIDACY_CONFIG_DIR override, else ~/.fidacy (XDG-friendly enough). */
41
50
  export declare function configDir(): string;
42
51
  export declare function configPath(): string;
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.1" : "dev";
488
+ var CLIENT_VERSION = true ? "0.3.3" : "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";
@@ -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))
@@ -419,6 +432,9 @@ import {
419
432
  readFileSync,
420
433
  writeFileSync
421
434
  } from "node:fs";
435
+ function hasEngineKey(keyOverride) {
436
+ return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
437
+ }
422
438
  function configDir() {
423
439
  return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
424
440
  }
@@ -479,18 +495,29 @@ function ensureState() {
479
495
  function resolveMandateRules(cfg) {
480
496
  const m = cfg?.mandate ?? {};
481
497
  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);
498
+ const envNum = (name, v) => {
499
+ if (v === void 0 || v.trim() === "") return void 0;
500
+ const n = Number(v);
501
+ if (!Number.isFinite(n) || n <= 0) {
502
+ console.error(
503
+ `[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).`
504
+ );
505
+ return void 0;
506
+ }
507
+ return n;
508
+ };
509
+ const fileNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
483
510
  return {
484
511
  payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
485
512
  categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
486
513
  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
514
+ perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum(m.perTxMax) ?? 2500,
515
+ maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum(m.maxTotal) ?? 1e4
489
516
  };
490
517
  }
491
518
 
492
519
  // src/telemetry.ts
493
- var CLIENT_VERSION = true ? "0.3.1" : "dev";
520
+ var CLIENT_VERSION = true ? "0.3.3" : "dev";
494
521
  function bandOf(amount) {
495
522
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
496
523
  if (amount < 10) return "lt10";
@@ -669,6 +696,7 @@ var AssessError = class extends Error {
669
696
  status;
670
697
  details;
671
698
  rejection_reasons;
699
+ billing;
672
700
  constructor(opts) {
673
701
  super(`Fidacy assess error (${opts.type}, HTTP ${opts.status})`);
674
702
  this.name = "AssessError";
@@ -676,6 +704,7 @@ var AssessError = class extends Error {
676
704
  this.status = opts.status;
677
705
  this.details = opts.details;
678
706
  this.rejection_reasons = opts.rejection_reasons;
707
+ this.billing = opts.billing;
679
708
  }
680
709
  };
681
710
  var DEFAULT_TIMEOUT = 1e4;
@@ -771,7 +800,12 @@ async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
771
800
  const type = isRecord(parsed) && typeof parsed.error === "string" && parsed.error || `http_${res.status}`;
772
801
  const details = isRecord(parsed) ? parsed.details : void 0;
773
802
  const rejection_reasons = isRecord(parsed) && Array.isArray(parsed.rejection_reasons) ? parsed.rejection_reasons : void 0;
774
- throw new AssessError({ type, status: res.status, details, rejection_reasons });
803
+ const billing = res.status === 402 && isRecord(parsed) ? {
804
+ reason: typeof parsed.reason === "string" ? parsed.reason : void 0,
805
+ detail: typeof parsed.detail === "string" ? parsed.detail : void 0,
806
+ billingUrl: typeof parsed.billing_url === "string" ? parsed.billing_url : void 0
807
+ } : void 0;
808
+ throw new AssessError({ type, status: res.status, details, rejection_reasons, billing });
775
809
  }
776
810
  if (!isRecord(parsed)) {
777
811
  throw new AssessError({ type: "invalid_response", status: res.status });
@@ -845,7 +879,7 @@ async function findArtifacts(sha2562, cfg) {
845
879
  }
846
880
 
847
881
  // src/provision.ts
848
- var CLIENT_VERSION2 = true ? "0.3.1" : "dev";
882
+ var CLIENT_VERSION2 = true ? "0.3.3" : "dev";
849
883
  function provisionEnabled() {
850
884
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
851
885
  return !(v === "1" || v === "true" || v === "yes");
@@ -952,10 +986,15 @@ function decisionNudge(status, violatedRule, upgradeToolName) {
952
986
  }
953
987
  return null;
954
988
  }
955
- function hostedWallCta(feature) {
989
+ function hostedWallCta(feature, billing) {
990
+ const noun = feature === "anchor" ? "Bitcoin-anchored proofs" : "server-signed verdicts";
991
+ if (hasEngineKey()) {
992
+ const where = billing?.billingUrl ?? "https://app.fidacy.com/billing";
993
+ const what = billing?.reason === "spending_cap_reached" ? `This account hit the monthly spending cap you set, so ${noun} are paused.` : billing?.detail ?? `This account's included ${noun} for the month are used up.`;
994
+ return `${what} The local firewall keeps deciding and blocking as normal \u2014 this only pauses ${noun}. For your operator: resolve it at ${where}. Pay-as-you-go needs no plan and starts at $0.04 per verdict, dropping to $0.025 past 50k and $0.015 past 250k; committed plans price below that curve.`;
995
+ }
956
996
  const url = claimUrl();
957
997
  const cta = url ? `open ${url} \u2014 free account, this install's history migrates` : `call the upgrade tool \u2014 free account, your history migrates`;
958
- const noun = feature === "anchor" ? "Bitcoin-anchored proofs" : "server-signed verdicts";
959
998
  if (nudgeOnce("hosted_wall")) {
960
999
  return `You've used this install's free allowance for ${noun} \u2014 the proofs anyone can re-verify at fidacy.com/verify, court-grade, not a local-only check. Keep them flowing: for your operator, ${cta}.`;
961
1000
  }
@@ -975,9 +1014,6 @@ function weekOneBootNudge(upgradeToolName) {
975
1014
 
976
1015
  // src/activation.ts
977
1016
  var FREE_DECISIONS = 20;
978
- function hasEngineKey(keyOverride) {
979
- return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
980
- }
981
1017
  function decisionsUsed() {
982
1018
  return Math.max(readConfig()?.decisions_count ?? 0, sessionDecisionCount());
983
1019
  }
@@ -1150,8 +1186,8 @@ function rows(s) {
1150
1186
  return out;
1151
1187
  }
1152
1188
  function renderBanner(s) {
1153
- const title = "FIDACY \xB7 the payment firewall for AI agents";
1154
- const sub = `signs the YES and the NO \xB7 v${s.version}`;
1189
+ const title = "FIDACY AI AGENT FIREWALL";
1190
+ const sub = `blocks the payment, signs the YES and the NO \xB7 v${s.version}`;
1155
1191
  const body = rows(s);
1156
1192
  if (!fancy()) {
1157
1193
  const lines2 = [
@@ -1276,10 +1312,14 @@ function renderAlertLine(alerts) {
1276
1312
  var state = ensureState();
1277
1313
  var core = makeCore();
1278
1314
  var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
1279
- var SERVER_VERSION = true ? "0.3.1" : "dev";
1315
+ var SERVER_VERSION = true ? "0.3.3" : "dev";
1280
1316
  var bootClaim = claimUrl();
1281
1317
  var server = new McpServer(
1282
- { name: "fidacy", version: SERVER_VERSION },
1318
+ // The commercial identity. `name` is the stable slug hosts key on; `title` is
1319
+ // what a human reads in the client's server list. The npm package stays
1320
+ // @fidacy/mcp — renaming the package would split the install base and the
1321
+ // download history for nothing, since what people SEE is this.
1322
+ { name: "fidacy-ai-agent-firewall", title: "Fidacy AI Agent Firewall", version: SERVER_VERSION },
1283
1323
  {
1284
1324
  instructions: bootClaim ? `Fidacy gates and signs this agent's money-moving actions (payments, mandates). Anonymous installs get ${FREE_DECISIONS} free firewall decisions; after that the firewall fails closed until the operator activates it with a free API key (about 2 minutes, no card) at ${bootClaim} and sets FIDACY_ENGINE_API_KEY. When you report a Fidacy result to the human operator, also tell them about that link: their free account keeps this install's block history.` : `Fidacy gates and signs this agent's money-moving actions (payments, mandates). Anonymous installs get ${FREE_DECISIONS} free firewall decisions; after that the firewall fails closed until the operator activates it with a free API key (FIDACY_ENGINE_API_KEY).`
1285
1325
  }
@@ -1504,7 +1544,7 @@ server.registerTool(
1504
1544
  } catch (e) {
1505
1545
  if (e instanceof AssessError) {
1506
1546
  if (e.status === 402) {
1507
- return { content: [{ type: "text", text: hostedWallCta("verdict") }], isError: true };
1547
+ return { content: [{ type: "text", text: hostedWallCta("verdict", e.billing) }], isError: true };
1508
1548
  }
1509
1549
  const reasons = e.rejection_reasons?.length ? " (" + e.rejection_reasons.map((x) => x.key).join(",") + ")" : "";
1510
1550
  return {
@@ -1569,7 +1609,7 @@ server.registerTool(
1569
1609
  } catch (e) {
1570
1610
  if (e instanceof AssessError) {
1571
1611
  if (e.status === 402) {
1572
- return { content: [{ type: "text", text: hostedWallCta("anchor") }], isError: true };
1612
+ return { content: [{ type: "text", text: hostedWallCta("anchor", e.billing) }], isError: true };
1573
1613
  }
1574
1614
  return { content: [{ type: "text", text: `ANCHOR ${e.status}: ${e.type}` }], isError: true };
1575
1615
  }
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))
@@ -542,6 +555,9 @@ import {
542
555
  readFileSync,
543
556
  writeFileSync
544
557
  } from "node:fs";
558
+ function hasEngineKey(keyOverride) {
559
+ return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
560
+ }
545
561
  function configDir() {
546
562
  return process.env.FIDACY_CONFIG_DIR ?? join(homedir(), ".fidacy");
547
563
  }
@@ -602,18 +618,29 @@ function ensureState() {
602
618
  function resolveMandateRules(cfg) {
603
619
  const m = cfg?.mandate ?? {};
604
620
  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);
621
+ const envNum = (name, v) => {
622
+ if (v === void 0 || v.trim() === "") return void 0;
623
+ const n = Number(v);
624
+ if (!Number.isFinite(n) || n <= 0) {
625
+ console.error(
626
+ `[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).`
627
+ );
628
+ return void 0;
629
+ }
630
+ return n;
631
+ };
632
+ const fileNum = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
606
633
  return {
607
634
  payees: envList(process.env.FIDACY_ALLOW_PAYEES) ?? m.payees ?? [],
608
635
  categories: envList(process.env.FIDACY_ALLOW_CATEGORIES) ?? m.categories ?? ["*"],
609
636
  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
637
+ perTxMax: envNum("FIDACY_PER_TX_MAX", process.env.FIDACY_PER_TX_MAX) ?? fileNum(m.perTxMax) ?? 2500,
638
+ maxTotal: envNum("FIDACY_MAX_TOTAL", process.env.FIDACY_MAX_TOTAL) ?? fileNum(m.maxTotal) ?? 1e4
612
639
  };
613
640
  }
614
641
 
615
642
  // src/telemetry.ts
616
- var CLIENT_VERSION = true ? "0.3.1" : "dev";
643
+ var CLIENT_VERSION = true ? "0.3.3" : "dev";
617
644
  function bandOf(amount) {
618
645
  if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0) return void 0;
619
646
  if (amount < 10) return "lt10";
@@ -801,7 +828,7 @@ function requestUpgrade() {
801
828
  }
802
829
 
803
830
  // src/provision.ts
804
- var CLIENT_VERSION2 = true ? "0.3.1" : "dev";
831
+ var CLIENT_VERSION2 = true ? "0.3.3" : "dev";
805
832
  function provisionEnabled() {
806
833
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
807
834
  return !(v === "1" || v === "true" || v === "yes");
@@ -910,9 +937,6 @@ function weekOneBootNudge(upgradeToolName) {
910
937
 
911
938
  // src/activation.ts
912
939
  var FREE_DECISIONS = 20;
913
- function hasEngineKey(keyOverride) {
914
- return Boolean((keyOverride ?? process.env.FIDACY_ENGINE_API_KEY ?? "").trim());
915
- }
916
940
  function decisionsUsed() {
917
941
  return Math.max(readConfig()?.decisions_count ?? 0, sessionDecisionCount());
918
942
  }
@@ -1193,6 +1217,7 @@ export {
1193
1217
  toRows,
1194
1218
  trialCountdownLine,
1195
1219
  upgradeUrl,
1220
+ validateMandateCaps,
1196
1221
  validateRequest,
1197
1222
  verify,
1198
1223
  verifyGrant,
package/dist/nudges.d.ts CHANGED
@@ -43,7 +43,11 @@ export declare function decisionNudge(status: "ALLOW" | "DENY", violatedRule: st
43
43
  * (nudge doctrine: never nag twice), a terse line every time after — the wall is
44
44
  * a direct answer to the agent's request, so it always says SOMETHING actionable.
45
45
  */
46
- export declare function hostedWallCta(feature: "verdict" | "anchor"): string;
46
+ export declare function hostedWallCta(feature: "verdict" | "anchor", billing?: {
47
+ reason?: string;
48
+ detail?: string;
49
+ billingUrl?: string;
50
+ }): string;
47
51
  /** The no-key CTA for a hosted tool: route the human to a free account. */
48
52
  export declare function noKeyCta(): string;
49
53
  /** One-time boot line after a week of real protection; null otherwise. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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
  }