@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 +6 -0
- package/dist/core.js +37 -4
- package/dist/firewall/evaluate.d.ts +11 -0
- package/dist/index.js +39 -6
- package/dist/lib.d.ts +1 -1
- package/dist/lib.js +40 -5
- package/package.json +5 -2
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) =>
|
|
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.
|
|
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) =>
|
|
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.
|
|
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.
|
|
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.
|
|
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) =>
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|