nymbal 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +994 -63
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { defineCommand as
|
|
2
|
+
import { defineCommand as defineCommand15, runMain } from "citty";
|
|
3
3
|
|
|
4
4
|
// src/utils/version.ts
|
|
5
5
|
import { readFileSync } from "fs";
|
|
@@ -56,9 +56,9 @@ function run(command, args, options = {}) {
|
|
|
56
56
|
shell: false,
|
|
57
57
|
...options
|
|
58
58
|
});
|
|
59
|
-
const done = new Promise((
|
|
59
|
+
const done = new Promise((resolve10, reject) => {
|
|
60
60
|
child2.on("error", reject);
|
|
61
|
-
child2.on("exit", (code) =>
|
|
61
|
+
child2.on("exit", (code) => resolve10(code ?? 0));
|
|
62
62
|
});
|
|
63
63
|
return { child: child2, done };
|
|
64
64
|
}
|
|
@@ -73,16 +73,16 @@ async function runOrFail(command, args, options = {}) {
|
|
|
73
73
|
// src/utils/port.ts
|
|
74
74
|
import { createServer } from "net";
|
|
75
75
|
async function isPortFree(port, host) {
|
|
76
|
-
return new Promise((
|
|
76
|
+
return new Promise((resolve10) => {
|
|
77
77
|
const server = createServer();
|
|
78
78
|
server.once("error", (err2) => {
|
|
79
79
|
if (err2.code !== "EADDRINUSE" && err2.code !== "EACCES") {
|
|
80
80
|
console.error(`[dev] unexpected error probing port ${port}:`, err2.message);
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
resolve10(false);
|
|
83
83
|
});
|
|
84
84
|
server.once("listening", () => {
|
|
85
|
-
server.close(() =>
|
|
85
|
+
server.close(() => resolve10(true));
|
|
86
86
|
});
|
|
87
87
|
if (host) {
|
|
88
88
|
server.listen(port, host);
|
|
@@ -243,8 +243,8 @@ async function runShutdown(steps, opts) {
|
|
|
243
243
|
pending = void 0;
|
|
244
244
|
})();
|
|
245
245
|
let timer;
|
|
246
|
-
const bound = new Promise((
|
|
247
|
-
timer = setTimeout(() =>
|
|
246
|
+
const bound = new Promise((resolve10) => {
|
|
247
|
+
timer = setTimeout(() => resolve10("bound"), opts.boundMs);
|
|
248
248
|
});
|
|
249
249
|
const outcome = await Promise.race([work.then(() => "done"), bound]);
|
|
250
250
|
clearTimeout(timer);
|
|
@@ -441,7 +441,8 @@ var devCommand = defineCommand2({
|
|
|
441
441
|
documentStore: platform.documentStore,
|
|
442
442
|
eventBus: platform.eventBus,
|
|
443
443
|
logger: platform.logger,
|
|
444
|
-
version
|
|
444
|
+
version,
|
|
445
|
+
search: app.adapters.search
|
|
445
446
|
});
|
|
446
447
|
}
|
|
447
448
|
}
|
|
@@ -456,7 +457,9 @@ var devCommand = defineCommand2({
|
|
|
456
457
|
await http.start();
|
|
457
458
|
const apiUrl = `http://localhost:${config.http.port}`;
|
|
458
459
|
const printSummary = (storefront) => {
|
|
459
|
-
console.log(
|
|
460
|
+
console.log(
|
|
461
|
+
renderDevSummary(summaryInfo(config, app, apiUrl, storefront, adminRoot !== null))
|
|
462
|
+
);
|
|
460
463
|
};
|
|
461
464
|
const stopApi = [
|
|
462
465
|
{ name: "http.stop", run: () => http.stop() },
|
|
@@ -546,10 +549,28 @@ import { defineCommand as defineCommand4 } from "citty";
|
|
|
546
549
|
import { createPlatform, resolvePrimitives, runSeed as runSeed2 } from "@nymbalhq/platform";
|
|
547
550
|
var seedCommand = defineCommand4({
|
|
548
551
|
meta: { name: "seed", description: "Re-seed demo content (products, categories)" },
|
|
549
|
-
|
|
552
|
+
args: {
|
|
553
|
+
localImages: {
|
|
554
|
+
type: "boolean",
|
|
555
|
+
// citty registers the camelCase key only; the alias maps the kebab flag onto it (see `dev --api-only`).
|
|
556
|
+
alias: "local-images",
|
|
557
|
+
description: "Write the product images through the store's media slot instead of linking a placeholder host",
|
|
558
|
+
default: false
|
|
559
|
+
},
|
|
560
|
+
mediaRoot: {
|
|
561
|
+
type: "string",
|
|
562
|
+
alias: "media-root",
|
|
563
|
+
description: "Where local-fs media is written (default: public/imported-media)"
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
async run({ args }) {
|
|
550
567
|
const { config, projectRoot } = await loadProject();
|
|
551
568
|
await migrateProject(config, projectRoot);
|
|
552
|
-
const
|
|
569
|
+
const mediaRoot = typeof args.mediaRoot === "string" && args.mediaRoot !== "" ? args.mediaRoot : void 0;
|
|
570
|
+
const platform = createPlatform(
|
|
571
|
+
config,
|
|
572
|
+
await resolvePrimitives(config, mediaRoot === void 0 ? {} : { mediaRoot })
|
|
573
|
+
);
|
|
553
574
|
try {
|
|
554
575
|
const result = await runSeed2({
|
|
555
576
|
config,
|
|
@@ -557,7 +578,8 @@ var seedCommand = defineCommand4({
|
|
|
557
578
|
documentStore: platform.documentStore,
|
|
558
579
|
eventBus: platform.eventBus,
|
|
559
580
|
logger: platform.logger,
|
|
560
|
-
version: getCliVersion()
|
|
581
|
+
version: getCliVersion(),
|
|
582
|
+
...args.localImages && { media: platform.media }
|
|
561
583
|
});
|
|
562
584
|
console.log(
|
|
563
585
|
result.alreadySeeded ? "\u2713 demo content already present, nothing to seed" : `\u2713 seeded ${result.products} products across ${result.categories} categories`
|
|
@@ -572,7 +594,7 @@ var seedCommand = defineCommand4({
|
|
|
572
594
|
import { defineCommand as defineCommand5 } from "citty";
|
|
573
595
|
import { intro, outro, text, select, spinner, isCancel, cancel } from "@clack/prompts";
|
|
574
596
|
import p from "picocolors";
|
|
575
|
-
import { createApp as createApp2 } from "@nymbalhq/platform";
|
|
597
|
+
import { createApp as createApp2, newCorrelationId, runAsActor, runWithCorrelation } from "@nymbalhq/platform";
|
|
576
598
|
import { runWooCommerceImport, writeReport } from "@nymbalhq/importers";
|
|
577
599
|
import { join as join3 } from "path";
|
|
578
600
|
var importCommand = defineCommand5({
|
|
@@ -700,35 +722,38 @@ var importCommand = defineCommand5({
|
|
|
700
722
|
const app = await createApp2(config, { version: getCliVersion() });
|
|
701
723
|
loadSpin.stop("Platform ready");
|
|
702
724
|
try {
|
|
703
|
-
const report = await
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
725
|
+
const report = await runWithCorrelation(
|
|
726
|
+
newCorrelationId("import:woocommerce"),
|
|
727
|
+
() => runAsActor({ kind: "import", id: "woocommerce" }, () => runWooCommerceImport(
|
|
728
|
+
{
|
|
729
|
+
credentials: { url: storeUrl, consumerKey, consumerSecret },
|
|
730
|
+
entities: entityList,
|
|
731
|
+
ai: aiEnabled,
|
|
732
|
+
gdpr: gdprEnabled,
|
|
733
|
+
fresh: args.fresh,
|
|
734
|
+
concurrency,
|
|
735
|
+
projectRoot,
|
|
736
|
+
currency: config.store.currency,
|
|
737
|
+
statePath
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
services: app.services,
|
|
741
|
+
repos: app.repos,
|
|
742
|
+
adapters: { ai: app.adapters.ai, reviews: app.adapters.reviews },
|
|
743
|
+
media: app.platform.media,
|
|
744
|
+
onProgress(event) {
|
|
745
|
+
if (event.kind === "phase") {
|
|
746
|
+
console.log(p.cyan(` \u2192 ${event.phase}`));
|
|
747
|
+
} else if (event.kind === "progress") {
|
|
748
|
+
const pct = Math.round(event.done / event.total * 100);
|
|
749
|
+
process.stdout.write(`\r ${event.entity}: ${event.done}/${event.total} (${pct}%) `);
|
|
750
|
+
if (event.done === event.total) process.stdout.write("\n");
|
|
751
|
+
} else if (event.kind === "error") {
|
|
752
|
+
console.log(p.red(` \u2717 ${event.phase} [${event.id}]: ${event.message}`));
|
|
753
|
+
}
|
|
729
754
|
}
|
|
730
755
|
}
|
|
731
|
-
|
|
756
|
+
))
|
|
732
757
|
);
|
|
733
758
|
const reportPath = await writeReport(report, projectRoot, config.store.name);
|
|
734
759
|
outro(
|
|
@@ -792,7 +817,9 @@ import { existsSync as existsSync5 } from "fs";
|
|
|
792
817
|
import { readFile } from "fs/promises";
|
|
793
818
|
import { createRequire as createRequire2 } from "module";
|
|
794
819
|
import { dirname as dirname5, join as join4, resolve as resolve5 } from "path";
|
|
820
|
+
import { pathToFileURL } from "url";
|
|
795
821
|
var RECIPE_HOLDERS = {
|
|
822
|
+
cloudflare: "@nymbalhq/security-cloudflare",
|
|
796
823
|
postgres: "@nymbalhq/platform",
|
|
797
824
|
redis: "@nymbalhq/doc-store-redis",
|
|
798
825
|
s3: "@nymbalhq/media-s3",
|
|
@@ -845,6 +872,34 @@ async function readRecipe(name, projectRoot) {
|
|
|
845
872
|
if (malformed) throw new Error(`${file} is not a ${name} recipe`);
|
|
846
873
|
return recipe;
|
|
847
874
|
}
|
|
875
|
+
function entryOf(dir, manifest) {
|
|
876
|
+
const exportsField = manifest["exports"];
|
|
877
|
+
const root = typeof exportsField === "object" ? exportsField["."] : exportsField;
|
|
878
|
+
const entry = typeof root === "string" ? root : typeof root === "object" && root !== null ? root["import"] ?? root["default"] : manifest["module"] ?? manifest["main"];
|
|
879
|
+
return typeof entry === "string" ? join4(dir, entry) : null;
|
|
880
|
+
}
|
|
881
|
+
async function settingsEnv(packages, projectRoot) {
|
|
882
|
+
const names = /* @__PURE__ */ new Set();
|
|
883
|
+
for (const pkg of packages.filter((p2) => p2.startsWith("@nymbalhq/"))) {
|
|
884
|
+
const dir = packageDir(pkg, projectRoot);
|
|
885
|
+
if (dir === null) continue;
|
|
886
|
+
const manifest = JSON.parse(await readFile(join4(dir, "package.json"), "utf8"));
|
|
887
|
+
const entry = entryOf(dir, manifest);
|
|
888
|
+
if (entry === null || !existsSync5(entry)) continue;
|
|
889
|
+
let mod;
|
|
890
|
+
try {
|
|
891
|
+
mod = await import(pathToFileURL(entry).href);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
if (error.code === "ERR_MODULE_NOT_FOUND") continue;
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
if (!Array.isArray(mod.settings)) continue;
|
|
897
|
+
for (const setting of mod.settings) {
|
|
898
|
+
if (typeof setting?.env === "string") names.add(setting.env);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return names;
|
|
902
|
+
}
|
|
848
903
|
|
|
849
904
|
// src/recipes/config-edit.ts
|
|
850
905
|
import { parseModule } from "magicast";
|
|
@@ -853,7 +908,8 @@ var SCAFFOLD_DEFAULTS = [
|
|
|
853
908
|
"in-memory",
|
|
854
909
|
"in-process",
|
|
855
910
|
"local-fs",
|
|
856
|
-
"native"
|
|
911
|
+
"native",
|
|
912
|
+
"middleware"
|
|
857
913
|
];
|
|
858
914
|
var CONFIG_MODULE = "@nymbalhq/config";
|
|
859
915
|
var child = (node, key) => node[key];
|
|
@@ -1384,7 +1440,12 @@ async function add(opts, cwd = process.cwd()) {
|
|
|
1384
1440
|
if (unset.length > 0 || recipe.next.length > 0) {
|
|
1385
1441
|
out();
|
|
1386
1442
|
out("Next:");
|
|
1387
|
-
|
|
1443
|
+
const fromSettings = unset.length > 0 ? await settingsEnv(recipe.install, root) : /* @__PURE__ */ new Set();
|
|
1444
|
+
for (const v of unset) {
|
|
1445
|
+
out(
|
|
1446
|
+
fromSettings.has(v.name) ? ` - set ${v.name} in .env or in the admin's Settings` : ` - set ${v.name} in .env`
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1388
1449
|
for (const line of recipe.next) out(` - ${line}`);
|
|
1389
1450
|
}
|
|
1390
1451
|
return 0;
|
|
@@ -1430,6 +1491,7 @@ import { errorText, HEALTH_CHECK_TIMEOUT_MS } from "@nymbalhq/types";
|
|
|
1430
1491
|
|
|
1431
1492
|
// src/commands/doctor-report.ts
|
|
1432
1493
|
import pc4 from "picocolors";
|
|
1494
|
+
import { scrub as scrubSettings } from "@nymbalhq/platform";
|
|
1433
1495
|
import { connectionSecrets, displayUrl as displayUrl2, SECRET_MIN_LENGTH } from "@nymbalhq/types";
|
|
1434
1496
|
var TEXT_STATUS = {
|
|
1435
1497
|
ok: "ok",
|
|
@@ -1454,7 +1516,8 @@ function addSecret(into, value) {
|
|
|
1454
1516
|
function collectFromConfig(value, key, into, seen) {
|
|
1455
1517
|
if (typeof value === "string") {
|
|
1456
1518
|
if (SECRET_KEY.test(key)) addSecret(into, value);
|
|
1457
|
-
if (value.includes("://"))
|
|
1519
|
+
if (value.includes("://"))
|
|
1520
|
+
for (const s of connectionSecrets(value, { user: false })) addSecret(into, s);
|
|
1458
1521
|
return;
|
|
1459
1522
|
}
|
|
1460
1523
|
if (value === null || typeof value !== "object" || seen.has(value)) return;
|
|
@@ -1467,12 +1530,13 @@ function secretsOf(config, env) {
|
|
|
1467
1530
|
for (const [name, value] of Object.entries(env)) {
|
|
1468
1531
|
if (value === void 0) continue;
|
|
1469
1532
|
if (SECRET_ENV.test(name)) addSecret(found, value);
|
|
1470
|
-
if (value.includes("://"))
|
|
1533
|
+
if (value.includes("://"))
|
|
1534
|
+
for (const s of connectionSecrets(value, { user: false })) addSecret(found, s);
|
|
1471
1535
|
}
|
|
1472
1536
|
return [...found].sort((a, b) => b.length - a.length);
|
|
1473
1537
|
}
|
|
1474
1538
|
function scrub(detail, secrets) {
|
|
1475
|
-
let text2 = detail.replace(URL_IN_TEXT, (match) => {
|
|
1539
|
+
let text2 = scrubSettings(detail).replace(URL_IN_TEXT, (match) => {
|
|
1476
1540
|
const trailing = TRAILING_PUNCTUATION.exec(match)?.[0] ?? "";
|
|
1477
1541
|
return displayUrl2(match.slice(0, match.length - trailing.length)) + trailing;
|
|
1478
1542
|
});
|
|
@@ -1489,7 +1553,11 @@ function formatDoctorReport(report, options) {
|
|
|
1489
1553
|
if (options.json) {
|
|
1490
1554
|
return JSON.stringify(
|
|
1491
1555
|
{
|
|
1492
|
-
checks: checks.map((c) => ({
|
|
1556
|
+
checks: checks.map((c) => ({
|
|
1557
|
+
name: c.name,
|
|
1558
|
+
status: JSON_STATUS[c.status],
|
|
1559
|
+
detail: c.detail
|
|
1560
|
+
}))
|
|
1493
1561
|
},
|
|
1494
1562
|
null,
|
|
1495
1563
|
2
|
|
@@ -1501,7 +1569,9 @@ function formatDoctorReport(report, options) {
|
|
|
1501
1569
|
return `${COLOUR[status](status.padEnd(4))} ${c.name.padEnd(width)} ${c.detail}`.trimEnd();
|
|
1502
1570
|
});
|
|
1503
1571
|
const count = (s) => checks.filter((c) => TEXT_STATUS[c.status] === s).length;
|
|
1504
|
-
return [...lines, "", `${count("down")} down, ${count("warn")} warn, ${count("ok")} ok`].join(
|
|
1572
|
+
return [...lines, "", `${count("down")} down, ${count("warn")} warn, ${count("ok")} ok`].join(
|
|
1573
|
+
"\n"
|
|
1574
|
+
);
|
|
1505
1575
|
}
|
|
1506
1576
|
|
|
1507
1577
|
// src/commands/doctor.ts
|
|
@@ -1558,19 +1628,849 @@ var doctorCommand = defineCommand8({
|
|
|
1558
1628
|
}
|
|
1559
1629
|
});
|
|
1560
1630
|
|
|
1561
|
-
// src/commands/
|
|
1631
|
+
// src/commands/secrets.ts
|
|
1562
1632
|
import { defineCommand as defineCommand9 } from "citty";
|
|
1563
|
-
import
|
|
1564
|
-
|
|
1633
|
+
import pc5 from "picocolors";
|
|
1634
|
+
import {
|
|
1635
|
+
createCommandStore as createCommandStore2,
|
|
1636
|
+
createSettingsRepository,
|
|
1637
|
+
decrypt,
|
|
1638
|
+
encrypt,
|
|
1639
|
+
isMissingSettingsTable,
|
|
1640
|
+
keyProblem,
|
|
1641
|
+
readSecretsKey,
|
|
1642
|
+
SECRETS_KEY_ENV,
|
|
1643
|
+
SECRETS_KEY_OLD_ENV,
|
|
1644
|
+
settingAad
|
|
1645
|
+
} from "@nymbalhq/platform";
|
|
1646
|
+
var RotationRefused = class extends Error {
|
|
1647
|
+
};
|
|
1648
|
+
function refuse(message) {
|
|
1649
|
+
process.stderr.write(`${pc5.red(`\u2717 ${message}`)}
|
|
1650
|
+
`);
|
|
1651
|
+
process.exitCode = 2;
|
|
1652
|
+
}
|
|
1653
|
+
async function rotateSecrets(env = process.env) {
|
|
1654
|
+
const oldKey = readSecretsKey(env, SECRETS_KEY_OLD_ENV);
|
|
1655
|
+
if (!oldKey.ok) throw new RotationRefused(keyProblem(oldKey.reason, SECRETS_KEY_OLD_ENV));
|
|
1656
|
+
const newKey = readSecretsKey(env, SECRETS_KEY_ENV);
|
|
1657
|
+
if (!newKey.ok) throw new RotationRefused(keyProblem(newKey.reason, SECRETS_KEY_ENV));
|
|
1658
|
+
const { config } = await loadProject();
|
|
1659
|
+
const store = createCommandStore2(config);
|
|
1660
|
+
try {
|
|
1661
|
+
const repo = createSettingsRepository(store);
|
|
1662
|
+
return await store.transaction(async (tx) => {
|
|
1663
|
+
const rows = await repo.list(tx);
|
|
1664
|
+
const rotated = [];
|
|
1665
|
+
for (const row2 of rows) {
|
|
1666
|
+
if (row2.ciphertext === null) continue;
|
|
1667
|
+
const field = `${row2.slot}.${row2.path}`;
|
|
1668
|
+
if (row2.keyId !== oldKey.id) {
|
|
1669
|
+
throw new RotationRefused(
|
|
1670
|
+
`${field} is under key ${row2.keyId ?? "(none)"}, not ${SECRETS_KEY_OLD_ENV} (key ${oldKey.id}); nothing was rotated`
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
let value;
|
|
1674
|
+
try {
|
|
1675
|
+
value = decrypt(oldKey.key, settingAad(row2.slot, row2.path), row2);
|
|
1676
|
+
} catch {
|
|
1677
|
+
throw new RotationRefused(
|
|
1678
|
+
`${field} does not decrypt under ${SECRETS_KEY_OLD_ENV}; nothing was rotated`
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
const sealed = encrypt(newKey.key, settingAad(row2.slot, row2.path), value);
|
|
1682
|
+
rotated.push({ ...row2, ...sealed, keyId: newKey.id, updatedAt: /* @__PURE__ */ new Date() });
|
|
1683
|
+
}
|
|
1684
|
+
for (const row2 of rotated) await repo.put(tx, row2);
|
|
1685
|
+
return rotated.length;
|
|
1686
|
+
});
|
|
1687
|
+
} finally {
|
|
1688
|
+
await store.close();
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
async function clearSecrets(filter) {
|
|
1692
|
+
const { config } = await loadProject();
|
|
1693
|
+
const store = createCommandStore2(config);
|
|
1694
|
+
try {
|
|
1695
|
+
const repo = createSettingsRepository(store);
|
|
1696
|
+
return await store.transaction((tx) => repo.clear(tx, filter));
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
if (isMissingSettingsTable(error)) return 0;
|
|
1699
|
+
throw error;
|
|
1700
|
+
} finally {
|
|
1701
|
+
await store.close();
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
var rotateCommand = defineCommand9({
|
|
1705
|
+
meta: {
|
|
1706
|
+
name: "rotate",
|
|
1707
|
+
description: `Re-encrypt every saved secret from ${SECRETS_KEY_OLD_ENV} to ${SECRETS_KEY_ENV}, all or none`
|
|
1708
|
+
},
|
|
1709
|
+
async run() {
|
|
1710
|
+
try {
|
|
1711
|
+
const count = await rotateSecrets();
|
|
1712
|
+
process.stdout.write(
|
|
1713
|
+
`${pc5.green(`\u2713 rotated ${count} secret${count === 1 ? "" : "s"} to ${SECRETS_KEY_ENV}`)}
|
|
1714
|
+
`
|
|
1715
|
+
);
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
refuse(error instanceof Error ? error.message : String(error));
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
var clearCommand = defineCommand9({
|
|
1722
|
+
meta: {
|
|
1723
|
+
name: "clear",
|
|
1724
|
+
description: "Delete saved settings (all, or one slot or path); needs no key"
|
|
1725
|
+
},
|
|
1726
|
+
args: {
|
|
1727
|
+
slot: { type: "string", description: "Only this slot" },
|
|
1728
|
+
path: { type: "string", description: "Only this path" }
|
|
1729
|
+
},
|
|
1730
|
+
async run({ args }) {
|
|
1731
|
+
const count = await clearSecrets({
|
|
1732
|
+
...args.slot !== void 0 && { slot: args.slot },
|
|
1733
|
+
...args.path !== void 0 && { path: args.path }
|
|
1734
|
+
});
|
|
1735
|
+
process.stdout.write(
|
|
1736
|
+
`${pc5.green(`\u2713 cleared ${count} setting${count === 1 ? "" : "s"}`)}; restart the store to apply
|
|
1737
|
+
`
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
var secretsCommand = defineCommand9({
|
|
1742
|
+
meta: { name: "secrets", description: "Manage the values saved in the admin's Settings" },
|
|
1743
|
+
subCommands: { rotate: rotateCommand, clear: clearCommand }
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
// src/commands/events.ts
|
|
1747
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1748
|
+
import { resolve as resolve6 } from "path";
|
|
1749
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
1750
|
+
import {
|
|
1751
|
+
createCommandStore as createCommandStore3,
|
|
1752
|
+
createEventLogRepository,
|
|
1753
|
+
sqliteFile as sqliteFile2
|
|
1754
|
+
} from "@nymbalhq/platform";
|
|
1755
|
+
import { errorText as errorText2, isValidCorrelationId } from "@nymbalhq/types";
|
|
1756
|
+
var POLL_MS = 500;
|
|
1757
|
+
var TAIL_PAGE = 1e3;
|
|
1758
|
+
var EventsError = class extends Error {
|
|
1759
|
+
};
|
|
1760
|
+
function formatEvent(row2, opts) {
|
|
1761
|
+
const { event } = row2;
|
|
1762
|
+
if (!opts.json) {
|
|
1763
|
+
const correlationId = isValidCorrelationId(event.correlationId) ? event.correlationId : JSON.stringify(event.correlationId);
|
|
1764
|
+
return `${row2.seq} ${event.timestamp} ${event.type} ${correlationId} ${event.id}`;
|
|
1765
|
+
}
|
|
1766
|
+
const { payload, ...envelope } = event;
|
|
1767
|
+
return JSON.stringify({
|
|
1768
|
+
...envelope,
|
|
1769
|
+
...opts.payload && { payload },
|
|
1770
|
+
seq: row2.seq,
|
|
1771
|
+
deliveredAt: row2.deliveredAt
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
function timeFlag(name, value) {
|
|
1775
|
+
if (value === void 0) return void 0;
|
|
1776
|
+
const at = new Date(value);
|
|
1777
|
+
if (value.trim() === "" || Number.isNaN(at.getTime())) {
|
|
1778
|
+
throw new EventsError(`--${name} must be an ISO 8601 time, got "${value}"`);
|
|
1779
|
+
}
|
|
1780
|
+
return at;
|
|
1781
|
+
}
|
|
1782
|
+
function countFlag(name, value, min) {
|
|
1783
|
+
if (value === void 0) return void 0;
|
|
1784
|
+
const n = Number(value);
|
|
1785
|
+
if (value.trim() === "" || !Number.isInteger(n) || n < min) {
|
|
1786
|
+
throw new EventsError(`--${name} must be a whole number of at least ${min}, got "${value}"`);
|
|
1787
|
+
}
|
|
1788
|
+
return n;
|
|
1789
|
+
}
|
|
1790
|
+
function typesFlag(value) {
|
|
1791
|
+
if (value === void 0) return {};
|
|
1792
|
+
if (value.trim() === "") throw new EventsError("--type must not be empty");
|
|
1793
|
+
return { types: [value] };
|
|
1794
|
+
}
|
|
1795
|
+
async function openLog() {
|
|
1796
|
+
const { config } = await loadProject();
|
|
1797
|
+
if (config.infrastructure.commandStore === "sqlite") {
|
|
1798
|
+
const path = sqliteFile2();
|
|
1799
|
+
if (!existsSync8(resolve6(path))) {
|
|
1800
|
+
throw new EventsError(`no database at ${path}: run \`nymbal migrate\``);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const store = createCommandStore3(config, { readOnly: true });
|
|
1804
|
+
return { store, eventLog: createEventLogRepository(store) };
|
|
1805
|
+
}
|
|
1806
|
+
async function guarded(body) {
|
|
1807
|
+
try {
|
|
1808
|
+
await body();
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
process.stderr.write(`nymbal events: ${errorText2(error)}
|
|
1811
|
+
`);
|
|
1812
|
+
process.exitCode = 2;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function write(lines) {
|
|
1816
|
+
if (lines.length === 0) return Promise.resolve();
|
|
1817
|
+
return new Promise((done) => process.stdout.write(`${lines.join("\n")}
|
|
1818
|
+
`, () => done()));
|
|
1819
|
+
}
|
|
1820
|
+
var outputArgs = {
|
|
1821
|
+
json: {
|
|
1822
|
+
type: "boolean",
|
|
1823
|
+
description: "One envelope per line, with seq and deliveredAt",
|
|
1824
|
+
default: false
|
|
1825
|
+
},
|
|
1826
|
+
payload: {
|
|
1827
|
+
type: "boolean",
|
|
1828
|
+
description: "Include each event\u2019s payload (it may hold customer data)",
|
|
1829
|
+
default: false
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
var queryCommand = defineCommand10({
|
|
1833
|
+
meta: { name: "query", description: "Print one correlation id\u2019s events, in seq order" },
|
|
1834
|
+
args: {
|
|
1835
|
+
correlation: { type: "string", description: "The correlation id (required)" },
|
|
1836
|
+
type: {
|
|
1837
|
+
type: "string",
|
|
1838
|
+
description: "Only types matching this glob; * matches anything (order.*)"
|
|
1839
|
+
},
|
|
1840
|
+
since: { type: "string", description: "Only events appended at or after this ISO time" },
|
|
1841
|
+
until: { type: "string", description: "Only events appended before this ISO time" },
|
|
1842
|
+
limit: { type: "string", description: "At most this many events (default 1000)" },
|
|
1843
|
+
...outputArgs
|
|
1844
|
+
},
|
|
1845
|
+
run: ({ args }) => guarded(async () => {
|
|
1846
|
+
const correlationId = args.correlation;
|
|
1847
|
+
if (correlationId === void 0 || correlationId === "") {
|
|
1848
|
+
throw new EventsError("--correlation <id> is required");
|
|
1849
|
+
}
|
|
1850
|
+
const since = timeFlag("since", args.since);
|
|
1851
|
+
const until = timeFlag("until", args.until);
|
|
1852
|
+
const filter = {
|
|
1853
|
+
...typesFlag(args.type),
|
|
1854
|
+
...since !== void 0 && { since },
|
|
1855
|
+
...until !== void 0 && { until },
|
|
1856
|
+
limit: countFlag("limit", args.limit, 1) ?? 1e3
|
|
1857
|
+
};
|
|
1858
|
+
const { store, eventLog } = await openLog();
|
|
1859
|
+
try {
|
|
1860
|
+
const rows = await store.read(
|
|
1861
|
+
(tx) => eventLog.queryByCorrelation(tx, correlationId, filter)
|
|
1862
|
+
);
|
|
1863
|
+
await write(rows.map((row2) => formatEvent(row2, args)));
|
|
1864
|
+
} finally {
|
|
1865
|
+
await store.close();
|
|
1866
|
+
}
|
|
1867
|
+
})
|
|
1868
|
+
});
|
|
1869
|
+
var tailCommand = defineCommand10({
|
|
1870
|
+
meta: {
|
|
1871
|
+
name: "tail",
|
|
1872
|
+
description: "Print events as they are appended, until interrupted. Exact for serially committed events; on Postgres an event whose lower seq commits after a higher one has been printed is skipped."
|
|
1873
|
+
},
|
|
1874
|
+
args: {
|
|
1875
|
+
type: {
|
|
1876
|
+
type: "string",
|
|
1877
|
+
description: "Only types matching this glob; * matches anything (order.*)"
|
|
1878
|
+
},
|
|
1879
|
+
from: {
|
|
1880
|
+
type: "string",
|
|
1881
|
+
description: "Print events above this seq (default: the current maximum)"
|
|
1882
|
+
},
|
|
1883
|
+
...outputArgs
|
|
1884
|
+
},
|
|
1885
|
+
run: ({ args }) => guarded(async () => {
|
|
1886
|
+
const types = typesFlag(args.type);
|
|
1887
|
+
const from = countFlag("from", args.from, 0);
|
|
1888
|
+
const { store, eventLog } = await openLog();
|
|
1889
|
+
let stopped = false;
|
|
1890
|
+
let wake;
|
|
1891
|
+
const stop = () => {
|
|
1892
|
+
stopped = true;
|
|
1893
|
+
wake?.();
|
|
1894
|
+
};
|
|
1895
|
+
process.once("SIGINT", stop);
|
|
1896
|
+
process.once("SIGTERM", stop);
|
|
1897
|
+
try {
|
|
1898
|
+
let cursor = from ?? await store.read((tx) => eventLog.maxSeq(tx));
|
|
1899
|
+
process.stderr.write(`tailing from seq ${cursor}
|
|
1900
|
+
`);
|
|
1901
|
+
while (!stopped) {
|
|
1902
|
+
const after = cursor;
|
|
1903
|
+
const rows = await store.read(
|
|
1904
|
+
(tx) => eventLog.listAfter(tx, after, { ...types, limit: TAIL_PAGE })
|
|
1905
|
+
);
|
|
1906
|
+
await write(rows.map((row2) => formatEvent(row2, args)));
|
|
1907
|
+
if (rows.length > 0) cursor = rows[rows.length - 1].seq;
|
|
1908
|
+
if (rows.length === TAIL_PAGE) continue;
|
|
1909
|
+
await new Promise((done) => {
|
|
1910
|
+
const timer = setTimeout(done, POLL_MS);
|
|
1911
|
+
wake = () => {
|
|
1912
|
+
clearTimeout(timer);
|
|
1913
|
+
done();
|
|
1914
|
+
};
|
|
1915
|
+
});
|
|
1916
|
+
wake = void 0;
|
|
1917
|
+
}
|
|
1918
|
+
} finally {
|
|
1919
|
+
process.off("SIGINT", stop);
|
|
1920
|
+
process.off("SIGTERM", stop);
|
|
1921
|
+
await store.close();
|
|
1922
|
+
}
|
|
1923
|
+
})
|
|
1924
|
+
});
|
|
1925
|
+
var eventsCommand = defineCommand10({
|
|
1926
|
+
meta: { name: "events", description: "Read the event log without changing it" },
|
|
1927
|
+
subCommands: { query: queryCommand, tail: tailCommand }
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
// src/commands/audit.ts
|
|
1931
|
+
import { createWriteStream, existsSync as existsSync9 } from "fs";
|
|
1932
|
+
import { resolve as resolve7 } from "path";
|
|
1933
|
+
import { once } from "events";
|
|
1934
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
1935
|
+
import {
|
|
1936
|
+
auditEntriesInOrder,
|
|
1937
|
+
createCommandStore as createCommandStore4,
|
|
1938
|
+
sqliteFile as sqliteFile3
|
|
1939
|
+
} from "@nymbalhq/platform";
|
|
1940
|
+
import { errorText as errorText3 } from "@nymbalhq/types";
|
|
1941
|
+
var AuditError = class extends Error {
|
|
1942
|
+
};
|
|
1943
|
+
function timeFlag2(name, value) {
|
|
1944
|
+
if (value === void 0) return void 0;
|
|
1945
|
+
const at = new Date(value);
|
|
1946
|
+
if (value.trim() === "" || Number.isNaN(at.getTime())) {
|
|
1947
|
+
throw new AuditError(`--${name} must be an ISO 8601 time, got "${value}"`);
|
|
1948
|
+
}
|
|
1949
|
+
return at;
|
|
1950
|
+
}
|
|
1951
|
+
async function write2(out2, text2) {
|
|
1952
|
+
if (!out2.write(text2)) await once(out2, "drain");
|
|
1953
|
+
}
|
|
1954
|
+
var exportCommand = defineCommand11({
|
|
1955
|
+
meta: {
|
|
1956
|
+
name: "export",
|
|
1957
|
+
description: "Print audit entries as NDJSON, oldest first: ids only, no event payloads or customer data"
|
|
1958
|
+
},
|
|
1959
|
+
args: {
|
|
1960
|
+
since: { type: "string", description: "Only entries at or after this ISO time" },
|
|
1961
|
+
until: { type: "string", description: "Only entries before this ISO time" },
|
|
1962
|
+
correlation: { type: "string", description: "Only the entries of this correlation id" },
|
|
1963
|
+
out: { type: "string", description: "Write the lines to this file instead of stdout" }
|
|
1964
|
+
},
|
|
1965
|
+
async run({ args }) {
|
|
1966
|
+
try {
|
|
1967
|
+
const since = timeFlag2("since", args.since);
|
|
1968
|
+
const until = timeFlag2("until", args.until);
|
|
1969
|
+
const filter = {
|
|
1970
|
+
...since !== void 0 && { since },
|
|
1971
|
+
...until !== void 0 && { until },
|
|
1972
|
+
...args.correlation !== void 0 && { correlation: args.correlation }
|
|
1973
|
+
};
|
|
1974
|
+
const { config } = await loadProject();
|
|
1975
|
+
if (config.infrastructure.commandStore === "sqlite") {
|
|
1976
|
+
const path = sqliteFile3();
|
|
1977
|
+
if (!existsSync9(resolve7(path))) {
|
|
1978
|
+
throw new AuditError(`no database at ${path}: run \`nymbal migrate\``);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
const store = createCommandStore4(config, { readOnly: true });
|
|
1982
|
+
const out2 = args.out === void 0 ? process.stdout : createWriteStream(args.out);
|
|
1983
|
+
try {
|
|
1984
|
+
for await (const entry of auditEntriesInOrder(store, filter)) {
|
|
1985
|
+
await write2(out2, `${JSON.stringify(entry)}
|
|
1986
|
+
`);
|
|
1987
|
+
}
|
|
1988
|
+
} finally {
|
|
1989
|
+
await store.close();
|
|
1990
|
+
if (out2 !== process.stdout) {
|
|
1991
|
+
out2.end();
|
|
1992
|
+
await once(out2, "finish");
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
process.stderr.write(`nymbal audit: ${errorText3(error)}
|
|
1997
|
+
`);
|
|
1998
|
+
process.exitCode = 2;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
var auditCommand = defineCommand11({
|
|
2003
|
+
meta: { name: "audit", description: "Read the audit trail" },
|
|
2004
|
+
subCommands: { export: exportCommand }
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
// src/commands/replay.ts
|
|
2008
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2009
|
+
import { resolve as resolve8 } from "path";
|
|
2010
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
2011
|
+
import {
|
|
2012
|
+
createLogger as createLogger3,
|
|
2013
|
+
diffReadModel,
|
|
2014
|
+
openReadModels,
|
|
2015
|
+
READ_MODEL_NAMES,
|
|
2016
|
+
rebuild,
|
|
2017
|
+
sqliteFile as sqliteFile4
|
|
2018
|
+
} from "@nymbalhq/platform";
|
|
2019
|
+
import { errorText as errorText4 } from "@nymbalhq/types";
|
|
2020
|
+
var ReplayError = class extends Error {
|
|
2021
|
+
};
|
|
2022
|
+
function modelsOf(to) {
|
|
2023
|
+
const choices = `${READ_MODEL_NAMES.join(", ")} or all`;
|
|
2024
|
+
if (to === void 0 || to === "") throw new ReplayError(`--to <model> is required: ${choices}`);
|
|
2025
|
+
if (to === "all") return READ_MODEL_NAMES;
|
|
2026
|
+
const name = READ_MODEL_NAMES.find((n) => n === to);
|
|
2027
|
+
if (name === void 0) throw new ReplayError(`--to must be ${choices}, got "${to}"`);
|
|
2028
|
+
return [name];
|
|
2029
|
+
}
|
|
2030
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
2031
|
+
function formatRebuildReport(report) {
|
|
2032
|
+
const lines = [];
|
|
2033
|
+
for (const entry of report) {
|
|
2034
|
+
const caught = plural(entry.caughtUpEvents, "event");
|
|
2035
|
+
lines.push(
|
|
2036
|
+
entry.failures.length === 0 ? `${entry.model}: swapped in ${entry.ms} ms, ${caught} caught up` : `${entry.model}: swapped in ${entry.ms} ms, but ${plural(entry.failures.length, "catch-up event")} failed (the model is swapped without them: fix the cause and replay it again)`
|
|
2037
|
+
);
|
|
2038
|
+
for (const c of entry.collections) lines.push(` ${c.name} ${c.documents}`);
|
|
2039
|
+
for (const f of entry.failures) {
|
|
2040
|
+
lines.push(` failed: seq ${f.seq} ${f.type} (${f.handler}): ${f.error}`);
|
|
2041
|
+
}
|
|
2042
|
+
if (entry.note !== void 0) lines.push(` note: ${entry.note}`);
|
|
2043
|
+
}
|
|
2044
|
+
return lines.join("\n");
|
|
2045
|
+
}
|
|
2046
|
+
function formatDiff(diffs) {
|
|
2047
|
+
const lines = [];
|
|
2048
|
+
for (const diff of diffs) {
|
|
2049
|
+
lines.push(`${diff.model} (dry run, nothing written):`);
|
|
2050
|
+
if (diff.skipped !== void 0) lines.push(` skipped: ${diff.skipped}`);
|
|
2051
|
+
for (const c of diff.collections) {
|
|
2052
|
+
lines.push(` ${c.name}: ${c.added} added, ${c.changed} changed, ${c.removed} removed`);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return lines.join("\n");
|
|
2056
|
+
}
|
|
2057
|
+
var differs = (diffs) => diffs.some((d) => d.collections.some((c) => c.added + c.changed + c.removed > 0));
|
|
2058
|
+
function write3(stream, text2) {
|
|
2059
|
+
return new Promise((done) => stream.write(`${text2}
|
|
2060
|
+
`, () => done()));
|
|
2061
|
+
}
|
|
2062
|
+
var replayCommand = defineCommand12({
|
|
2063
|
+
meta: {
|
|
2064
|
+
name: "replay",
|
|
2065
|
+
description: "Rebuild a derived read model from the command store and swap it in: catalogue, orders, customers, search or all"
|
|
2066
|
+
},
|
|
2067
|
+
args: {
|
|
2068
|
+
to: {
|
|
2069
|
+
type: "string",
|
|
2070
|
+
description: "The read model: catalogue, orders, customers, search or all"
|
|
2071
|
+
},
|
|
2072
|
+
"dry-run": {
|
|
2073
|
+
type: "boolean",
|
|
2074
|
+
description: "Rebuild into memory and count what differs from the live read model; exits 1 if anything does",
|
|
2075
|
+
default: false
|
|
2076
|
+
}
|
|
2077
|
+
},
|
|
2078
|
+
async run({ args }) {
|
|
2079
|
+
try {
|
|
2080
|
+
const names = modelsOf(args.to);
|
|
2081
|
+
const { config, adapters } = await loadProject();
|
|
2082
|
+
const shared = names.filter((n) => n !== "search");
|
|
2083
|
+
if (config.infrastructure.documentStore === "in-memory" && shared.length > 0) {
|
|
2084
|
+
throw new ReplayError(
|
|
2085
|
+
`the ${shared.join(", ")} read model${shared.length === 1 ? "" : "s"} live in the server's own process (documentStore: in-memory), out of reach of this one; a restart of the server rebuilds them. With documentStore redis or postgres-jsonb the read models are shared, and replay rebuilds them in place. --to search works here: the native index lives in the command store.`
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
if (config.infrastructure.commandStore === "sqlite" && !existsSync10(resolve8(sqliteFile4()))) {
|
|
2089
|
+
throw new ReplayError(`no database at ${sqliteFile4()}: run \`nymbal migrate\``);
|
|
2090
|
+
}
|
|
2091
|
+
const { deps, close } = await openReadModels(config, {
|
|
2092
|
+
// The report says what happened; log lines would repeat it.
|
|
2093
|
+
logger: createLogger3({ level: "silent", pretty: false }),
|
|
2094
|
+
...adapters !== void 0 && { adapters }
|
|
2095
|
+
});
|
|
2096
|
+
try {
|
|
2097
|
+
if (args["dry-run"]) {
|
|
2098
|
+
const diffs = [];
|
|
2099
|
+
for (const name of names) diffs.push(await diffReadModel(name, deps));
|
|
2100
|
+
await write3(process.stdout, formatDiff(diffs));
|
|
2101
|
+
process.exitCode = differs(diffs) ? 1 : 0;
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
const report = await rebuild(names, deps);
|
|
2105
|
+
await write3(process.stdout, formatRebuildReport(report));
|
|
2106
|
+
process.exitCode = report.some((e) => e.failures.length > 0) ? 2 : 0;
|
|
2107
|
+
} finally {
|
|
2108
|
+
await close();
|
|
2109
|
+
}
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
await write3(process.stderr, `nymbal replay: ${errorText4(error)}`);
|
|
2112
|
+
process.exitCode = 2;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
// src/commands/security.ts
|
|
2118
|
+
import { defineCommand as defineCommand13 } from "citty";
|
|
2119
|
+
import { createLogger as createLogger4, importProviderPackage } from "@nymbalhq/platform";
|
|
2120
|
+
import {
|
|
2121
|
+
errorText as errorText5
|
|
2122
|
+
} from "@nymbalhq/types";
|
|
2123
|
+
var failure = (...stderr) => ({ code: 2, stdout: [], stderr });
|
|
2124
|
+
var isEmpty = (plan) => plan.phases.every((p2) => p2.add.length + p2.change.length + p2.remove.length === 0);
|
|
2125
|
+
function formatPlan(plan) {
|
|
2126
|
+
const lines = [];
|
|
2127
|
+
for (const { phase, add: add2, change, remove } of plan.phases) {
|
|
2128
|
+
if (add2.length + change.length + remove.length === 0) continue;
|
|
2129
|
+
lines.push(
|
|
2130
|
+
`${phase}: ${add2.length} to add, ${change.length} to change, ${remove.length} to remove`
|
|
2131
|
+
);
|
|
2132
|
+
for (const rule of add2) lines.push(` + ${rule.ref} ${rule.action} ${rule.expression}`);
|
|
2133
|
+
for (const { ref, to } of change) lines.push(` ~ ${ref} ${to.action} ${to.expression}`);
|
|
2134
|
+
for (const rule of remove) lines.push(` - ${rule.ref}`);
|
|
2135
|
+
}
|
|
2136
|
+
return lines;
|
|
2137
|
+
}
|
|
2138
|
+
function invalid(result) {
|
|
2139
|
+
return failure(
|
|
2140
|
+
"The security config cannot go onto the zone:",
|
|
2141
|
+
...result.errors.map((e) => ` ${e.code} ${e.message}`)
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
function edgeNotes(security, mod) {
|
|
2145
|
+
const notes = Object.keys(security.rateLimit).filter((key) => key.startsWith("/") && mod.hasSingleStar(key)).map(
|
|
2146
|
+
(key) => `note: '${key}' matches one path segment per * in process, but any number at the edge (Cloudflare's * crosses /)`
|
|
2147
|
+
);
|
|
2148
|
+
if (security.botProtection.mode !== "off") {
|
|
2149
|
+
notes.push(
|
|
2150
|
+
"note: botProtection is left as the zone has it: Bot Fight Mode is a zone setting, not a ruleset rule"
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
return notes;
|
|
2154
|
+
}
|
|
2155
|
+
function finish(outcome, secrets) {
|
|
2156
|
+
const masked = (lines) => lines.map((line) => secrets.reduce((text2, secret) => text2.split(secret).join("***"), line)).map((line) => `${line}
|
|
2157
|
+
`).join("");
|
|
2158
|
+
process.stderr.write(
|
|
2159
|
+
masked(outcome.stderr),
|
|
2160
|
+
() => process.stdout.write(masked(outcome.stdout), () => process.exit(outcome.code))
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
async function runEdge(command, body) {
|
|
2164
|
+
const secrets = [process.env["CLOUDFLARE_API_TOKEN"]];
|
|
2165
|
+
let outcome;
|
|
2166
|
+
try {
|
|
2167
|
+
const { config } = await loadProject();
|
|
2168
|
+
const security = config.security;
|
|
2169
|
+
secrets.push(security.cloudflare?.apiToken);
|
|
2170
|
+
if (security.adapter !== "cloudflare") {
|
|
2171
|
+
outcome = failure(
|
|
2172
|
+
`security.adapter is '${security.adapter}', which has no edge rules for nymbal ${command}: set security.adapter to 'cloudflare' (nymbal add cloudflare) to manage them on a Cloudflare zone`
|
|
2173
|
+
);
|
|
2174
|
+
} else {
|
|
2175
|
+
const mod = await importProviderPackage(
|
|
2176
|
+
"security",
|
|
2177
|
+
"cloudflare",
|
|
2178
|
+
process.env
|
|
2179
|
+
);
|
|
2180
|
+
const adapter = await mod.createAdapter({
|
|
2181
|
+
slot: "security",
|
|
2182
|
+
config: security.cloudflare ?? {},
|
|
2183
|
+
logger: createLogger4({ level: "silent", pretty: false }),
|
|
2184
|
+
env: process.env
|
|
2185
|
+
});
|
|
2186
|
+
outcome = await body(adapter, security, mod);
|
|
2187
|
+
}
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
outcome = failure(`nymbal ${command} failed: ${errorText5(error)}`);
|
|
2190
|
+
}
|
|
2191
|
+
finish(
|
|
2192
|
+
outcome,
|
|
2193
|
+
secrets.filter((s) => typeof s === "string" && s !== "")
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
var json = {
|
|
2197
|
+
type: "boolean",
|
|
2198
|
+
description: "Print the result as one JSON document",
|
|
2199
|
+
default: false
|
|
2200
|
+
};
|
|
2201
|
+
var securityPlanCommand = defineCommand13({
|
|
2202
|
+
meta: {
|
|
2203
|
+
name: "security:plan",
|
|
2204
|
+
description: "Show what security:apply would change on the Cloudflare zone"
|
|
2205
|
+
},
|
|
2206
|
+
args: { json },
|
|
2207
|
+
run: ({ args }) => runEdge("security:plan", async (adapter, security, mod) => {
|
|
2208
|
+
const check = await adapter.validate(security);
|
|
2209
|
+
if (!check.valid) return invalid(check);
|
|
2210
|
+
const plan = await adapter.plan(security);
|
|
2211
|
+
const notes = edgeNotes(security, mod);
|
|
2212
|
+
if (args.json) return { code: 0, stdout: [JSON.stringify(plan, null, 2)], stderr: notes };
|
|
2213
|
+
const lines = isEmpty(plan) ? ["No changes: the zone holds every Nymbal rule."] : formatPlan(plan);
|
|
2214
|
+
return { code: 0, stdout: [...lines, ...notes], stderr: [] };
|
|
2215
|
+
})
|
|
2216
|
+
});
|
|
2217
|
+
var securityApplyCommand = defineCommand13({
|
|
2218
|
+
meta: {
|
|
2219
|
+
name: "security:apply",
|
|
2220
|
+
description: "Write the store's security rules to the Cloudflare zone, leaving the zone's own rules"
|
|
2221
|
+
},
|
|
2222
|
+
run: () => runEdge("security:apply", async (adapter, security) => {
|
|
2223
|
+
const check = await adapter.validate(security);
|
|
2224
|
+
if (!check.valid) return invalid(check);
|
|
2225
|
+
const done = await adapter.apply(security);
|
|
2226
|
+
const lines = isEmpty(done) ? ["Nothing to write: the zone already holds every Nymbal rule."] : ["Applied:", ...formatPlan(done)];
|
|
2227
|
+
return { code: 0, stdout: lines, stderr: [] };
|
|
2228
|
+
})
|
|
2229
|
+
});
|
|
2230
|
+
var securityDriftCommand = defineCommand13({
|
|
2231
|
+
meta: {
|
|
2232
|
+
name: "security:drift",
|
|
2233
|
+
description: "Report where the zone's Nymbal rules differ from the config (exit 1 on drift)"
|
|
2234
|
+
},
|
|
2235
|
+
args: { json },
|
|
2236
|
+
run: ({ args }) => runEdge("security:drift", async (adapter, security) => {
|
|
2237
|
+
const report = await adapter.drift(security);
|
|
2238
|
+
const code = report.drift ? 1 : 0;
|
|
2239
|
+
if (args.json) return { code, stdout: [JSON.stringify(report, null, 2)], stderr: [] };
|
|
2240
|
+
if (!report.drift)
|
|
2241
|
+
return { code, stdout: ["In sync: the zone holds every Nymbal rule."], stderr: [] };
|
|
2242
|
+
const lines = report.differences.map(
|
|
2243
|
+
(d) => ` ${d.path} configured ${JSON.stringify(d.configured) ?? "nothing"}, live ${JSON.stringify(d.actual) ?? "nothing"}`
|
|
2244
|
+
);
|
|
2245
|
+
return {
|
|
2246
|
+
code,
|
|
2247
|
+
stdout: [
|
|
2248
|
+
`Drift: ${report.differences.length} difference(s); security:apply restores them`,
|
|
2249
|
+
...lines
|
|
2250
|
+
],
|
|
2251
|
+
stderr: []
|
|
2252
|
+
};
|
|
2253
|
+
})
|
|
2254
|
+
});
|
|
2255
|
+
var securityInspectCommand = defineCommand13({
|
|
2256
|
+
meta: {
|
|
2257
|
+
name: "security:inspect",
|
|
2258
|
+
description: "List the Nymbal rules live on the Cloudflare zone"
|
|
2259
|
+
},
|
|
2260
|
+
run: () => runEdge("security:inspect", async (adapter) => {
|
|
2261
|
+
const { rulesApplied } = await adapter.inspect();
|
|
2262
|
+
const lines = rulesApplied.length === 0 ? ["No Nymbal rules are live on the zone."] : rulesApplied;
|
|
2263
|
+
return { code: 0, stdout: lines, stderr: [] };
|
|
2264
|
+
})
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
// src/commands/test.ts
|
|
2268
|
+
import { defineCommand as defineCommand14 } from "citty";
|
|
2269
|
+
import { loadConfig as loadConfig4 } from "@nymbalhq/config";
|
|
2270
|
+
|
|
2271
|
+
// src/commands/test-contracts.ts
|
|
2272
|
+
import { mkdirSync, realpathSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2273
|
+
import { createRequire as createRequire3 } from "module";
|
|
2274
|
+
import { dirname as dirname7, join as join6, resolve as resolve9 } from "path";
|
|
2275
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
2276
|
+
import { isDeepStrictEqual } from "util";
|
|
2277
|
+
import { findConfigFile as findConfigFile3, loadConfig as loadConfig3 } from "@nymbalhq/config";
|
|
2278
|
+
var CertifyError = class extends Error {
|
|
2279
|
+
};
|
|
2280
|
+
var firstLine = (text2) => text2.split("\n")[0].trim();
|
|
2281
|
+
var messageOf = (err2) => firstLine(err2 instanceof Error ? err2.message : String(err2));
|
|
2282
|
+
function resolveFrom(project, specifier, name, install) {
|
|
2283
|
+
try {
|
|
2284
|
+
return createRequire3(join6(project, "package.json")).resolve(specifier);
|
|
2285
|
+
} catch (err2) {
|
|
2286
|
+
if (err2.code === "MODULE_NOT_FOUND") {
|
|
2287
|
+
throw new CertifyError(`${name} is not installed in ${project}: ${install}`);
|
|
2288
|
+
}
|
|
2289
|
+
throw new CertifyError(`${name} could not be resolved from ${project}: ${messageOf(err2)}`);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
async function slotOptions(project, pkg, slot) {
|
|
2293
|
+
if (findConfigFile3(project) === null) return void 0;
|
|
2294
|
+
let commerce;
|
|
2295
|
+
try {
|
|
2296
|
+
commerce = (await loadConfig3(project)).config.commerce;
|
|
2297
|
+
} catch (err2) {
|
|
2298
|
+
throw new CertifyError(`the project's config could not be loaded: ${messageOf(err2)}`);
|
|
2299
|
+
}
|
|
2300
|
+
const naming = Object.entries(commerce).filter(([, block2]) => {
|
|
2301
|
+
const b = block2;
|
|
2302
|
+
return b.provider === "custom" && b.package === pkg;
|
|
2303
|
+
});
|
|
2304
|
+
const picked = slot === void 0 ? naming : naming.filter(([name]) => name === slot);
|
|
2305
|
+
if (slot !== void 0 && picked.length === 0) {
|
|
2306
|
+
throw new CertifyError(`no commerce slot '${slot}' with provider 'custom' names ${pkg}`);
|
|
2307
|
+
}
|
|
2308
|
+
if (picked.length > 1) {
|
|
2309
|
+
throw new CertifyError(
|
|
2310
|
+
`${pkg} is named by the ${picked.map(([name]) => name).join(", ")} slots: pick one with --slot`
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
const block = picked[0]?.[1];
|
|
2314
|
+
return block === void 0 ? void 0 : block.config ?? {};
|
|
2315
|
+
}
|
|
2316
|
+
function withoutUndefined(value, path = /* @__PURE__ */ new Set()) {
|
|
2317
|
+
if (value === null || typeof value !== "object" || path.has(value)) return value;
|
|
2318
|
+
const proto = Object.getPrototypeOf(value);
|
|
2319
|
+
if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return value;
|
|
2320
|
+
path.add(value);
|
|
2321
|
+
const out2 = Array.isArray(value) ? value.map((item) => withoutUndefined(item, path)) : Object.fromEntries(
|
|
2322
|
+
Object.entries(value).filter(([, v]) => v !== void 0).map(([k, v]) => [k, withoutUndefined(v, path)])
|
|
2323
|
+
);
|
|
2324
|
+
path.delete(value);
|
|
2325
|
+
return out2;
|
|
2326
|
+
}
|
|
2327
|
+
function survivesJson(value) {
|
|
2328
|
+
try {
|
|
2329
|
+
return isDeepStrictEqual(JSON.parse(JSON.stringify(value)), value);
|
|
2330
|
+
} catch {
|
|
2331
|
+
return false;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
async function closeProbe(adapter) {
|
|
2335
|
+
const close = adapter?.close;
|
|
2336
|
+
if (typeof close === "function") await close.call(adapter);
|
|
2337
|
+
}
|
|
2338
|
+
function countCases(files) {
|
|
2339
|
+
let passed = 0;
|
|
2340
|
+
let failed = 0;
|
|
2341
|
+
const visit = (task) => {
|
|
2342
|
+
if (task.type === "test") {
|
|
2343
|
+
if (task.result?.state === "pass") passed += 1;
|
|
2344
|
+
else if (task.mode !== "skip" && task.mode !== "todo") failed += 1;
|
|
2345
|
+
}
|
|
2346
|
+
for (const child2 of task.tasks ?? []) visit(child2);
|
|
2347
|
+
};
|
|
2348
|
+
for (const file of files) visit(file);
|
|
2349
|
+
return { passed, failed };
|
|
2350
|
+
}
|
|
2351
|
+
async function certifyPackage(input) {
|
|
2352
|
+
const { pkg, slot } = input;
|
|
2353
|
+
const config = findConfigFile3(input.cwd);
|
|
2354
|
+
const project = realpathSync(config === null ? resolve9(input.cwd) : dirname7(config));
|
|
2355
|
+
const vitestNode = resolveFrom(project, "vitest/node", "vitest", "npm install --save-dev vitest");
|
|
2356
|
+
const kindsEntry = resolveFrom(
|
|
2357
|
+
project,
|
|
2358
|
+
"@nymbalhq/contract-tests/kinds",
|
|
2359
|
+
"@nymbalhq/contract-tests",
|
|
2360
|
+
"npm install --save-dev @nymbalhq/contract-tests"
|
|
2361
|
+
);
|
|
2362
|
+
if (!isInstalled(pkg, project)) {
|
|
2363
|
+
throw new CertifyError(`${pkg} is not installed in ${project}: npm install ${pkg}`);
|
|
2364
|
+
}
|
|
2365
|
+
const contracts = join6(project, ".nymbal", "contracts");
|
|
2366
|
+
const slug = pkg.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "");
|
|
2367
|
+
mkdirSync(contracts, { recursive: true });
|
|
2368
|
+
const shim = join6(contracts, `${slug}.adapter.mjs`);
|
|
2369
|
+
writeFileSync2(shim, `export * from ${JSON.stringify(pkg)}
|
|
2370
|
+
`);
|
|
2371
|
+
let mod;
|
|
2372
|
+
try {
|
|
2373
|
+
mod = await import(pathToFileURL2(shim).href);
|
|
2374
|
+
} catch (err2) {
|
|
2375
|
+
throw new CertifyError(`${pkg} could not be loaded: ${messageOf(err2)}`);
|
|
2376
|
+
}
|
|
2377
|
+
if (typeof mod.createAdapter !== "function") {
|
|
2378
|
+
throw new CertifyError(`${pkg} exports no createAdapter(options) function to certify`);
|
|
2379
|
+
}
|
|
2380
|
+
const createAdapter = mod.createAdapter;
|
|
2381
|
+
const extra = mod.contractOptions ?? {};
|
|
2382
|
+
const slotConfig = await slotOptions(project, pkg, slot);
|
|
2383
|
+
const fromSlot = slotConfig === void 0 ? void 0 : withoutUndefined(slotConfig);
|
|
2384
|
+
const options = fromSlot ?? extra.options ?? {};
|
|
2385
|
+
if (fromSlot !== void 0 && !survivesJson(fromSlot)) {
|
|
2386
|
+
throw new CertifyError(
|
|
2387
|
+
`the config of the slot naming ${pkg} holds a value JSON cannot carry (a function, Date, BigInt or cycle): certify it with plain data`
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
let kind;
|
|
2391
|
+
try {
|
|
2392
|
+
const probe = await createAdapter(options);
|
|
2393
|
+
const own = probe?.kind;
|
|
2394
|
+
kind = typeof own === "string" ? own : extra.kind;
|
|
2395
|
+
await closeProbe(probe);
|
|
2396
|
+
} catch (err2) {
|
|
2397
|
+
throw new CertifyError(`createAdapter from ${pkg} threw: ${messageOf(err2)}`);
|
|
2398
|
+
}
|
|
2399
|
+
if (kind === void 0) {
|
|
2400
|
+
throw new CertifyError(
|
|
2401
|
+
`${pkg}'s adapter has no kind: a primitive names its own in contractOptions.kind`
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2404
|
+
const { CONTRACT_KINDS } = await import(pathToFileURL2(kindsEntry).href);
|
|
2405
|
+
if (!CONTRACT_KINDS.includes(kind)) {
|
|
2406
|
+
throw new CertifyError(
|
|
2407
|
+
`no contract suite for kind '${kind}' (${pkg}); the kinds with one are ${CONTRACT_KINDS.join(", ")}`
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
const file = `${slug}.contract.test.mjs`;
|
|
2411
|
+
writeFileSync2(
|
|
2412
|
+
join6(contracts, file),
|
|
2413
|
+
[
|
|
2414
|
+
`// Written by \`nymbal test:contracts --package ${pkg}\`, and again on every run.`,
|
|
2415
|
+
`import * as adapter from ${JSON.stringify(pkg)}`,
|
|
2416
|
+
`import { contractSuites } from '@nymbalhq/contract-tests'`,
|
|
2417
|
+
``,
|
|
2418
|
+
fromSlot === void 0 ? `const options = adapter.contractOptions?.options ?? {}` : `const options = ${JSON.stringify(fromSlot)}`,
|
|
2419
|
+
`const unreachable = adapter.contractOptions?.unreachable`,
|
|
2420
|
+
``,
|
|
2421
|
+
`async function build(config) {`,
|
|
2422
|
+
` const built = await adapter.createAdapter(config)`,
|
|
2423
|
+
` if (typeof built?.initialize === 'function') await built.initialize(config)`,
|
|
2424
|
+
` return built`,
|
|
2425
|
+
`}`,
|
|
2426
|
+
``,
|
|
2427
|
+
`contractSuites[${JSON.stringify(kind)}](${JSON.stringify(pkg)}, () => build(options), {`,
|
|
2428
|
+
` ...(unreachable !== undefined && { unreachable: () => build(unreachable) }),`,
|
|
2429
|
+
`})`,
|
|
2430
|
+
``
|
|
2431
|
+
].join("\n")
|
|
2432
|
+
);
|
|
2433
|
+
const { startVitest } = await import(pathToFileURL2(vitestNode).href);
|
|
2434
|
+
const vitest = await startVitest(
|
|
2435
|
+
"test",
|
|
2436
|
+
[],
|
|
2437
|
+
{
|
|
2438
|
+
config: false,
|
|
2439
|
+
root: contracts,
|
|
2440
|
+
include: [file],
|
|
2441
|
+
watch: false,
|
|
2442
|
+
testTimeout: 6e4,
|
|
2443
|
+
hookTimeout: 6e4
|
|
2444
|
+
},
|
|
2445
|
+
{ cacheDir: join6(contracts, ".vite") }
|
|
2446
|
+
);
|
|
2447
|
+
if (vitest === void 0) throw new CertifyError(`vitest did not start for ${pkg}`);
|
|
2448
|
+
const files = vitest.state.getFiles();
|
|
2449
|
+
const { passed, failed } = countCases(files);
|
|
2450
|
+
const broken = files.some((f) => f.result?.state === "fail") || vitest.state.getUnhandledErrors().length > 0;
|
|
2451
|
+
await vitest.close();
|
|
2452
|
+
const total = passed + failed;
|
|
2453
|
+
if (failed === 0 && total > 0 && !broken) {
|
|
2454
|
+
process.stdout.write(`certified ${pkg} (${kind}): ${passed} cases passed
|
|
2455
|
+
`);
|
|
2456
|
+
return 0;
|
|
2457
|
+
}
|
|
2458
|
+
process.stdout.write(`not certified ${pkg} (${kind}): ${failed} of ${total} cases failed
|
|
2459
|
+
`);
|
|
2460
|
+
return 1;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// src/commands/test.ts
|
|
2464
|
+
var testCommand = defineCommand14({
|
|
1565
2465
|
meta: { name: "test", description: "Run unit + integration tests (vitest)" },
|
|
1566
2466
|
async run() {
|
|
1567
2467
|
await runOrFail("pnpm", ["vitest", "run"]);
|
|
1568
2468
|
}
|
|
1569
2469
|
});
|
|
1570
|
-
var testE2eCommand =
|
|
2470
|
+
var testE2eCommand = defineCommand14({
|
|
1571
2471
|
meta: { name: "test:e2e", description: "Run Playwright E2E against the configured template" },
|
|
1572
2472
|
async run() {
|
|
1573
|
-
const { config } = await
|
|
2473
|
+
const { config } = await loadConfig4();
|
|
1574
2474
|
await runOrFail("pnpm", [
|
|
1575
2475
|
"--filter",
|
|
1576
2476
|
"@nymbalhq/e2e",
|
|
@@ -1584,7 +2484,7 @@ var testE2eCommand = defineCommand9({
|
|
|
1584
2484
|
]);
|
|
1585
2485
|
}
|
|
1586
2486
|
});
|
|
1587
|
-
var testVisualCommand =
|
|
2487
|
+
var testVisualCommand = defineCommand14({
|
|
1588
2488
|
meta: { name: "test:visual", description: "Run visual regression tests (Playwright toHaveScreenshot)" },
|
|
1589
2489
|
args: {
|
|
1590
2490
|
"update-baselines": {
|
|
@@ -1594,7 +2494,7 @@ var testVisualCommand = defineCommand9({
|
|
|
1594
2494
|
}
|
|
1595
2495
|
},
|
|
1596
2496
|
async run({ args }) {
|
|
1597
|
-
const { config } = await
|
|
2497
|
+
const { config } = await loadConfig4();
|
|
1598
2498
|
const isCI = !!process.env.CI;
|
|
1599
2499
|
const updateBaselines = args["update-baselines"];
|
|
1600
2500
|
if (updateBaselines && !isCI) {
|
|
@@ -1629,12 +2529,35 @@ var testVisualCommand = defineCommand9({
|
|
|
1629
2529
|
}
|
|
1630
2530
|
}
|
|
1631
2531
|
});
|
|
1632
|
-
var testContractsCommand =
|
|
1633
|
-
meta: { name: "test:contracts", description: "Run adapter contract tests" },
|
|
2532
|
+
var testContractsCommand = defineCommand14({
|
|
2533
|
+
meta: { name: "test:contracts", description: "Run adapter contract tests, or certify an adapter package" },
|
|
1634
2534
|
args: {
|
|
1635
|
-
adapter: { type: "string", description: "Run only contracts for a specific adapter (e.g. payments, email)" }
|
|
2535
|
+
adapter: { type: "string", description: "Run only contracts for a specific adapter (e.g. payments, email)" },
|
|
2536
|
+
package: {
|
|
2537
|
+
type: "string",
|
|
2538
|
+
description: "Certify an installed adapter package against the published suite for its kind"
|
|
2539
|
+
},
|
|
2540
|
+
slot: { type: "string", description: "With --package: the commerce slot whose config builds it" }
|
|
1636
2541
|
},
|
|
1637
2542
|
async run({ args }) {
|
|
2543
|
+
if (args.package) {
|
|
2544
|
+
const code = await certifyPackage({
|
|
2545
|
+
pkg: String(args.package),
|
|
2546
|
+
...args.slot !== void 0 && { slot: String(args.slot) },
|
|
2547
|
+
cwd: process.cwd()
|
|
2548
|
+
}).catch((err2) => {
|
|
2549
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
2550
|
+
const line = err2 instanceof CertifyError ? message : `test:contracts: ${message}`;
|
|
2551
|
+
process.stderr.write(`${line.split("\n")[0]}
|
|
2552
|
+
`);
|
|
2553
|
+
return 1;
|
|
2554
|
+
});
|
|
2555
|
+
await Promise.all([
|
|
2556
|
+
new Promise((done) => process.stdout.write("", done)),
|
|
2557
|
+
new Promise((done) => process.stderr.write("", done))
|
|
2558
|
+
]);
|
|
2559
|
+
process.exit(code);
|
|
2560
|
+
}
|
|
1638
2561
|
const vArgs = [
|
|
1639
2562
|
"vitest",
|
|
1640
2563
|
"run",
|
|
@@ -1647,10 +2570,10 @@ var testContractsCommand = defineCommand9({
|
|
|
1647
2570
|
await runOrFail("pnpm", vArgs);
|
|
1648
2571
|
}
|
|
1649
2572
|
});
|
|
1650
|
-
var testPerformanceCommand =
|
|
2573
|
+
var testPerformanceCommand = defineCommand14({
|
|
1651
2574
|
meta: { name: "test:performance", description: "Run Lighthouse CI against the configured template" },
|
|
1652
2575
|
async run() {
|
|
1653
|
-
const { config } = await
|
|
2576
|
+
const { config } = await loadConfig4();
|
|
1654
2577
|
const port = config.template === "nextjs" ? 3e3 : 4321;
|
|
1655
2578
|
await runOrFail("pnpm", ["lhci", "autorun"], {
|
|
1656
2579
|
env: {
|
|
@@ -1661,7 +2584,7 @@ var testPerformanceCommand = defineCommand9({
|
|
|
1661
2584
|
});
|
|
1662
2585
|
}
|
|
1663
2586
|
});
|
|
1664
|
-
var testAllCommand =
|
|
2587
|
+
var testAllCommand = defineCommand14({
|
|
1665
2588
|
meta: { name: "test:all", description: "Run unit, contract, e2e, visual, and performance tests" },
|
|
1666
2589
|
async run() {
|
|
1667
2590
|
const steps = [
|
|
@@ -1676,7 +2599,7 @@ var testAllCommand = defineCommand9({
|
|
|
1676
2599
|
process.exit(code);
|
|
1677
2600
|
}
|
|
1678
2601
|
}
|
|
1679
|
-
const { config } = await
|
|
2602
|
+
const { config } = await loadConfig4();
|
|
1680
2603
|
await runOrFail("pnpm", [
|
|
1681
2604
|
"--filter",
|
|
1682
2605
|
"@nymbalhq/e2e",
|
|
@@ -1706,7 +2629,7 @@ var testAllCommand = defineCommand9({
|
|
|
1706
2629
|
});
|
|
1707
2630
|
|
|
1708
2631
|
// src/index.ts
|
|
1709
|
-
var main =
|
|
2632
|
+
var main = defineCommand15({
|
|
1710
2633
|
meta: {
|
|
1711
2634
|
name: "nymbal",
|
|
1712
2635
|
version: getCliVersion(),
|
|
@@ -1721,6 +2644,14 @@ var main = defineCommand10({
|
|
|
1721
2644
|
"config:validate": configValidateCommand,
|
|
1722
2645
|
add: addCommand,
|
|
1723
2646
|
doctor: doctorCommand,
|
|
2647
|
+
"security:plan": securityPlanCommand,
|
|
2648
|
+
"security:apply": securityApplyCommand,
|
|
2649
|
+
"security:drift": securityDriftCommand,
|
|
2650
|
+
"security:inspect": securityInspectCommand,
|
|
2651
|
+
secrets: secretsCommand,
|
|
2652
|
+
events: eventsCommand,
|
|
2653
|
+
audit: auditCommand,
|
|
2654
|
+
replay: replayCommand,
|
|
1724
2655
|
test: testCommand,
|
|
1725
2656
|
"test:e2e": testE2eCommand,
|
|
1726
2657
|
"test:visual": testVisualCommand,
|