create-noy-db 0.9.0 → 0.10.0-pre.1
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/README.md +34 -23
- package/dist/bin/create.js +41 -33
- package/dist/bin/create.js.map +1 -1
- package/dist/bin/noy-db.d.ts +57 -0
- package/dist/bin/noy-db.js +1725 -56
- package/dist/bin/noy-db.js.map +1 -1
- package/dist/index.d.ts +278 -4
- package/dist/index.js +1617 -131
- package/dist/index.js.map +1 -1
- package/package.json +20 -10
- package/templates/catalog.json +651 -0
- package/templates/electron/README.md +3 -3
- package/templates/minimal/README.md +35 -0
- package/templates/minimal/_gitignore +29 -0
- package/templates/minimal/index.html +20 -0
- package/templates/minimal/package.json +21 -0
- package/templates/minimal/src/main.ts +47 -0
- package/templates/minimal/tsconfig.json +17 -0
- package/templates/minimal/vite.config.ts +10 -0
- package/templates/nuxt-default/README.md +4 -5
- package/templates/vanilla/README.md +3 -4
- package/templates/vite-vue/README.md +3 -4
package/dist/bin/noy-db.js
CHANGED
|
@@ -1,7 +1,149 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/wizard/augment.ts
|
|
13
|
+
var augment_exports = {};
|
|
14
|
+
__export(augment_exports, {
|
|
15
|
+
augmentNuxtConfig: () => augmentNuxtConfig,
|
|
16
|
+
writeAugmentedConfig: () => writeAugmentedConfig
|
|
17
|
+
});
|
|
18
|
+
import { promises as fs4 } from "fs";
|
|
19
|
+
import { loadFile, generateCode, builders } from "magicast";
|
|
20
|
+
import { createPatch } from "diff";
|
|
21
|
+
async function augmentNuxtConfig(options) {
|
|
22
|
+
const originalCode = await fs4.readFile(options.configPath, "utf8");
|
|
23
|
+
const mod = await loadFile(options.configPath);
|
|
24
|
+
const exported = mod.exports.default;
|
|
25
|
+
if (exported === void 0 || exported === null) {
|
|
26
|
+
return {
|
|
27
|
+
kind: "unsupported-shape",
|
|
28
|
+
configPath: options.configPath,
|
|
29
|
+
reason: `${options.configPath} has no default export. Expected \`export default defineNuxtConfig({...})\` or \`export default {...}\`.`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const config = resolveConfigObject(exported);
|
|
33
|
+
if (!config) {
|
|
34
|
+
return {
|
|
35
|
+
kind: "unsupported-shape",
|
|
36
|
+
configPath: options.configPath,
|
|
37
|
+
reason: `Could not find the config object in ${options.configPath}. Expected \`export default defineNuxtConfig({ modules: [], ... })\` or a plain object literal.`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const skipReasons = [];
|
|
41
|
+
const modulesRaw = config.modules;
|
|
42
|
+
let modulesWasMissing = false;
|
|
43
|
+
if (modulesRaw === void 0) {
|
|
44
|
+
modulesWasMissing = true;
|
|
45
|
+
config.modules = [];
|
|
46
|
+
} else if (typeof modulesRaw !== "object" || !isProxyArray(modulesRaw)) {
|
|
47
|
+
return {
|
|
48
|
+
kind: "unsupported-shape",
|
|
49
|
+
configPath: options.configPath,
|
|
50
|
+
reason: `\`modules\` in ${options.configPath} is not an array literal. Edit it manually and re-run the wizard if you want to continue.`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const modules = config.modules;
|
|
54
|
+
const alreadyHasModule = Array.from(modules).some((m) => String(m) === "@noy-db/in-nuxt");
|
|
55
|
+
if (alreadyHasModule) {
|
|
56
|
+
skipReasons.push("`@noy-db/in-nuxt` already in modules");
|
|
57
|
+
} else {
|
|
58
|
+
modules.push("@noy-db/in-nuxt");
|
|
59
|
+
}
|
|
60
|
+
const noydbRaw = config.noydb;
|
|
61
|
+
if (noydbRaw !== void 0) {
|
|
62
|
+
skipReasons.push("`noydb` key already set");
|
|
63
|
+
} else {
|
|
64
|
+
config.noydb = builders.raw(
|
|
65
|
+
`{ adapter: '${options.adapter}', pinia: true, devtools: true }`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (skipReasons.length === 2 && !modulesWasMissing) {
|
|
69
|
+
return {
|
|
70
|
+
kind: "already-configured",
|
|
71
|
+
configPath: options.configPath,
|
|
72
|
+
reason: skipReasons.join("; ")
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const generated = generateCode(mod).code;
|
|
76
|
+
const diff = createPatch(
|
|
77
|
+
options.configPath,
|
|
78
|
+
originalCode,
|
|
79
|
+
generated,
|
|
80
|
+
"",
|
|
81
|
+
"",
|
|
82
|
+
{ context: 3 }
|
|
83
|
+
);
|
|
84
|
+
return {
|
|
85
|
+
kind: "proposed-change",
|
|
86
|
+
configPath: options.configPath,
|
|
87
|
+
originalCode,
|
|
88
|
+
newCode: generated,
|
|
89
|
+
diff,
|
|
90
|
+
dryRun: options.dryRun === true
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function writeAugmentedConfig(configPath, newCode) {
|
|
94
|
+
await fs4.writeFile(configPath, newCode, "utf8");
|
|
95
|
+
}
|
|
96
|
+
function resolveConfigObject(exported) {
|
|
97
|
+
if (!exported || typeof exported !== "object") return null;
|
|
98
|
+
const proxy = exported;
|
|
99
|
+
if (proxy.$type === "function-call" && proxy.$args) {
|
|
100
|
+
const firstArg = proxy.$args[0];
|
|
101
|
+
if (firstArg && typeof firstArg === "object") {
|
|
102
|
+
return firstArg;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (proxy.$type === "object" || proxy.$type === void 0) {
|
|
107
|
+
return proxy;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function isProxyArray(value) {
|
|
112
|
+
if (!value || typeof value !== "object") return false;
|
|
113
|
+
const proxy = value;
|
|
114
|
+
return proxy.$type === "array";
|
|
115
|
+
}
|
|
116
|
+
var init_augment = __esm({
|
|
117
|
+
"src/wizard/augment.ts"() {
|
|
118
|
+
"use strict";
|
|
119
|
+
}
|
|
120
|
+
});
|
|
2
121
|
|
|
3
122
|
// src/bin/noy-db.ts
|
|
4
|
-
import
|
|
123
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
124
|
+
import pc2 from "picocolors";
|
|
125
|
+
|
|
126
|
+
// src/commands/resolve-bin.ts
|
|
127
|
+
import { existsSync, readFileSync } from "fs";
|
|
128
|
+
import { dirname, join } from "path";
|
|
129
|
+
function resolveBin(pkg, binName, from = process.cwd()) {
|
|
130
|
+
let dir = from;
|
|
131
|
+
for (; ; ) {
|
|
132
|
+
const manifestPath = join(dir, "node_modules", ...pkg.split("/"), "package.json");
|
|
133
|
+
if (existsSync(manifestPath)) {
|
|
134
|
+
try {
|
|
135
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
136
|
+
const rel = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[binName];
|
|
137
|
+
return rel ? join(dirname(manifestPath), rel) : null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const parent = dirname(dir);
|
|
143
|
+
if (parent === dir) return null;
|
|
144
|
+
dir = parent;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
5
147
|
|
|
6
148
|
// src/commands/add.ts
|
|
7
149
|
import { promises as fs } from "fs";
|
|
@@ -307,36 +449,1490 @@ async function backup(options) {
|
|
|
307
449
|
}
|
|
308
450
|
}
|
|
309
451
|
|
|
452
|
+
// src/commands/pod/inspect.ts
|
|
453
|
+
import { readPodHeader } from "@noy-db/hub";
|
|
454
|
+
import { readFile } from "fs/promises";
|
|
455
|
+
async function inspect(filePath) {
|
|
456
|
+
const bytes = await readFile(filePath);
|
|
457
|
+
const header = readPodHeader(new Uint8Array(bytes));
|
|
458
|
+
return {
|
|
459
|
+
formatVersion: header.formatVersion,
|
|
460
|
+
handle: header.handle,
|
|
461
|
+
bodyBytes: header.bodyBytes,
|
|
462
|
+
bodySha256: header.bodySha256
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async function runInspect(argv) {
|
|
466
|
+
const file = argv[0];
|
|
467
|
+
if (!file) {
|
|
468
|
+
process.stderr.write("usage: noy-db pod inspect <file.noydb>\n");
|
|
469
|
+
return 2;
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const info = await inspect(file);
|
|
473
|
+
process.stdout.write(JSON.stringify(info, null, 2) + "\n");
|
|
474
|
+
return 0;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
process.stderr.write(`inspect failed: ${err.message}
|
|
477
|
+
`);
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/commands/pod/verify.ts
|
|
483
|
+
import { readPod, readPodHeader as readPodHeader2 } from "@noy-db/hub";
|
|
484
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
485
|
+
async function verify(filePath) {
|
|
486
|
+
const bytes = new Uint8Array(await readFile2(filePath));
|
|
487
|
+
const checks = { magic: false, header: false, bodyHash: false };
|
|
488
|
+
let handle = "";
|
|
489
|
+
let bodyBytes = 0;
|
|
490
|
+
try {
|
|
491
|
+
const header = readPodHeader2(bytes);
|
|
492
|
+
checks.magic = true;
|
|
493
|
+
checks.header = true;
|
|
494
|
+
handle = header.handle;
|
|
495
|
+
bodyBytes = header.bodyBytes;
|
|
496
|
+
await readPod(bytes);
|
|
497
|
+
checks.bodyHash = true;
|
|
498
|
+
return { ok: true, file: filePath, handle, bodyBytes, checks };
|
|
499
|
+
} catch (err) {
|
|
500
|
+
return {
|
|
501
|
+
ok: false,
|
|
502
|
+
file: filePath,
|
|
503
|
+
handle,
|
|
504
|
+
bodyBytes,
|
|
505
|
+
checks,
|
|
506
|
+
error: err.message
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function runVerify(argv) {
|
|
511
|
+
const file = argv[0];
|
|
512
|
+
if (!file) {
|
|
513
|
+
process.stderr.write("usage: noy-db pod verify <file.noydb>\n");
|
|
514
|
+
return 2;
|
|
515
|
+
}
|
|
516
|
+
const report = await verify(file);
|
|
517
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
518
|
+
return report.ok ? 0 : 1;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/commands/pod/describe.ts
|
|
522
|
+
import { readFile as readFile3, writeFile, mkdir } from "fs/promises";
|
|
523
|
+
import { dirname as dirname2, basename, join as join2 } from "path";
|
|
524
|
+
import { createHash } from "crypto";
|
|
525
|
+
import { stringify as yamlStringify } from "yaml";
|
|
526
|
+
import { createNoydb as createNoydb5, readPod as readPod2, loadPersistedSchema } from "@noy-db/hub";
|
|
527
|
+
import { ConflictError } from "@noy-db/hub/to";
|
|
528
|
+
var VERSION = "0.1.0";
|
|
529
|
+
function memoryStore2() {
|
|
530
|
+
const data = /* @__PURE__ */ new Map();
|
|
531
|
+
const getColl = (v, c) => {
|
|
532
|
+
let vm = data.get(v);
|
|
533
|
+
if (!vm) {
|
|
534
|
+
vm = /* @__PURE__ */ new Map();
|
|
535
|
+
data.set(v, vm);
|
|
536
|
+
}
|
|
537
|
+
let cm = vm.get(c);
|
|
538
|
+
if (!cm) {
|
|
539
|
+
cm = /* @__PURE__ */ new Map();
|
|
540
|
+
vm.set(c, cm);
|
|
541
|
+
}
|
|
542
|
+
return cm;
|
|
543
|
+
};
|
|
544
|
+
return {
|
|
545
|
+
async get(v, c, id) {
|
|
546
|
+
return data.get(v)?.get(c)?.get(id) ?? null;
|
|
547
|
+
},
|
|
548
|
+
async put(v, c, id, env, ev) {
|
|
549
|
+
const coll = getColl(v, c);
|
|
550
|
+
const ex = coll.get(id);
|
|
551
|
+
if (ev !== void 0 && ex && ex._v !== ev) throw new ConflictError(ex._v);
|
|
552
|
+
coll.set(id, env);
|
|
553
|
+
},
|
|
554
|
+
async delete(v, c, id) {
|
|
555
|
+
data.get(v)?.get(c)?.delete(id);
|
|
556
|
+
},
|
|
557
|
+
async list(v, c) {
|
|
558
|
+
return [...data.get(v)?.get(c)?.keys() ?? []];
|
|
559
|
+
},
|
|
560
|
+
async loadAll(v) {
|
|
561
|
+
const vm = data.get(v);
|
|
562
|
+
const snap = {};
|
|
563
|
+
if (vm) for (const [cn, cm] of vm) {
|
|
564
|
+
if (cn.startsWith("_")) continue;
|
|
565
|
+
const r = {};
|
|
566
|
+
for (const [id, e] of cm) r[id] = e;
|
|
567
|
+
snap[cn] = r;
|
|
568
|
+
}
|
|
569
|
+
return snap;
|
|
570
|
+
},
|
|
571
|
+
async saveAll(v, snap) {
|
|
572
|
+
for (const [cn, recs] of Object.entries(snap)) {
|
|
573
|
+
const coll = getColl(v, cn);
|
|
574
|
+
for (const [id, env] of Object.entries(recs)) coll.set(id, env);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
async function describeBundle(opts) {
|
|
580
|
+
const bytes = await readFile3(opts.bundlePath);
|
|
581
|
+
const sourceSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
582
|
+
const { dumpJson } = await readPod2(new Uint8Array(bytes));
|
|
583
|
+
const backup2 = JSON.parse(dumpJson);
|
|
584
|
+
const compartmentName = backup2._compartment ?? "vault";
|
|
585
|
+
const db = await createNoydb5({
|
|
586
|
+
store: memoryStore2(),
|
|
587
|
+
user: opts.user,
|
|
588
|
+
secret: opts.secret
|
|
589
|
+
});
|
|
590
|
+
const vault = await db.openVault(compartmentName);
|
|
591
|
+
await vault.load(dumpJson);
|
|
592
|
+
const collectionNames = await vault.collections();
|
|
593
|
+
for (const name of collectionNames) {
|
|
594
|
+
if (!name.startsWith("_")) vault.collection(name);
|
|
595
|
+
}
|
|
596
|
+
const snapshot = await vault.dumpSchema({
|
|
597
|
+
withStats: opts.withStats,
|
|
598
|
+
sampleSize: opts.sampleSize
|
|
599
|
+
});
|
|
600
|
+
const enriched = opts.schemas === "full" ? await enrichWithFullSchemas(snapshot, vault, collectionNames) : snapshot;
|
|
601
|
+
const { _noydb_snapshot: _ignore, ...rest } = enriched;
|
|
602
|
+
void _ignore;
|
|
603
|
+
const withProvenance = {
|
|
604
|
+
_noydb_snapshot: 2,
|
|
605
|
+
// #1447 — carried through from the snapshot rather than restated, so a CLI
|
|
606
|
+
// dump reports the same emitter capabilities the vault did. Restating it
|
|
607
|
+
// here would let the two drift, and a reader cannot tell which is right.
|
|
608
|
+
reports: rest.reports,
|
|
609
|
+
_provenance: {
|
|
610
|
+
generatedBy: `noy-db describe v${VERSION}`,
|
|
611
|
+
source: basename(opts.bundlePath),
|
|
612
|
+
sourceSha256,
|
|
613
|
+
emittedAt: enriched.emittedAt
|
|
614
|
+
},
|
|
615
|
+
vault: rest.vault,
|
|
616
|
+
emittedAt: rest.emittedAt,
|
|
617
|
+
subsystems: rest.subsystems,
|
|
618
|
+
collections: rest.collections,
|
|
619
|
+
materializedViews: rest.materializedViews,
|
|
620
|
+
overlayViews: rest.overlayViews,
|
|
621
|
+
derivations: rest.derivations,
|
|
622
|
+
...rest.internal !== void 0 ? { internal: rest.internal } : {}
|
|
623
|
+
};
|
|
624
|
+
if (opts.schemas === "sidecar" && opts.outPath) {
|
|
625
|
+
await writeSidecarSchemas(opts.outPath, vault, collectionNames);
|
|
626
|
+
}
|
|
627
|
+
const emitted = emit(withProvenance, opts.format);
|
|
628
|
+
if (opts.outPath) {
|
|
629
|
+
await mkdir(dirname2(opts.outPath), { recursive: true });
|
|
630
|
+
await writeFile(opts.outPath, emitted);
|
|
631
|
+
}
|
|
632
|
+
return emitted;
|
|
633
|
+
}
|
|
634
|
+
async function enrichWithFullSchemas(snapshot, vault, collectionNames) {
|
|
635
|
+
const state = vault._introspectState();
|
|
636
|
+
const collections = {};
|
|
637
|
+
for (const name of Object.keys(snapshot.collections)) {
|
|
638
|
+
const base = snapshot.collections[name];
|
|
639
|
+
if (!base) continue;
|
|
640
|
+
if (!collectionNames.includes(name)) {
|
|
641
|
+
collections[name] = base;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
const dek = await state.getDEK(name);
|
|
646
|
+
const persisted = await loadPersistedSchema(state.adapter, state.name, name, dek);
|
|
647
|
+
if (persisted?.jsonSchema) {
|
|
648
|
+
collections[name] = { ...base, jsonSchema: persisted.jsonSchema };
|
|
649
|
+
} else {
|
|
650
|
+
collections[name] = base;
|
|
651
|
+
}
|
|
652
|
+
} catch {
|
|
653
|
+
collections[name] = base;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return { ...snapshot, collections };
|
|
657
|
+
}
|
|
658
|
+
async function writeSidecarSchemas(outPath, vault, collectionNames) {
|
|
659
|
+
const state = vault._introspectState();
|
|
660
|
+
const sidecarDir = `${outPath}.schemas`;
|
|
661
|
+
await mkdir(sidecarDir, { recursive: true });
|
|
662
|
+
for (const name of collectionNames) {
|
|
663
|
+
if (name.startsWith("_")) continue;
|
|
664
|
+
try {
|
|
665
|
+
const dek = await state.getDEK(name);
|
|
666
|
+
const persisted = await loadPersistedSchema(state.adapter, state.name, name, dek);
|
|
667
|
+
if (persisted?.jsonSchema) {
|
|
668
|
+
await writeFile(
|
|
669
|
+
join2(sidecarDir, `${name}.schema.json`),
|
|
670
|
+
JSON.stringify(persisted.jsonSchema, null, 2)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
} catch {
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function emit(snapshot, format) {
|
|
678
|
+
if (format === "json") {
|
|
679
|
+
return JSON.stringify(snapshot, null, 2) + "\n";
|
|
680
|
+
}
|
|
681
|
+
return yamlStringify(snapshot, {
|
|
682
|
+
aliasDuplicateObjects: false,
|
|
683
|
+
indent: 2,
|
|
684
|
+
lineWidth: 0
|
|
685
|
+
// never wrap; preserves long strings
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function parseArgs(argv) {
|
|
689
|
+
const out = {
|
|
690
|
+
bundlePath: "",
|
|
691
|
+
format: "yaml",
|
|
692
|
+
withStats: false,
|
|
693
|
+
schemas: "none",
|
|
694
|
+
sampleSize: 50
|
|
695
|
+
};
|
|
696
|
+
let i = 0;
|
|
697
|
+
while (i < argv.length) {
|
|
698
|
+
const arg = argv[i];
|
|
699
|
+
if (arg === "--format") {
|
|
700
|
+
const v = argv[++i];
|
|
701
|
+
if (v !== "yaml" && v !== "json") return { error: `--format must be yaml or json (got: ${v ?? "<none>"})` };
|
|
702
|
+
out.format = v;
|
|
703
|
+
} else if (arg === "--with-stats") {
|
|
704
|
+
out.withStats = true;
|
|
705
|
+
} else if (arg === "--schemas") {
|
|
706
|
+
const v = argv[++i];
|
|
707
|
+
if (v !== "none" && v !== "full" && v !== "sidecar") {
|
|
708
|
+
return { error: `--schemas must be none|full|sidecar (got: ${v ?? "<none>"})` };
|
|
709
|
+
}
|
|
710
|
+
out.schemas = v;
|
|
711
|
+
} else if (arg === "-o" || arg === "--out") {
|
|
712
|
+
const v = argv[++i];
|
|
713
|
+
if (v === void 0) return { error: "-o requires a value" };
|
|
714
|
+
out.outPath = v;
|
|
715
|
+
} else if (arg === "--secret") {
|
|
716
|
+
const v = argv[++i];
|
|
717
|
+
if (v === void 0) return { error: "--secret requires a value" };
|
|
718
|
+
out.secret = v;
|
|
719
|
+
} else if (arg === "--user") {
|
|
720
|
+
const v = argv[++i];
|
|
721
|
+
if (v === void 0) return { error: "--user requires a value" };
|
|
722
|
+
out.user = v;
|
|
723
|
+
} else if (arg === "--sample") {
|
|
724
|
+
const n = parseInt(argv[++i] ?? "", 10);
|
|
725
|
+
if (Number.isNaN(n) || n < 0) return { error: `--sample must be a non-negative integer` };
|
|
726
|
+
out.sampleSize = n;
|
|
727
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
728
|
+
return { error: "help" };
|
|
729
|
+
} else if (arg?.startsWith("-")) {
|
|
730
|
+
return { error: `unknown option: ${arg}` };
|
|
731
|
+
} else if (!out.bundlePath) {
|
|
732
|
+
out.bundlePath = arg ?? "";
|
|
733
|
+
} else {
|
|
734
|
+
return { error: `unexpected positional arg: ${arg}` };
|
|
735
|
+
}
|
|
736
|
+
i++;
|
|
737
|
+
}
|
|
738
|
+
if (!out.bundlePath) return { error: "missing bundle path" };
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
function usage() {
|
|
742
|
+
return [
|
|
743
|
+
"usage: noy-db describe <bundle-path> [options]",
|
|
744
|
+
"",
|
|
745
|
+
" --format <yaml|json> output format (default: yaml)",
|
|
746
|
+
" --with-stats include records / bytes / oldest / newest per collection",
|
|
747
|
+
" --schemas <mode> JSON Schema body inclusion (default: none)",
|
|
748
|
+
" - none: summary only",
|
|
749
|
+
" - full: inline complete JSON Schema per collection",
|
|
750
|
+
" - sidecar: write `<out>.schemas/<col>.schema.json` siblings",
|
|
751
|
+
" -o, --out <file> write to file instead of stdout",
|
|
752
|
+
" --user <userId> keyring user identifier (required)",
|
|
753
|
+
" --secret <p> decryption secret (or via NOYDB_SECRET env)",
|
|
754
|
+
" --sample <n> max records sampled when no persisted/live schema (default: 50)",
|
|
755
|
+
"",
|
|
756
|
+
"Exit codes: 0 ok, 1 internal error, 2 usage error, 3 auth failure"
|
|
757
|
+
].join("\n");
|
|
758
|
+
}
|
|
759
|
+
async function runDescribe(argv) {
|
|
760
|
+
const parsed = parseArgs(argv);
|
|
761
|
+
if ("error" in parsed) {
|
|
762
|
+
if (parsed.error === "help") {
|
|
763
|
+
process.stdout.write(usage() + "\n");
|
|
764
|
+
return 0;
|
|
765
|
+
}
|
|
766
|
+
process.stderr.write(`describe: ${parsed.error}
|
|
767
|
+
|
|
768
|
+
${usage()}
|
|
769
|
+
`);
|
|
770
|
+
return 2;
|
|
771
|
+
}
|
|
772
|
+
const secret = parsed.secret ?? process.env.NOYDB_SECRET;
|
|
773
|
+
if (!secret) {
|
|
774
|
+
process.stderr.write("describe: secret required \u2014 pass --secret or set NOYDB_SECRET env\n");
|
|
775
|
+
return 3;
|
|
776
|
+
}
|
|
777
|
+
if (parsed.secret) {
|
|
778
|
+
process.stderr.write(
|
|
779
|
+
"[noy-db] warning: --secret appears in shell history; consider NOYDB_SECRET env instead\n"
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (!parsed.user) {
|
|
783
|
+
process.stderr.write("describe: --user <userId> is required to unlock the bundle\n");
|
|
784
|
+
return 2;
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
const out = await describeBundle({
|
|
788
|
+
bundlePath: parsed.bundlePath,
|
|
789
|
+
user: parsed.user,
|
|
790
|
+
secret,
|
|
791
|
+
format: parsed.format,
|
|
792
|
+
withStats: parsed.withStats,
|
|
793
|
+
schemas: parsed.schemas,
|
|
794
|
+
...parsed.outPath !== void 0 ? { outPath: parsed.outPath } : {},
|
|
795
|
+
sampleSize: parsed.sampleSize
|
|
796
|
+
});
|
|
797
|
+
if (!parsed.outPath) process.stdout.write(out);
|
|
798
|
+
return 0;
|
|
799
|
+
} catch (err) {
|
|
800
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
801
|
+
if (/decrypt|tamper|wrong key|invalid/i.test(msg)) {
|
|
802
|
+
process.stderr.write(`describe: decryption failed \u2014 check --user and --secret
|
|
803
|
+
${msg}
|
|
804
|
+
`);
|
|
805
|
+
return 3;
|
|
806
|
+
}
|
|
807
|
+
process.stderr.write(`describe: ${msg}
|
|
808
|
+
`);
|
|
809
|
+
return 1;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/commands/pod/config.ts
|
|
814
|
+
import { pathToFileURL } from "url";
|
|
815
|
+
import { resolve } from "path";
|
|
816
|
+
function safeStringify(v) {
|
|
817
|
+
if (typeof v === "string") return v;
|
|
818
|
+
if (typeof v === "number" || typeof v === "boolean" || v === null || v === void 0) {
|
|
819
|
+
return String(v);
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
return JSON.stringify(v);
|
|
823
|
+
} catch {
|
|
824
|
+
return "<unserializable>";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function validateOptions(opts) {
|
|
828
|
+
const issues = [];
|
|
829
|
+
if (!isRecord(opts)) {
|
|
830
|
+
issues.push({
|
|
831
|
+
severity: "error",
|
|
832
|
+
code: "not-object",
|
|
833
|
+
path: "<root>",
|
|
834
|
+
message: "NoydbOptions must be an object"
|
|
835
|
+
});
|
|
836
|
+
return { ok: false, issues };
|
|
837
|
+
}
|
|
838
|
+
if (!opts.store) {
|
|
839
|
+
issues.push({
|
|
840
|
+
severity: "error",
|
|
841
|
+
code: "missing-store",
|
|
842
|
+
path: "store",
|
|
843
|
+
message: "`store` is required"
|
|
844
|
+
});
|
|
845
|
+
} else if (!isStoreShape(opts.store)) {
|
|
846
|
+
issues.push({
|
|
847
|
+
severity: "error",
|
|
848
|
+
code: "bad-store-shape",
|
|
849
|
+
path: "store",
|
|
850
|
+
message: "`store` does not expose the 6-method NoydbStore contract"
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
if (opts.sync !== void 0) {
|
|
854
|
+
const targets = normalizeSync(opts.sync);
|
|
855
|
+
targets.forEach((t, i) => validateTarget(t, `sync[${i}]`, issues));
|
|
856
|
+
}
|
|
857
|
+
if (opts.syncPolicy !== void 0 && opts.sync === void 0) {
|
|
858
|
+
issues.push({
|
|
859
|
+
severity: "warn",
|
|
860
|
+
code: "policy-without-sync",
|
|
861
|
+
path: "syncPolicy",
|
|
862
|
+
message: "`syncPolicy` has no effect without a `sync` target"
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
if (typeof opts.user !== "string" || !opts.user) {
|
|
866
|
+
issues.push({
|
|
867
|
+
severity: "warn",
|
|
868
|
+
code: "missing-user",
|
|
869
|
+
path: "user",
|
|
870
|
+
message: '`user` identifier is recommended \u2014 audit entries default to "anonymous" without it'
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
if (opts.secret === void 0 && opts.secret === void 0 && opts.encrypt !== false) {
|
|
874
|
+
issues.push({
|
|
875
|
+
severity: "warn",
|
|
876
|
+
code: "no-secret",
|
|
877
|
+
path: "secret",
|
|
878
|
+
message: "`secret` / `secret` missing and `encrypt` not explicitly false \u2014 vault open will fail"
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
const hasError = issues.some((i) => i.severity === "error");
|
|
882
|
+
return { ok: !hasError, issues };
|
|
883
|
+
}
|
|
884
|
+
async function loadOptionsFromFile(filePath) {
|
|
885
|
+
const abs = resolve(filePath);
|
|
886
|
+
if (abs.endsWith(".ts") || abs.endsWith(".mts") || abs.endsWith(".cts")) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
`TypeScript config files are not directly loadable \u2014 Node has no native .ts loader.
|
|
889
|
+
Options:
|
|
890
|
+
(a) compile first: tsc ${filePath} && noy-db config validate ${filePath.replace(/\.[mc]?ts$/, ".js")}
|
|
891
|
+
(b) run via tsx: npx tsx $(which noy-db) config validate ${filePath}
|
|
892
|
+
(c) rename to .mjs/.js if your config has no TS-only syntax`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
const mod = await import(pathToFileURL(abs).href);
|
|
896
|
+
const value = mod.default ?? mod;
|
|
897
|
+
if (typeof value === "function") {
|
|
898
|
+
return await value();
|
|
899
|
+
}
|
|
900
|
+
return value;
|
|
901
|
+
}
|
|
902
|
+
async function runConfigValidate(argv) {
|
|
903
|
+
const file = argv[0];
|
|
904
|
+
if (!file) {
|
|
905
|
+
process.stderr.write("usage: noy-db config validate <file.ts|js>\n");
|
|
906
|
+
return 2;
|
|
907
|
+
}
|
|
908
|
+
let opts;
|
|
909
|
+
try {
|
|
910
|
+
opts = await loadOptionsFromFile(file);
|
|
911
|
+
} catch (err) {
|
|
912
|
+
process.stderr.write(`failed to load ${file}: ${err.message}
|
|
913
|
+
`);
|
|
914
|
+
return 1;
|
|
915
|
+
}
|
|
916
|
+
const report = validateOptions(opts);
|
|
917
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
918
|
+
return report.ok ? 0 : 1;
|
|
919
|
+
}
|
|
920
|
+
var PROFILE_IDS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"];
|
|
921
|
+
function scaffold(profile) {
|
|
922
|
+
switch (profile) {
|
|
923
|
+
case "A":
|
|
924
|
+
return {
|
|
925
|
+
profile,
|
|
926
|
+
notes: "Local-only single-user. No cloud.",
|
|
927
|
+
code: [
|
|
928
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
929
|
+
`import { toFile } from '@noy-db/to-file'`,
|
|
930
|
+
``,
|
|
931
|
+
`export default {`,
|
|
932
|
+
` store: toFile({ dir: process.env.NOYDB_DATA_DIR ?? './data' }),`,
|
|
933
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
934
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
935
|
+
`}`
|
|
936
|
+
].join("\n"),
|
|
937
|
+
env: `NOYDB_USER=owner
|
|
938
|
+
NOYDB_SECRET=
|
|
939
|
+
NOYDB_DATA_DIR=./data
|
|
940
|
+
`
|
|
941
|
+
};
|
|
942
|
+
case "B":
|
|
943
|
+
return {
|
|
944
|
+
profile,
|
|
945
|
+
notes: "Offline-first + cloud mirror. Local authoritative, sync opportunistically.",
|
|
946
|
+
code: [
|
|
947
|
+
`import { createNoydb, INDEXED_STORE_POLICY } from '@noy-db/hub'`,
|
|
948
|
+
`import { toBrowserIdb } from '@noy-db/to-browser-idb'`,
|
|
949
|
+
`import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
|
|
950
|
+
``,
|
|
951
|
+
`export default {`,
|
|
952
|
+
` store: toBrowserIdb({ prefix: process.env.NOYDB_APP ?? 'myapp' }),`,
|
|
953
|
+
` sync: [{`,
|
|
954
|
+
` store: toAwsDynamo({`,
|
|
955
|
+
` table: process.env.NOYDB_DYNAMO_TABLE!,`,
|
|
956
|
+
` region: process.env.AWS_REGION ?? 'us-east-1',`,
|
|
957
|
+
` }),`,
|
|
958
|
+
` role: 'sync-peer',`,
|
|
959
|
+
` label: 'dynamo-live',`,
|
|
960
|
+
` }],`,
|
|
961
|
+
` syncPolicy: INDEXED_STORE_POLICY,`,
|
|
962
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
963
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
964
|
+
`}`
|
|
965
|
+
].join("\n"),
|
|
966
|
+
env: `NOYDB_USER=owner
|
|
967
|
+
NOYDB_SECRET=
|
|
968
|
+
NOYDB_APP=myapp
|
|
969
|
+
NOYDB_DYNAMO_TABLE=myapp-live
|
|
970
|
+
AWS_REGION=us-east-1
|
|
971
|
+
`
|
|
972
|
+
};
|
|
973
|
+
case "C":
|
|
974
|
+
return {
|
|
975
|
+
profile,
|
|
976
|
+
notes: "Records + blobs split (routeStore). Dynamo for records, S3 for blobs.",
|
|
977
|
+
code: [
|
|
978
|
+
`import { createNoydb, routeStore } from '@noy-db/hub'`,
|
|
979
|
+
`import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
|
|
980
|
+
`import { toAwsS3 } from '@noy-db/to-aws-s3'`,
|
|
981
|
+
``,
|
|
982
|
+
`export default {`,
|
|
983
|
+
` store: routeStore({`,
|
|
984
|
+
` default: toAwsDynamo({`,
|
|
985
|
+
` table: process.env.NOYDB_DYNAMO_TABLE!,`,
|
|
986
|
+
` region: process.env.AWS_REGION ?? 'us-east-1',`,
|
|
987
|
+
` }),`,
|
|
988
|
+
` blobs: toAwsS3({`,
|
|
989
|
+
` bucket: process.env.NOYDB_S3_BUCKET!,`,
|
|
990
|
+
` region: process.env.AWS_REGION ?? 'us-east-1',`,
|
|
991
|
+
` }),`,
|
|
992
|
+
` }),`,
|
|
993
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
994
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
995
|
+
`}`
|
|
996
|
+
].join("\n"),
|
|
997
|
+
env: `NOYDB_USER=owner
|
|
998
|
+
NOYDB_SECRET=
|
|
999
|
+
NOYDB_DYNAMO_TABLE=myapp-records
|
|
1000
|
+
NOYDB_S3_BUCKET=myapp-blobs
|
|
1001
|
+
AWS_REGION=us-east-1
|
|
1002
|
+
`
|
|
1003
|
+
};
|
|
1004
|
+
case "G":
|
|
1005
|
+
return {
|
|
1006
|
+
profile,
|
|
1007
|
+
notes: "Middleware-hardened production: retry + breaker + cache + metrics.",
|
|
1008
|
+
code: [
|
|
1009
|
+
`import { createNoydb, wrapStore, withRetry, withCircuitBreaker, withCache, withHealthCheck, withMetrics } from '@noy-db/hub'`,
|
|
1010
|
+
`import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
|
|
1011
|
+
``,
|
|
1012
|
+
`export default {`,
|
|
1013
|
+
` store: wrapStore(`,
|
|
1014
|
+
` toAwsDynamo({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
|
|
1015
|
+
` withRetry({ maxRetries: 3 }),`,
|
|
1016
|
+
` withCircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 }),`,
|
|
1017
|
+
` withCache({ ttlMs: 60_000 }),`,
|
|
1018
|
+
` withHealthCheck(),`,
|
|
1019
|
+
` withMetrics({ onOperation: op => console.log(op) }),`,
|
|
1020
|
+
` ),`,
|
|
1021
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
1022
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
1023
|
+
`}`
|
|
1024
|
+
].join("\n"),
|
|
1025
|
+
env: `NOYDB_USER=owner
|
|
1026
|
+
NOYDB_SECRET=
|
|
1027
|
+
NOYDB_DYNAMO_TABLE=myapp-prod
|
|
1028
|
+
`
|
|
1029
|
+
};
|
|
1030
|
+
case "D":
|
|
1031
|
+
return {
|
|
1032
|
+
profile,
|
|
1033
|
+
notes: "Hot + cold tiered. Records age out to archive after N days.",
|
|
1034
|
+
code: [
|
|
1035
|
+
`import { createNoydb, routeStore } from '@noy-db/hub'`,
|
|
1036
|
+
`import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
|
|
1037
|
+
`import { toAwsS3 } from '@noy-db/to-aws-s3'`,
|
|
1038
|
+
``,
|
|
1039
|
+
`export default {`,
|
|
1040
|
+
` store: routeStore({`,
|
|
1041
|
+
` default: toAwsDynamo({ table: process.env.NOYDB_DYNAMO_TABLE! }),`,
|
|
1042
|
+
` age: {`,
|
|
1043
|
+
` cold: toAwsS3({ bucket: process.env.NOYDB_S3_COLD! }),`,
|
|
1044
|
+
` coldAfterDays: Number(process.env.NOYDB_COLD_AFTER_DAYS ?? '90'),`,
|
|
1045
|
+
` },`,
|
|
1046
|
+
` }),`,
|
|
1047
|
+
` user: process.env.NOYDB_USER ?? 'owner',`,
|
|
1048
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
1049
|
+
`}`
|
|
1050
|
+
].join("\n"),
|
|
1051
|
+
env: `NOYDB_USER=owner
|
|
1052
|
+
NOYDB_SECRET=
|
|
1053
|
+
NOYDB_DYNAMO_TABLE=myapp-hot
|
|
1054
|
+
NOYDB_S3_COLD=myapp-archive
|
|
1055
|
+
NOYDB_COLD_AFTER_DAYS=90
|
|
1056
|
+
`
|
|
1057
|
+
};
|
|
1058
|
+
case "E":
|
|
1059
|
+
return {
|
|
1060
|
+
profile,
|
|
1061
|
+
notes: "Multi-peer team sync. Primary + peer + backup + archive.",
|
|
1062
|
+
code: [
|
|
1063
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
1064
|
+
`import { toBrowserIdb } from '@noy-db/to-browser-idb'`,
|
|
1065
|
+
`import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
|
|
1066
|
+
`import { toAwsS3 } from '@noy-db/to-aws-s3'`,
|
|
1067
|
+
``,
|
|
1068
|
+
`export default {`,
|
|
1069
|
+
` store: toBrowserIdb({ prefix: process.env.NOYDB_APP ?? 'team' }),`,
|
|
1070
|
+
` sync: [`,
|
|
1071
|
+
` { store: toAwsDynamo({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer', label: 'team-hot' },`,
|
|
1072
|
+
` { store: toAwsS3({ bucket: process.env.NOYDB_S3_BACKUP! }), role: 'backup', label: 'team-backup' },`,
|
|
1073
|
+
` { store: toAwsS3({ bucket: process.env.NOYDB_S3_ARCHIVE! }), role: 'archive', label: 'team-archive' },`,
|
|
1074
|
+
` ],`,
|
|
1075
|
+
` user: process.env.NOYDB_USER ?? 'member',`,
|
|
1076
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
1077
|
+
`}`
|
|
1078
|
+
].join("\n"),
|
|
1079
|
+
env: `NOYDB_USER=member
|
|
1080
|
+
NOYDB_SECRET=
|
|
1081
|
+
NOYDB_APP=team
|
|
1082
|
+
NOYDB_DYNAMO_TABLE=team-hot
|
|
1083
|
+
NOYDB_S3_BACKUP=team-backup
|
|
1084
|
+
NOYDB_S3_ARCHIVE=team-archive
|
|
1085
|
+
`
|
|
1086
|
+
};
|
|
1087
|
+
case "F":
|
|
1088
|
+
return {
|
|
1089
|
+
profile,
|
|
1090
|
+
notes: "CRDT collaboration \u2014 Yjs-backed shared records over the encrypted envelope.",
|
|
1091
|
+
code: [
|
|
1092
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
1093
|
+
`import { toBrowserIdb } from '@noy-db/to-browser-idb'`,
|
|
1094
|
+
`import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
|
|
1095
|
+
`// import { yjsCollection } from '@noy-db/in-yjs' // use inside your app`,
|
|
1096
|
+
``,
|
|
1097
|
+
`export default {`,
|
|
1098
|
+
` store: toBrowserIdb({ prefix: 'collab' }),`,
|
|
1099
|
+
` sync: [{ store: toAwsDynamo({ table: process.env.NOYDB_DYNAMO_TABLE! }), role: 'sync-peer' }],`,
|
|
1100
|
+
` user: process.env.NOYDB_USER!,`,
|
|
1101
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
1102
|
+
`}`,
|
|
1103
|
+
``,
|
|
1104
|
+
`// After createNoydb(), replace normal collections with yjsCollection() for CRDT fields.`
|
|
1105
|
+
].join("\n"),
|
|
1106
|
+
env: `NOYDB_USER=
|
|
1107
|
+
NOYDB_SECRET=
|
|
1108
|
+
NOYDB_DYNAMO_TABLE=collab-live
|
|
1109
|
+
`
|
|
1110
|
+
};
|
|
1111
|
+
case "H":
|
|
1112
|
+
return {
|
|
1113
|
+
profile,
|
|
1114
|
+
notes: "USB-portable \u2014 everything on a single file store, no cloud.",
|
|
1115
|
+
code: [
|
|
1116
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
1117
|
+
`import { toFile } from '@noy-db/to-file'`,
|
|
1118
|
+
``,
|
|
1119
|
+
`export default {`,
|
|
1120
|
+
` store: toFile({ dir: process.env.NOYDB_DATA_DIR ?? '/Volumes/MY_USB/data' }),`,
|
|
1121
|
+
` user: process.env.NOYDB_USER!,`,
|
|
1122
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
1123
|
+
`}`
|
|
1124
|
+
].join("\n"),
|
|
1125
|
+
env: `NOYDB_USER=
|
|
1126
|
+
NOYDB_SECRET=
|
|
1127
|
+
NOYDB_DATA_DIR=/Volumes/MY_USB/data
|
|
1128
|
+
`
|
|
1129
|
+
};
|
|
1130
|
+
case "I":
|
|
1131
|
+
return {
|
|
1132
|
+
profile,
|
|
1133
|
+
notes: "Multi-tenant geographic sharding \u2014 per-vault primary per region.",
|
|
1134
|
+
code: [
|
|
1135
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
1136
|
+
`import { toAwsDynamo } from '@noy-db/to-aws-dynamo'`,
|
|
1137
|
+
``,
|
|
1138
|
+
`// Pick the nearest region at init time based on tenant config.`,
|
|
1139
|
+
`const region = process.env.NOYDB_REGION ?? 'ap-southeast-1'`,
|
|
1140
|
+
`const table = \`myapp-\${region}\``,
|
|
1141
|
+
``,
|
|
1142
|
+
`export default {`,
|
|
1143
|
+
` store: toAwsDynamo({ table, region }),`,
|
|
1144
|
+
` user: process.env.NOYDB_USER!,`,
|
|
1145
|
+
` secret: process.env.NOYDB_SECRET,`,
|
|
1146
|
+
`}`
|
|
1147
|
+
].join("\n"),
|
|
1148
|
+
env: `NOYDB_USER=
|
|
1149
|
+
NOYDB_SECRET=
|
|
1150
|
+
NOYDB_REGION=ap-southeast-1
|
|
1151
|
+
`
|
|
1152
|
+
};
|
|
1153
|
+
case "J":
|
|
1154
|
+
return {
|
|
1155
|
+
profile,
|
|
1156
|
+
notes: "Authentication bridge (secret-less unlock via OIDC or WebAuthn).",
|
|
1157
|
+
code: [
|
|
1158
|
+
`import { createNoydb } from '@noy-db/hub'`,
|
|
1159
|
+
`import { toBrowserIdb } from '@noy-db/to-browser-idb'`,
|
|
1160
|
+
`// import { unlockWebAuthn } from '@noy-db/on-webauthn' // in the browser`,
|
|
1161
|
+
`// import { unlockOidc } from '@noy-db/on-oidc' // in a server app`,
|
|
1162
|
+
``,
|
|
1163
|
+
`export default {`,
|
|
1164
|
+
` store: toBrowserIdb({ prefix: process.env.NOYDB_APP ?? 'app' }),`,
|
|
1165
|
+
` user: process.env.NOYDB_USER!,`,
|
|
1166
|
+
` // secret supplied by the unlock method at openVault() time, not here.`,
|
|
1167
|
+
`}`
|
|
1168
|
+
].join("\n"),
|
|
1169
|
+
env: `NOYDB_USER=
|
|
1170
|
+
NOYDB_APP=app
|
|
1171
|
+
`
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function listProfiles() {
|
|
1176
|
+
return PROFILE_IDS.map((id) => ` ${id} ${scaffold(id).notes}`).join("\n");
|
|
1177
|
+
}
|
|
1178
|
+
function runConfigProfiles() {
|
|
1179
|
+
process.stdout.write(`Topology profiles for \`noy-db config scaffold --profile=<id>\`:
|
|
1180
|
+
${listProfiles()}
|
|
1181
|
+
`);
|
|
1182
|
+
return 0;
|
|
1183
|
+
}
|
|
1184
|
+
async function runConfigScaffold(argv) {
|
|
1185
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1186
|
+
process.stdout.write(`Usage: noy-db config scaffold [--profile=<id>] (default A)
|
|
1187
|
+
|
|
1188
|
+
${listProfiles()}
|
|
1189
|
+
|
|
1190
|
+
The config goes to stdout; the .env template goes to stderr.
|
|
1191
|
+
`);
|
|
1192
|
+
return 0;
|
|
1193
|
+
}
|
|
1194
|
+
const profileArg = argv.find((a) => a.startsWith("--profile="));
|
|
1195
|
+
const profile = profileArg?.split("=")[1] ?? "A";
|
|
1196
|
+
if (!PROFILE_IDS.includes(profile)) {
|
|
1197
|
+
process.stderr.write(`unknown profile: ${profile}. Valid: ${PROFILE_IDS.join(" ")} \u2014 run \`noy-db config profiles\`
|
|
1198
|
+
`);
|
|
1199
|
+
return 2;
|
|
1200
|
+
}
|
|
1201
|
+
const out = scaffold(profile);
|
|
1202
|
+
process.stdout.write(`// \u2500\u2500 noy-db config (profile ${out.profile}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1203
|
+
`);
|
|
1204
|
+
process.stdout.write(`// ${out.notes}
|
|
1205
|
+
|
|
1206
|
+
`);
|
|
1207
|
+
process.stdout.write(out.code + "\n");
|
|
1208
|
+
if (out.env) {
|
|
1209
|
+
process.stderr.write(`
|
|
1210
|
+
# \u2500\u2500 .env template (profile ${out.profile}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1211
|
+
`);
|
|
1212
|
+
process.stderr.write(out.env);
|
|
1213
|
+
}
|
|
1214
|
+
return 0;
|
|
1215
|
+
}
|
|
1216
|
+
function isRecord(v) {
|
|
1217
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1218
|
+
}
|
|
1219
|
+
function isStoreShape(v) {
|
|
1220
|
+
if (!isRecord(v)) return false;
|
|
1221
|
+
return ["get", "put", "delete", "list", "loadAll", "saveAll"].every((m) => typeof v[m] === "function");
|
|
1222
|
+
}
|
|
1223
|
+
function normalizeSync(v) {
|
|
1224
|
+
if (Array.isArray(v)) return v;
|
|
1225
|
+
return [v];
|
|
1226
|
+
}
|
|
1227
|
+
function validateTarget(t, path4, issues) {
|
|
1228
|
+
if (!isRecord(t)) {
|
|
1229
|
+
issues.push({
|
|
1230
|
+
severity: "error",
|
|
1231
|
+
code: "bad-target",
|
|
1232
|
+
path: path4,
|
|
1233
|
+
message: "sync target must be an object with { store, role }"
|
|
1234
|
+
});
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (t["role"] === void 0) {
|
|
1238
|
+
if (!isStoreShape(t)) {
|
|
1239
|
+
issues.push({
|
|
1240
|
+
severity: "error",
|
|
1241
|
+
code: "bad-store-shape",
|
|
1242
|
+
path: path4,
|
|
1243
|
+
message: "sync target store does not expose the 6-method contract"
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const validRoles = ["sync-peer", "backup", "archive"];
|
|
1249
|
+
if (!validRoles.includes(safeStringify(t["role"]))) {
|
|
1250
|
+
issues.push({
|
|
1251
|
+
severity: "error",
|
|
1252
|
+
code: "bad-role",
|
|
1253
|
+
path: `${path4}.role`,
|
|
1254
|
+
message: `role must be one of ${validRoles.join(", ")}; got ${safeStringify(t["role"])}`
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
if (!t["store"] || !isStoreShape(t["store"])) {
|
|
1258
|
+
issues.push({
|
|
1259
|
+
severity: "error",
|
|
1260
|
+
code: "bad-store-shape",
|
|
1261
|
+
path: `${path4}.store`,
|
|
1262
|
+
message: "sync target store does not expose the 6-method contract"
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
if (t["role"] === "archive" && isRecord(t["policy"]) && isRecord(t["policy"]["pull"])) {
|
|
1266
|
+
issues.push({
|
|
1267
|
+
severity: "error",
|
|
1268
|
+
code: "archive-pull-configured",
|
|
1269
|
+
path: `${path4}.policy.pull`,
|
|
1270
|
+
message: "archive targets are push-only \u2014 pull policy is invalid"
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/commands/pod/monitor.ts
|
|
1276
|
+
async function runMonitor(argv) {
|
|
1277
|
+
const file = argv[0];
|
|
1278
|
+
if (!file) {
|
|
1279
|
+
process.stderr.write("usage: noy-db monitor <config.ts> [--interval=ms]\n");
|
|
1280
|
+
return 2;
|
|
1281
|
+
}
|
|
1282
|
+
const intervalArg = argv.find((a) => a.startsWith("--interval="));
|
|
1283
|
+
const intervalMs = intervalArg ? parseInt(intervalArg.split("=")[1] ?? "5000", 10) : 5e3;
|
|
1284
|
+
let opts;
|
|
1285
|
+
try {
|
|
1286
|
+
const loaded = await loadOptionsFromFile(file);
|
|
1287
|
+
if (typeof loaded !== "object" || loaded === null) {
|
|
1288
|
+
process.stderr.write(`config file must export a NoydbOptions-shaped object
|
|
1289
|
+
`);
|
|
1290
|
+
return 1;
|
|
1291
|
+
}
|
|
1292
|
+
opts = loaded;
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
process.stderr.write(`failed to load ${file}: ${err.message}
|
|
1295
|
+
`);
|
|
1296
|
+
return 1;
|
|
1297
|
+
}
|
|
1298
|
+
const innerStore = opts["store"];
|
|
1299
|
+
if (!innerStore || typeof innerStore !== "object") {
|
|
1300
|
+
process.stderr.write("config has no `store` \u2014 nothing to monitor\n");
|
|
1301
|
+
return 1;
|
|
1302
|
+
}
|
|
1303
|
+
const meter = innerStore.meter;
|
|
1304
|
+
if (!meter || typeof meter.snapshot !== "function") {
|
|
1305
|
+
process.stderr.write(
|
|
1306
|
+
"config `store` carries no meter handle \u2014 wrap it in the config file:\n import { toMeter } from '@noy-db/to-meter'\n export default { store: toMeter(inner, { degradedMs: 500 }), ... }\nonDegraded / onRestored belong there too.\n"
|
|
1307
|
+
);
|
|
1308
|
+
return 1;
|
|
1309
|
+
}
|
|
1310
|
+
const hub = await import("@noy-db/hub");
|
|
1311
|
+
await hub.createNoydb(opts);
|
|
1312
|
+
process.stdout.write(`monitoring ${file} \u2014 interval ${intervalMs}ms \u2014 Ctrl-C to stop
|
|
1313
|
+
|
|
1314
|
+
`);
|
|
1315
|
+
const stop = installSigintHandler(meter);
|
|
1316
|
+
return new Promise((resolveP) => {
|
|
1317
|
+
const timer = setInterval(() => {
|
|
1318
|
+
if (stop.signalled) {
|
|
1319
|
+
clearInterval(timer);
|
|
1320
|
+
meter.close();
|
|
1321
|
+
resolveP(0);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const snap = meter.snapshot();
|
|
1325
|
+
process.stdout.write(formatSnapshot(snap) + "\n");
|
|
1326
|
+
}, intervalMs);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
function installSigintHandler(meter) {
|
|
1330
|
+
const state = { signalled: false };
|
|
1331
|
+
const handler = () => {
|
|
1332
|
+
state.signalled = true;
|
|
1333
|
+
meter.close();
|
|
1334
|
+
};
|
|
1335
|
+
process.on("SIGINT", handler);
|
|
1336
|
+
process.on("SIGTERM", handler);
|
|
1337
|
+
return state;
|
|
1338
|
+
}
|
|
1339
|
+
function formatSnapshot(snap) {
|
|
1340
|
+
const lines = [];
|
|
1341
|
+
const ts = new Date(snap.collectedAt).toISOString().slice(11, 19);
|
|
1342
|
+
lines.push(`[${ts}] status=${snap.status} calls=${snap.totalCalls} casConflicts=${snap.casConflicts} windowMs=${snap.windowMs}`);
|
|
1343
|
+
for (const m of ["get", "put", "delete", "list", "loadAll", "saveAll"]) {
|
|
1344
|
+
const s = snap.byMethod[m];
|
|
1345
|
+
if (s.count === 0) continue;
|
|
1346
|
+
lines.push(` ${m.padEnd(7)} count=${s.count} errors=${s.errors} p50=${s.p50}ms p99=${s.p99}ms max=${s.max}ms avg=${s.avg}ms`);
|
|
1347
|
+
}
|
|
1348
|
+
return lines.join("\n");
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/commands/doctor.ts
|
|
1352
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1353
|
+
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
1354
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1355
|
+
var f = (id, level, message, fix, where) => where === void 0 ? { id, level, message, fix } : { id, level, message, fix, where };
|
|
1356
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null;
|
|
1357
|
+
function gitignores(root, name) {
|
|
1358
|
+
const p = join3(root, ".gitignore");
|
|
1359
|
+
if (!existsSync2(p)) return false;
|
|
1360
|
+
return readFileSync2(p, "utf8").split("\n").some((l) => {
|
|
1361
|
+
const t = l.trim();
|
|
1362
|
+
return t === name || t === `/${name}` || t === `${name}*` || t === ".env*" && name.startsWith(".env");
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
async function listSafely(store, vault, collection) {
|
|
1366
|
+
try {
|
|
1367
|
+
return await store.list(vault, collection);
|
|
1368
|
+
} catch {
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
var secretChecks = {
|
|
1373
|
+
id: "NOY-S",
|
|
1374
|
+
run: ({ options, configPath, mode }) => {
|
|
1375
|
+
if (!options) return [];
|
|
1376
|
+
const out = [];
|
|
1377
|
+
const secret = options["secret"];
|
|
1378
|
+
if (typeof secret === "string" && secret.length > 0) {
|
|
1379
|
+
out.push(f(
|
|
1380
|
+
"NOY-S001",
|
|
1381
|
+
"error",
|
|
1382
|
+
"the secret is a literal string in the config \u2014 it ships with your source",
|
|
1383
|
+
"read it from the environment (`secret: process.env.NOYDB_SECRET`) or prompt for it; keep `.env` out of git (see NOY-H001)",
|
|
1384
|
+
configPath
|
|
1385
|
+
));
|
|
1386
|
+
} else if (secret === void 0 && options["getKeyring"] === void 0 && options["secretMode"] !== "managed") {
|
|
1387
|
+
out.push(f(
|
|
1388
|
+
"NOY-S002",
|
|
1389
|
+
mode === "prod" ? "warn" : "info",
|
|
1390
|
+
"no secret and no getKeyring in the config \u2014 the app must supply one at runtime",
|
|
1391
|
+
"that is fine for an app that prompts; set `secret: process.env.NOYDB_SECRET` for a headless process",
|
|
1392
|
+
configPath
|
|
1393
|
+
));
|
|
1394
|
+
}
|
|
1395
|
+
if (options["secretMode"] === "managed" && options["sealingKey"] === void 0) {
|
|
1396
|
+
out.push(f(
|
|
1397
|
+
"NOY-S003",
|
|
1398
|
+
"error",
|
|
1399
|
+
'`secretMode: "managed"` without a `sealingKey` \u2014 managed mode has nothing to seal with',
|
|
1400
|
+
"pass a sealer from an `@noy-db/at-*` package (`sealingKey: awsKms({...})`), or drop managed mode",
|
|
1401
|
+
configPath
|
|
1402
|
+
));
|
|
1403
|
+
}
|
|
1404
|
+
return out;
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
var capsuleChecks = {
|
|
1408
|
+
id: "NOY-C",
|
|
1409
|
+
run: ({ options, configPath, mode, manifest }) => {
|
|
1410
|
+
const out = [];
|
|
1411
|
+
if (options?.["encrypt"] === false) {
|
|
1412
|
+
out.push(f(
|
|
1413
|
+
"NOY-C001",
|
|
1414
|
+
mode === "prod" ? "error" : "warn",
|
|
1415
|
+
"`encrypt: false` \u2014 records are stored as plaintext; this is not zero-knowledge",
|
|
1416
|
+
'remove `encrypt: false` for anything that leaves your machine; it exists for the "see the ciphertext" control and tests',
|
|
1417
|
+
configPath
|
|
1418
|
+
));
|
|
1419
|
+
}
|
|
1420
|
+
const deps = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
1421
|
+
if ("@noy-db/exclave-plain" in deps) {
|
|
1422
|
+
out.push(f(
|
|
1423
|
+
"NOY-C002",
|
|
1424
|
+
mode === "prod" ? "error" : "warn",
|
|
1425
|
+
"`@noy-db/exclave-plain` is installed \u2014 when bound (`noy-db:exclave-plain` condition) the vault stores readable rows",
|
|
1426
|
+
"keep it to builds whose access control lives in the store (IAM, a DB grant); never in a build that serves untrusted readers"
|
|
1427
|
+
));
|
|
1428
|
+
}
|
|
1429
|
+
return out;
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
var topologyChecks = {
|
|
1433
|
+
id: "NOY-T",
|
|
1434
|
+
run: ({ options, configPath, mode }) => {
|
|
1435
|
+
if (!options) return [];
|
|
1436
|
+
const out = [];
|
|
1437
|
+
const store = options["store"];
|
|
1438
|
+
const storeName = isRecord2(store) && typeof store["name"] === "string" ? store["name"] : void 0;
|
|
1439
|
+
if (storeName === "memory" || storeName === "memoryStore") {
|
|
1440
|
+
out.push(f(
|
|
1441
|
+
"NOY-T001",
|
|
1442
|
+
mode === "prod" ? "error" : "info",
|
|
1443
|
+
"the store is in-memory \u2014 data does not survive a reload",
|
|
1444
|
+
"pick a persistent store: `noy-db add @noy-db/to-browser-idb` (browser) or `@noy-db/to-file` (Node)",
|
|
1445
|
+
configPath
|
|
1446
|
+
));
|
|
1447
|
+
}
|
|
1448
|
+
const sync = options["sync"];
|
|
1449
|
+
const hasSync = Array.isArray(sync) ? sync.length > 0 : sync !== void 0 && sync !== null;
|
|
1450
|
+
if (!hasSync) {
|
|
1451
|
+
out.push(f(
|
|
1452
|
+
"NOY-T002",
|
|
1453
|
+
"info",
|
|
1454
|
+
"no sync target \u2014 this vault lives in one store only; lose it and the data is gone",
|
|
1455
|
+
"add a second store as a sync target (a cloud bucket, a file on a USB stick): `noy-db config profiles` shows the shapes",
|
|
1456
|
+
configPath
|
|
1457
|
+
));
|
|
1458
|
+
}
|
|
1459
|
+
return out;
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
var recoveryChecks = {
|
|
1463
|
+
id: "NOY-R",
|
|
1464
|
+
run: async ({ store, vaults, options, mode }) => {
|
|
1465
|
+
if (!store || vaults.length === 0) return [];
|
|
1466
|
+
const out = [];
|
|
1467
|
+
for (const vault of vaults) {
|
|
1468
|
+
const users = await listSafely(store, vault, "_keyring");
|
|
1469
|
+
if (users === null) continue;
|
|
1470
|
+
if (users.length === 0) continue;
|
|
1471
|
+
const entries = async (id) => {
|
|
1472
|
+
try {
|
|
1473
|
+
const env = await store.get(vault, "_meta", id);
|
|
1474
|
+
if (!env) return 0;
|
|
1475
|
+
const doc = JSON.parse(env._data ?? "");
|
|
1476
|
+
return Array.isArray(doc.entries) ? doc.entries.length : 0;
|
|
1477
|
+
} catch {
|
|
1478
|
+
return 0;
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
const paper = await entries("recovery-paper");
|
|
1482
|
+
const shamir = await entries("recovery-shamir");
|
|
1483
|
+
if (paper + shamir === 0) {
|
|
1484
|
+
out.push(f(
|
|
1485
|
+
"NOY-R001",
|
|
1486
|
+
mode === "prod" ? "error" : "warn",
|
|
1487
|
+
`vault "${vault}" has no recovery enrolled \u2014 a forgotten secret means the data is gone`,
|
|
1488
|
+
"enrol one: `noy-db add @noy-db/on-recovery` (printable codes) or `@noy-db/on-shamir` (k-of-n shares), then `db.team.enrollRecovery(...)`",
|
|
1489
|
+
vault
|
|
1490
|
+
));
|
|
1491
|
+
} else if (shamir === 0 && options?.["secretMode"] === "managed") {
|
|
1492
|
+
out.push(f(
|
|
1493
|
+
"NOY-R002",
|
|
1494
|
+
"error",
|
|
1495
|
+
`vault "${vault}" is managed-mode with only paper recovery \u2014 managed mode requires a strong (Shamir) profile`,
|
|
1496
|
+
"enrol Shamir: `noy-db add @noy-db/on-shamir`",
|
|
1497
|
+
vault
|
|
1498
|
+
));
|
|
1499
|
+
}
|
|
1500
|
+
let owners = 0;
|
|
1501
|
+
for (const uid of users) {
|
|
1502
|
+
try {
|
|
1503
|
+
const env = await store.get(vault, "_keyring", uid);
|
|
1504
|
+
const file = env ? JSON.parse(env._data ?? "") : null;
|
|
1505
|
+
if (file?.role === "owner") owners++;
|
|
1506
|
+
} catch {
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (owners === 1 && users.length > 1) {
|
|
1510
|
+
out.push(f(
|
|
1511
|
+
"NOY-R003",
|
|
1512
|
+
"info",
|
|
1513
|
+
`vault "${vault}" has one owner among ${users.length} users`,
|
|
1514
|
+
"a second owner can recover members and rotate keys when the first is unavailable: `noy-db user add <id> owner \u2026`",
|
|
1515
|
+
vault
|
|
1516
|
+
));
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return out;
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
var dependencyChecks = {
|
|
1523
|
+
id: "NOY-D",
|
|
1524
|
+
run: ({ root, manifest, configPath }) => {
|
|
1525
|
+
const out = [];
|
|
1526
|
+
const deps = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
1527
|
+
const hub = deps["@noy-db/hub"];
|
|
1528
|
+
if (!hub) {
|
|
1529
|
+
out.push(f("NOY-D001", "error", "`@noy-db/hub` is not a dependency of this project", "add it: `pnpm add @noy-db/hub`", join3(root, "package.json")));
|
|
1530
|
+
}
|
|
1531
|
+
if (configPath && existsSync2(configPath)) {
|
|
1532
|
+
const src = readFileSync2(configPath, "utf8");
|
|
1533
|
+
const named = new Set([...src.matchAll(/from\s+['"](@noy-db\/[a-z0-9-]+)/g)].map((m) => m[1]));
|
|
1534
|
+
for (const pkg of named) {
|
|
1535
|
+
if (!(pkg in deps)) {
|
|
1536
|
+
out.push(f("NOY-D002", "error", `the config imports \`${pkg}\`, which is not in package.json`, `install it: \`pnpm add ${pkg}\``, configPath));
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return out;
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
var hygieneChecks = {
|
|
1544
|
+
id: "NOY-H",
|
|
1545
|
+
run: ({ root }) => {
|
|
1546
|
+
const out = [];
|
|
1547
|
+
const hasEnv = existsSync2(join3(root, ".env")) || existsSync2(join3(root, ".env.local"));
|
|
1548
|
+
if (hasEnv && !gitignores(root, ".env")) {
|
|
1549
|
+
out.push(f(
|
|
1550
|
+
"NOY-H001",
|
|
1551
|
+
"error",
|
|
1552
|
+
"a `.env` file exists and `.gitignore` does not ignore it \u2014 one `git add .` away from committing secrets",
|
|
1553
|
+
"add `.env` and `.env.*` to .gitignore (`noy-db doctor --fix` does this)",
|
|
1554
|
+
join3(root, ".gitignore")
|
|
1555
|
+
));
|
|
1556
|
+
}
|
|
1557
|
+
return out;
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
var BUILTIN_CHECKS = [secretChecks, capsuleChecks, topologyChecks, recoveryChecks, dependencyChecks, hygieneChecks];
|
|
1561
|
+
async function satelliteChecks(root, manifest) {
|
|
1562
|
+
const checks = [];
|
|
1563
|
+
const findings = [];
|
|
1564
|
+
const names = Object.keys({ ...manifest.dependencies, ...manifest.devDependencies }).filter((n) => n.startsWith("@noy-db/") && n !== "@noy-db/hub");
|
|
1565
|
+
for (const name of names) {
|
|
1566
|
+
const manifestPath = join3(root, "node_modules", ...name.split("/"), "package.json");
|
|
1567
|
+
if (!existsSync2(manifestPath)) continue;
|
|
1568
|
+
try {
|
|
1569
|
+
const pkg = JSON.parse(readFileSync2(manifestPath, "utf8"));
|
|
1570
|
+
const entry = isRecord2(pkg.exports) && "." in pkg.exports ? (() => {
|
|
1571
|
+
const dot = pkg.exports["."];
|
|
1572
|
+
return typeof dot === "string" ? dot : isRecord2(dot) ? dot["default"] ?? dot["import"] : void 0;
|
|
1573
|
+
})() : pkg.module ?? pkg.main;
|
|
1574
|
+
if (!entry) continue;
|
|
1575
|
+
const mod = await import(pathToFileURL2(join3(dirname3(manifestPath), entry)).href);
|
|
1576
|
+
if (Array.isArray(mod.noydbDoctorChecks)) {
|
|
1577
|
+
for (const c of mod.noydbDoctorChecks) if (c && typeof c.id === "string" && typeof c.run === "function") checks.push(c);
|
|
1578
|
+
}
|
|
1579
|
+
} catch (err) {
|
|
1580
|
+
findings.push(f("NOY-D003", "warn", `\`${name}\` is installed but failed to import: ${err.message.split("\n")[0]}`, "reinstall it, or check its peer dependencies", name));
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return { checks, findings };
|
|
1584
|
+
}
|
|
1585
|
+
async function doctor(opts = {}) {
|
|
1586
|
+
const root = resolve2(opts.root ?? process.cwd());
|
|
1587
|
+
const mode = opts.mode ?? (process.env["NODE_ENV"] === "production" ? "prod" : "dev");
|
|
1588
|
+
const manifestPath = join3(root, "package.json");
|
|
1589
|
+
const manifest = existsSync2(manifestPath) ? JSON.parse(readFileSync2(manifestPath, "utf8")) : {};
|
|
1590
|
+
const configPath = opts.configPath ?? ["noydb.config.mjs", "noydb.config.js"].map((n) => join3(root, n)).find((p) => existsSync2(p));
|
|
1591
|
+
let options;
|
|
1592
|
+
let store;
|
|
1593
|
+
const findings = [];
|
|
1594
|
+
if (configPath) {
|
|
1595
|
+
try {
|
|
1596
|
+
const loaded = await loadOptionsFromFile(configPath);
|
|
1597
|
+
if (isRecord2(loaded)) {
|
|
1598
|
+
options = loaded;
|
|
1599
|
+
const s = loaded["store"];
|
|
1600
|
+
if (isRecord2(s) && typeof s["get"] === "function" && typeof s["list"] === "function") store = s;
|
|
1601
|
+
}
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
findings.push(f("NOY-D004", "error", `the config file did not load: ${err.message.split("\n")[0]}`, "run `noy-db config validate <file>` for the details", configPath));
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
let vaults = opts.vaults ?? [];
|
|
1607
|
+
if (vaults.length === 0 && store && typeof store.listVaults === "function") {
|
|
1608
|
+
try {
|
|
1609
|
+
vaults = await store.listVaults();
|
|
1610
|
+
} catch {
|
|
1611
|
+
vaults = [];
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const ctx = { mode, root, manifest, vaults, ...configPath ? { configPath } : {}, ...options ? { options } : {}, ...store ? { store } : {} };
|
|
1615
|
+
const sat = opts.noSatellites ? { checks: [], findings: [] } : await satelliteChecks(root, manifest);
|
|
1616
|
+
findings.push(...sat.findings);
|
|
1617
|
+
const checks = [...BUILTIN_CHECKS, ...sat.checks];
|
|
1618
|
+
const ran = [];
|
|
1619
|
+
for (const c of checks) {
|
|
1620
|
+
ran.push(c.id);
|
|
1621
|
+
try {
|
|
1622
|
+
findings.push(...await c.run(ctx));
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
findings.push(f("NOY-D005", "warn", `check ${c.id} threw: ${err.message.split("\n")[0]}`, "report this against the package that registered the check"));
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const counts = { error: 0, warn: 0, info: 0 };
|
|
1628
|
+
for (const x of findings) counts[x.level]++;
|
|
1629
|
+
return { mode, findings, counts, ran };
|
|
1630
|
+
}
|
|
1631
|
+
function applyFixes(root, report) {
|
|
1632
|
+
const done = [];
|
|
1633
|
+
if (report.findings.some((x) => x.id === "NOY-H001")) {
|
|
1634
|
+
const p = join3(root, ".gitignore");
|
|
1635
|
+
const cur = existsSync2(p) ? readFileSync2(p, "utf8") : "";
|
|
1636
|
+
const add = [".env", ".env.*"].filter((l) => !cur.split("\n").some((x) => x.trim() === l));
|
|
1637
|
+
if (add.length) {
|
|
1638
|
+
const next = cur + (cur.endsWith("\n") || cur === "" ? "" : "\n") + "# noy-db doctor (NOY-H001): never commit secrets\n" + add.join("\n") + "\n";
|
|
1639
|
+
writeFileSync(p, next);
|
|
1640
|
+
done.push(`.gitignore += ${add.join(", ")}`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return done;
|
|
1644
|
+
}
|
|
1645
|
+
var LEVEL_RANK = { error: 0, warn: 1, info: 2 };
|
|
1646
|
+
var ICON = { error: "\u2717", warn: "!", info: "\xB7" };
|
|
1647
|
+
function formatReport(r) {
|
|
1648
|
+
const lines = [`noy-db doctor \u2014 mode ${r.mode}, ${r.ran.length} check group(s)`];
|
|
1649
|
+
if (r.findings.length === 0) lines.push(" \u2713 nothing to report");
|
|
1650
|
+
for (const x of [...r.findings].sort((a, b) => LEVEL_RANK[a.level] - LEVEL_RANK[b.level] || a.id.localeCompare(b.id))) {
|
|
1651
|
+
lines.push(` ${ICON[x.level]} ${x.id} ${x.message}${x.where ? ` (${x.where})` : ""}`);
|
|
1652
|
+
lines.push(` \u2192 ${x.fix}`);
|
|
1653
|
+
}
|
|
1654
|
+
lines.push(` ${r.counts.error} error \xB7 ${r.counts.warn} warn \xB7 ${r.counts.info} info`);
|
|
1655
|
+
return lines.join("\n");
|
|
1656
|
+
}
|
|
1657
|
+
async function runDoctor(argv) {
|
|
1658
|
+
const flag = (n) => argv.find((a) => a.startsWith(`--${n}=`))?.slice(n.length + 3);
|
|
1659
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1660
|
+
process.stdout.write([
|
|
1661
|
+
"Usage: noy-db doctor [config.mjs] [--mode=dev|prod] [--vault=<name>] [--json] [--fix] [--fail-level=error|warn|info]",
|
|
1662
|
+
"",
|
|
1663
|
+
"Readiness checks that need no secret. Ids are stable (NOY-S secret \xB7 NOY-R recovery \xB7 NOY-T topology \xB7",
|
|
1664
|
+
"NOY-C capsule \xB7 NOY-D dependencies \xB7 NOY-H hygiene); every finding carries a fix.",
|
|
1665
|
+
"",
|
|
1666
|
+
" --mode dev (default unless NODE_ENV=production) or prod \u2014 the same finding can be a warn in dev and an error in prod",
|
|
1667
|
+
" --fail-level exit 1 at or above this level. DEFAULT: error. Pass --fail-level=warn to gate a CI job on warnings too.",
|
|
1668
|
+
" --fix apply the idempotent fixes (today: gitignore .env)",
|
|
1669
|
+
" --json machine-readable report",
|
|
1670
|
+
" crypto the in-memory crypto round-trip (formerly `noy-db verify`)"
|
|
1671
|
+
].join("\n") + "\n");
|
|
1672
|
+
return 0;
|
|
1673
|
+
}
|
|
1674
|
+
const configArg = argv.find((a) => !a.startsWith("--"));
|
|
1675
|
+
const modeArg = flag("mode");
|
|
1676
|
+
if (modeArg !== void 0 && modeArg !== "dev" && modeArg !== "prod") {
|
|
1677
|
+
process.stderr.write(`--mode must be dev or prod (got ${modeArg})
|
|
1678
|
+
`);
|
|
1679
|
+
return 2;
|
|
1680
|
+
}
|
|
1681
|
+
const failArg = flag("fail-level") ?? "error";
|
|
1682
|
+
if (!(failArg in LEVEL_RANK)) {
|
|
1683
|
+
process.stderr.write(`--fail-level must be error, warn or info (got ${failArg})
|
|
1684
|
+
`);
|
|
1685
|
+
return 2;
|
|
1686
|
+
}
|
|
1687
|
+
const vault = flag("vault");
|
|
1688
|
+
const report = await doctor({
|
|
1689
|
+
...configArg ? { configPath: resolve2(configArg) } : {},
|
|
1690
|
+
...modeArg ? { mode: modeArg } : {},
|
|
1691
|
+
...vault ? { vaults: [vault] } : {}
|
|
1692
|
+
});
|
|
1693
|
+
let fixed = [];
|
|
1694
|
+
if (argv.includes("--fix")) fixed = applyFixes(process.cwd(), report);
|
|
1695
|
+
if (argv.includes("--json")) process.stdout.write(JSON.stringify({ ...report, fixed }, null, 2) + "\n");
|
|
1696
|
+
else {
|
|
1697
|
+
process.stdout.write(formatReport(report) + "\n");
|
|
1698
|
+
for (const d of fixed) process.stdout.write(` fixed: ${d}
|
|
1699
|
+
`);
|
|
1700
|
+
process.stdout.write(` (exit is non-zero at --fail-level=${failArg}; default error)
|
|
1701
|
+
`);
|
|
1702
|
+
}
|
|
1703
|
+
const worst = Math.min(...report.findings.map((x) => LEVEL_RANK[x.level]), 99);
|
|
1704
|
+
return worst <= LEVEL_RANK[failArg] ? 1 : 0;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/commands/add-package.ts
|
|
1708
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1709
|
+
import { join as join4 } from "path";
|
|
1710
|
+
import { spawnSync } from "child_process";
|
|
1711
|
+
import pc from "picocolors";
|
|
1712
|
+
|
|
1713
|
+
// src/wizard/render.ts
|
|
1714
|
+
import { promises as fs3 } from "fs";
|
|
1715
|
+
import path3 from "path";
|
|
1716
|
+
import { fileURLToPath } from "url";
|
|
1717
|
+
function templateDir(name) {
|
|
1718
|
+
const here = fileURLToPath(import.meta.url);
|
|
1719
|
+
const packageRoot = path3.resolve(path3.dirname(here), "..", "..");
|
|
1720
|
+
return path3.join(packageRoot, "templates", name);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// src/commands/add-package.ts
|
|
1724
|
+
var CATALOG_URL = "https://noy-db.lanna.ai/catalog.json";
|
|
1725
|
+
function bundledCatalog() {
|
|
1726
|
+
return JSON.parse(readFileSync3(templateDir("catalog.json"), "utf8"));
|
|
1727
|
+
}
|
|
1728
|
+
async function loadCatalog(opts = {}) {
|
|
1729
|
+
if (!opts.offline) {
|
|
1730
|
+
try {
|
|
1731
|
+
const ctl = new AbortController();
|
|
1732
|
+
const t = setTimeout(() => ctl.abort(), opts.timeoutMs ?? 3e3);
|
|
1733
|
+
const res = await (opts.fetchImpl ?? fetch)(CATALOG_URL, { signal: ctl.signal });
|
|
1734
|
+
clearTimeout(t);
|
|
1735
|
+
if (res.ok) {
|
|
1736
|
+
const catalog = await res.json();
|
|
1737
|
+
if (catalog.schemaVersion === 1 && Array.isArray(catalog.packages)) return { catalog, source: "live" };
|
|
1738
|
+
}
|
|
1739
|
+
} catch {
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return { catalog: bundledCatalog(), source: "bundled" };
|
|
1743
|
+
}
|
|
1744
|
+
function normalisePrefix(input) {
|
|
1745
|
+
if (!input) return void 0;
|
|
1746
|
+
const s = input.replace(/^@noy-db\//, "").replace(/-$/, "");
|
|
1747
|
+
return /^(to|in|on|as|by|at)$/.test(s) ? `${s}-` : void 0;
|
|
1748
|
+
}
|
|
1749
|
+
function formatList(catalog, prefix) {
|
|
1750
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1751
|
+
for (const p of catalog.packages) {
|
|
1752
|
+
if (prefix && p.prefix !== prefix) continue;
|
|
1753
|
+
(groups.get(p.prefix) ?? groups.set(p.prefix, []).get(p.prefix)).push(p);
|
|
1754
|
+
}
|
|
1755
|
+
const out = [];
|
|
1756
|
+
for (const [pfx, pkgs] of groups) {
|
|
1757
|
+
const g = catalog.prefixes[pfx];
|
|
1758
|
+
out.push(`${pc.bold(pfx)} ${g?.reads ?? ""} ${pc.dim(`\xB7 plaintext: ${g?.plaintext ?? "?"}`)}`);
|
|
1759
|
+
for (const p of pkgs) out.push(` ${p.package.padEnd(30)} ${p.name}${p.status !== "stable" ? pc.dim(` (${p.status})`) : ""}`);
|
|
1760
|
+
out.push("");
|
|
1761
|
+
}
|
|
1762
|
+
out.push(pc.dim(`noy-db add <package> installs one and tells you what trust boundary it crosses.`));
|
|
1763
|
+
return out.join("\n");
|
|
1764
|
+
}
|
|
1765
|
+
function findPackage(catalog, input) {
|
|
1766
|
+
const full = input.startsWith("@noy-db/") ? input : `@noy-db/${input}`;
|
|
1767
|
+
return catalog.packages.find((p) => p.package === full) ?? catalog.packages.find((p) => p.package.endsWith(`-${input}`));
|
|
1768
|
+
}
|
|
1769
|
+
function detectPackageManager(root) {
|
|
1770
|
+
if (existsSync3(join4(root, "bun.lockb")) || existsSync3(join4(root, "bun.lock"))) return "bun";
|
|
1771
|
+
if (existsSync3(join4(root, "yarn.lock"))) return "yarn";
|
|
1772
|
+
if (existsSync3(join4(root, "package-lock.json"))) return "npm";
|
|
1773
|
+
return "pnpm";
|
|
1774
|
+
}
|
|
1775
|
+
function installArgv(pm, pkg, dev) {
|
|
1776
|
+
switch (pm) {
|
|
1777
|
+
case "npm":
|
|
1778
|
+
return ["npm", "install", ...dev ? ["--save-dev"] : [], pkg];
|
|
1779
|
+
case "yarn":
|
|
1780
|
+
return ["yarn", "add", ...dev ? ["--dev"] : [], pkg];
|
|
1781
|
+
case "bun":
|
|
1782
|
+
return ["bun", "add", ...dev ? ["--dev"] : [], pkg];
|
|
1783
|
+
default:
|
|
1784
|
+
return ["pnpm", "add", ...dev ? ["-D"] : [], pkg];
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
function snippetFor(p) {
|
|
1788
|
+
const name = p.package.replace("@noy-db/", "");
|
|
1789
|
+
const camel = name.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
1790
|
+
switch (p.prefix) {
|
|
1791
|
+
case "to-":
|
|
1792
|
+
return `import { ${camel} } from '${p.package}'
|
|
1793
|
+
// createNoydb({ store: ${camel}({ /* \u2026 */ }), user, secret }) \u2014 or as a sync target: sync: [{ store: ${camel}(...), role: 'backup' }]`;
|
|
1794
|
+
case "as-":
|
|
1795
|
+
return `import { ${camel.replace(/^as/, "to")} } from '${p.package}'
|
|
1796
|
+
// await vault.export(${camel.replace(/^as/, "to")}(), { collections: ['\u2026'] }) \u26A0\uFE0F the output is plaintext`;
|
|
1797
|
+
case "on-":
|
|
1798
|
+
return `import \u2026 from '${p.package}'
|
|
1799
|
+
// an unlock method is enrolled per user; see the package README for enroll*/unlock* and \`noy-db doctor\` for what is enrolled`;
|
|
1800
|
+
case "by-":
|
|
1801
|
+
return `import \u2026 from '${p.package}'
|
|
1802
|
+
// a session-share transport; pair it on both peers that already hold the secret`;
|
|
1803
|
+
case "at-":
|
|
1804
|
+
return `import \u2026 from '${p.package}'
|
|
1805
|
+
// createNoydb({ secretMode: 'managed', sealingKey: <provider>(\u2026) }) \u26A0\uFE0F the host you name can decrypt the slice it unseals`;
|
|
1806
|
+
default:
|
|
1807
|
+
return `import \u2026 from '${p.package}'`;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
async function addPackage(input, opts = {}) {
|
|
1811
|
+
const root = opts.root ?? process.cwd();
|
|
1812
|
+
const { catalog } = await loadCatalog({ ...opts.offline !== void 0 ? { offline: opts.offline } : {}, ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {} });
|
|
1813
|
+
const pkg = findPackage(catalog, input);
|
|
1814
|
+
if (!pkg) {
|
|
1815
|
+
const near = catalog.packages.filter((p) => p.package.includes(input.replace(/^@noy-db\//, ""))).map((p) => p.package).slice(0, 5);
|
|
1816
|
+
throw new Error(`no package named "${input}" in the catalog${near.length ? ` \u2014 did you mean: ${near.join(", ")}` : ""}. \`noy-db add --list\` shows them all.`);
|
|
1817
|
+
}
|
|
1818
|
+
const pm = detectPackageManager(root);
|
|
1819
|
+
const command = installArgv(pm, pkg.package, opts.dev ?? false);
|
|
1820
|
+
let installed = false;
|
|
1821
|
+
if (!opts.dryRun) {
|
|
1822
|
+
const r = spawnSync(command[0], command.slice(1), { cwd: root, stdio: "inherit" });
|
|
1823
|
+
if (r.status !== 0) throw new Error(`${command.join(" ")} exited ${r.status ?? "with a signal"}`);
|
|
1824
|
+
installed = true;
|
|
1825
|
+
}
|
|
1826
|
+
let wired = null;
|
|
1827
|
+
if (pkg.prefix === "in-" && pkg.package === "@noy-db/in-nuxt") {
|
|
1828
|
+
const cfg = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"].map((n) => join4(root, n)).find((p) => existsSync3(p));
|
|
1829
|
+
if (cfg && !opts.dryRun) {
|
|
1830
|
+
const { augmentNuxtConfig: augmentNuxtConfig2, writeAugmentedConfig: writeAugmentedConfig2 } = await Promise.resolve().then(() => (init_augment(), augment_exports));
|
|
1831
|
+
const res = await augmentNuxtConfig2({ configPath: cfg, adapter: "browser" });
|
|
1832
|
+
if (res.kind === "proposed-change") {
|
|
1833
|
+
await writeAugmentedConfig2(cfg, res.newCode);
|
|
1834
|
+
wired = "nuxt-module";
|
|
1835
|
+
} else if (res.kind === "already-configured") wired = "nuxt-module";
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const trust = catalog.prefixes[pkg.prefix]?.trust ?? "";
|
|
1839
|
+
return { pkg, installed, command, wired, trust, snippet: snippetFor(pkg) };
|
|
1840
|
+
}
|
|
1841
|
+
async function runAddPackage(argv) {
|
|
1842
|
+
const flags = new Set(argv.filter((a) => a.startsWith("--")));
|
|
1843
|
+
const positional = argv.filter((a) => !a.startsWith("--"));
|
|
1844
|
+
if (flags.has("--help") || flags.has("-h")) {
|
|
1845
|
+
process.stdout.write([
|
|
1846
|
+
"Usage: noy-db add <package> [--dev] [--dry-run] [--offline]",
|
|
1847
|
+
" noy-db add --list [to|in|on|as|by|at]",
|
|
1848
|
+
"",
|
|
1849
|
+
"Installs a @noy-db package with your package manager, wires it where a config can be edited",
|
|
1850
|
+
"(a Nuxt module), prints the import to paste otherwise, and says what trust boundary the layer crosses.",
|
|
1851
|
+
`The list comes from ${CATALOG_URL}, falling back to the copy this version ships.`
|
|
1852
|
+
].join("\n") + "\n");
|
|
1853
|
+
return 0;
|
|
1854
|
+
}
|
|
1855
|
+
if (flags.has("--list")) {
|
|
1856
|
+
const prefix = normalisePrefix(positional[0]);
|
|
1857
|
+
if (positional[0] && !prefix) {
|
|
1858
|
+
process.stderr.write(`unknown prefix "${positional[0]}" \u2014 one of to in on as by at
|
|
1859
|
+
`);
|
|
1860
|
+
return 2;
|
|
1861
|
+
}
|
|
1862
|
+
const { catalog, source } = await loadCatalog({ offline: flags.has("--offline") });
|
|
1863
|
+
process.stdout.write(formatList(catalog, prefix) + "\n" + pc.dim(`(catalog: ${source}${source === "bundled" ? ", " + catalog.packages.length + " packages" : ""})`) + "\n");
|
|
1864
|
+
return 0;
|
|
1865
|
+
}
|
|
1866
|
+
const input = positional[0];
|
|
1867
|
+
if (!input) {
|
|
1868
|
+
process.stderr.write("usage: noy-db add <package> (or `noy-db add --list`)\n");
|
|
1869
|
+
return 2;
|
|
1870
|
+
}
|
|
1871
|
+
try {
|
|
1872
|
+
const r = await addPackage(input, { dev: flags.has("--dev"), dryRun: flags.has("--dry-run"), offline: flags.has("--offline") });
|
|
1873
|
+
process.stdout.write(`${r.installed ? pc.green("\u2714") : pc.dim("dry:")} ${r.command.join(" ")}
|
|
1874
|
+
`);
|
|
1875
|
+
if (r.wired) process.stdout.write(`${pc.green("\u2714")} wired into nuxt.config (${r.wired})
|
|
1876
|
+
`);
|
|
1877
|
+
process.stdout.write(`
|
|
1878
|
+
${pc.bold(r.pkg.prefix)} ${r.trust}
|
|
1879
|
+
|
|
1880
|
+
${r.snippet}
|
|
1881
|
+
`);
|
|
1882
|
+
return 0;
|
|
1883
|
+
} catch (err) {
|
|
1884
|
+
process.stderr.write(`${pc.red("error:")} ${err.message}
|
|
1885
|
+
`);
|
|
1886
|
+
return 1;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
310
1890
|
// src/bin/noy-db.ts
|
|
311
1891
|
var HELP = `Usage: noy-db <command> [args]
|
|
312
1892
|
|
|
313
|
-
|
|
1893
|
+
Project
|
|
314
1894
|
add <collection> Scaffold a new collection store + page
|
|
315
|
-
add
|
|
316
|
-
|
|
317
|
-
|
|
1895
|
+
add <@noy-db/package> [--dev] [--dry-run] Install a package, wire it, print the trust boundary
|
|
1896
|
+
add --list [to|in|on|as|by|at] The landscape \u2014 every package, by layer
|
|
1897
|
+
user add <userId> <role> [options] Grant a new user access to a vault
|
|
1898
|
+
user rotate [options] Rotate DEKs for a vault
|
|
318
1899
|
backup <target> [options] Dump a vault to a file
|
|
319
|
-
|
|
1900
|
+
doctor [config] [--mode=prod] [--fix] Readiness checks: secret, recovery, topology, capsule, deps, hygiene
|
|
1901
|
+
doctor crypto In-memory crypto round-trip check
|
|
1902
|
+
|
|
1903
|
+
Pods (.noydb files)
|
|
1904
|
+
pod inspect <file.noydb> Print the bundle header (no secret)
|
|
1905
|
+
pod verify <file.noydb> Verify bundle integrity (no secret)
|
|
1906
|
+
describe <file.noydb> [options] YAML/JSON schema audit (needs the secret)
|
|
320
1907
|
|
|
321
|
-
|
|
1908
|
+
Configuration
|
|
1909
|
+
config validate <file.js|mjs> Sanity-check a NoydbOptions file
|
|
1910
|
+
config scaffold [--profile=<id>] Topology skeleton \u2192 stdout, .env template \u2192 stderr
|
|
1911
|
+
config profiles List the ten topology profiles
|
|
1912
|
+
|
|
1913
|
+
Running
|
|
1914
|
+
monitor <file.js|mjs> [--interval=ms] Live store metrics (the store must be metered)
|
|
1915
|
+
inspect <file.js|mjs> --vault=<name> [...] Terminal inspector (installs on demand)
|
|
1916
|
+
|
|
1917
|
+
Common options for user add / user rotate / backup:
|
|
322
1918
|
--dir <path> Data directory (file adapter). Default: ./data
|
|
323
|
-
--vault <name>
|
|
1919
|
+
--vault <name> Vault (tenant) name. Required.
|
|
324
1920
|
--user <id> Your user id in the vault. Required.
|
|
325
|
-
--collections <list>
|
|
326
|
-
rotate: invoices,clients
|
|
327
|
-
add user: invoices:rw,clients:ro (operator/client only)
|
|
1921
|
+
--collections <list> rotate: invoices,clients \xB7 user add: invoices:rw,clients:ro
|
|
328
1922
|
|
|
329
1923
|
Examples:
|
|
330
1924
|
noy-db add invoices
|
|
331
|
-
noy-db add
|
|
332
|
-
noy-db
|
|
333
|
-
noy-db
|
|
334
|
-
noy-db
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
adapter directory in place. For new projects, use
|
|
338
|
-
\`npm create noy-db\` instead.
|
|
1925
|
+
noy-db user add ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw
|
|
1926
|
+
noy-db config profiles
|
|
1927
|
+
noy-db config scaffold --profile=B > noydb.config.mjs
|
|
1928
|
+
noy-db doctor --mode=prod --fail-level=warn
|
|
1929
|
+
|
|
1930
|
+
New project? \`npm create noy-db@latest\`.
|
|
339
1931
|
`;
|
|
1932
|
+
function renamed(from, to) {
|
|
1933
|
+
process.stderr.write(`${pc2.dim(`(\`noy-db ${from}\` is now \`noy-db ${to}\`)`)}
|
|
1934
|
+
`);
|
|
1935
|
+
}
|
|
340
1936
|
async function main() {
|
|
341
1937
|
const [, , command, ...rest] = process.argv;
|
|
342
1938
|
switch (command) {
|
|
@@ -346,71 +1942,145 @@ async function main() {
|
|
|
346
1942
|
case "--help":
|
|
347
1943
|
process.stdout.write(HELP);
|
|
348
1944
|
return;
|
|
349
|
-
case "add":
|
|
1945
|
+
case "add": {
|
|
1946
|
+
if (rest[0] === "user") {
|
|
1947
|
+
renamed("add user", "user add");
|
|
1948
|
+
await runAddUser(rest.slice(1));
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
const first = rest[0];
|
|
1952
|
+
if (rest.some((a) => a === "--list" || a === "--help") || first && /^(@noy-db\/|to-|in-|on-|as-|by-|at-)/.test(first)) {
|
|
1953
|
+
process.exit(await runAddPackage(rest));
|
|
1954
|
+
}
|
|
350
1955
|
await runAdd(rest);
|
|
351
1956
|
return;
|
|
352
|
-
|
|
353
|
-
|
|
1957
|
+
}
|
|
1958
|
+
case "user": {
|
|
1959
|
+
const [sub, ...subRest] = rest;
|
|
1960
|
+
if (sub === "add") {
|
|
1961
|
+
await runAddUser(subRest);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (sub === "rotate") {
|
|
1965
|
+
await runRotate(subRest);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
fail2(`\`noy-db user\` takes \`add\` or \`rotate\``);
|
|
354
1969
|
return;
|
|
1970
|
+
}
|
|
355
1971
|
case "rotate":
|
|
1972
|
+
renamed("rotate", "user rotate");
|
|
356
1973
|
await runRotate(rest);
|
|
357
1974
|
return;
|
|
358
1975
|
case "backup":
|
|
359
1976
|
await runBackup(rest);
|
|
360
1977
|
return;
|
|
1978
|
+
case "verify":
|
|
1979
|
+
renamed("verify", "doctor crypto");
|
|
1980
|
+
await runCryptoCheck();
|
|
1981
|
+
return;
|
|
1982
|
+
case "doctor": {
|
|
1983
|
+
if (rest[0] === "crypto") {
|
|
1984
|
+
await runCryptoCheck();
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
process.exit(await runDoctor(rest));
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
case "pod": {
|
|
1991
|
+
const [sub, ...subRest] = rest;
|
|
1992
|
+
if (sub === "inspect") process.exit(await runInspect(subRest));
|
|
1993
|
+
if (sub === "verify") process.exit(await runVerify(subRest));
|
|
1994
|
+
fail2(`\`noy-db pod\` takes \`inspect\` or \`verify\``);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
case "describe":
|
|
1998
|
+
process.exit(await runDescribe(rest));
|
|
1999
|
+
return;
|
|
2000
|
+
case "config": {
|
|
2001
|
+
const [sub, ...subRest] = rest;
|
|
2002
|
+
if (sub === "validate") process.exit(await runConfigValidate(subRest));
|
|
2003
|
+
if (sub === "scaffold") process.exit(await runConfigScaffold(subRest));
|
|
2004
|
+
if (sub === "profiles") process.exit(runConfigProfiles());
|
|
2005
|
+
fail2(`\`noy-db config\` takes \`validate\`, \`scaffold\` or \`profiles\``);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
case "monitor":
|
|
2009
|
+
process.exit(await runMonitor(rest));
|
|
2010
|
+
return;
|
|
2011
|
+
case "inspect":
|
|
2012
|
+
process.exit(runTuiInspect(rest));
|
|
2013
|
+
return;
|
|
361
2014
|
default:
|
|
362
|
-
process.stderr.write(`${
|
|
2015
|
+
process.stderr.write(`${pc2.red("error:")} unknown command '${command}'
|
|
363
2016
|
|
|
364
2017
|
${HELP}`);
|
|
365
2018
|
process.exit(2);
|
|
366
2019
|
}
|
|
367
2020
|
}
|
|
2021
|
+
function fail2(message) {
|
|
2022
|
+
process.stderr.write(`${pc2.red("error:")} ${message}
|
|
2023
|
+
`);
|
|
2024
|
+
process.exit(2);
|
|
2025
|
+
}
|
|
2026
|
+
function runTuiInspect(args) {
|
|
2027
|
+
const bin = resolveBin("@noy-db/in-devtools-tui", "noydb-inspect");
|
|
2028
|
+
if (!bin) {
|
|
2029
|
+
process.stderr.write(
|
|
2030
|
+
`${pc2.red("error:")} the terminal inspector is not installed in this project.
|
|
2031
|
+
|
|
2032
|
+
${pc2.bold("pnpm add -D @noy-db/in-devtools-tui")} ${pc2.dim("(or npm/yarn/bun)")}
|
|
2033
|
+
|
|
2034
|
+
then re-run \`noy-db inspect\`. It needs a config file, --vault=<name>, and a secret (--secret or NOYDB_SECRET).
|
|
2035
|
+
`
|
|
2036
|
+
);
|
|
2037
|
+
return 2;
|
|
2038
|
+
}
|
|
2039
|
+
const r = spawnSync2(process.execPath, [bin, ...args], { stdio: "inherit" });
|
|
2040
|
+
return r.status ?? 1;
|
|
2041
|
+
}
|
|
368
2042
|
async function runAdd(args) {
|
|
369
2043
|
const first = args[0];
|
|
370
|
-
if (first === "user") {
|
|
371
|
-
await runAddUser(args.slice(1));
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
2044
|
if (!first) {
|
|
375
2045
|
process.stderr.write(
|
|
376
|
-
`${
|
|
2046
|
+
`${pc2.red("error:")} \`noy-db add\` requires a collection name
|
|
377
2047
|
|
|
378
2048
|
Examples:
|
|
379
2049
|
noy-db add invoices
|
|
380
|
-
noy-db add
|
|
2050
|
+
noy-db user add alice operator ...
|
|
381
2051
|
`
|
|
382
2052
|
);
|
|
383
2053
|
process.exit(2);
|
|
384
2054
|
}
|
|
385
2055
|
try {
|
|
386
2056
|
const result = await addCollection({ name: first });
|
|
387
|
-
process.stdout.write(`${
|
|
2057
|
+
process.stdout.write(`${pc2.green("\u2714")} Created:
|
|
388
2058
|
`);
|
|
389
2059
|
for (const file of result.files) {
|
|
390
|
-
process.stdout.write(` ${
|
|
2060
|
+
process.stdout.write(` ${pc2.dim("\u2192")} ${file}
|
|
391
2061
|
`);
|
|
392
2062
|
}
|
|
393
2063
|
process.stdout.write(
|
|
394
2064
|
`
|
|
395
|
-
Next: visit ${
|
|
2065
|
+
Next: visit ${pc2.cyan(`/${first}`)} in your dev server, then edit ${pc2.bold(`app/stores/${first}.ts`)} to match your domain.
|
|
396
2066
|
`
|
|
397
2067
|
);
|
|
398
2068
|
} catch (err) {
|
|
399
|
-
process.stderr.write(`${
|
|
2069
|
+
process.stderr.write(`${pc2.red("error:")} ${err.message}
|
|
400
2070
|
`);
|
|
401
2071
|
process.exit(1);
|
|
402
2072
|
}
|
|
403
2073
|
}
|
|
404
|
-
async function
|
|
405
|
-
process.stdout.write(`${
|
|
2074
|
+
async function runCryptoCheck() {
|
|
2075
|
+
process.stdout.write(`${pc2.dim("\u25B8 Running noy-db crypto round-trip\u2026")}
|
|
406
2076
|
`);
|
|
407
2077
|
const result = await verifyIntegrity();
|
|
408
2078
|
if (result.ok) {
|
|
409
|
-
process.stdout.write(`${
|
|
2079
|
+
process.stdout.write(`${pc2.green("\u2714")} ${result.message} ${pc2.dim(`(${result.durationMs}ms)`)}
|
|
410
2080
|
`);
|
|
411
2081
|
return;
|
|
412
2082
|
}
|
|
413
|
-
process.stderr.write(`${
|
|
2083
|
+
process.stderr.write(`${pc2.red("\u2718")} ${result.message} ${pc2.dim(`(${result.durationMs}ms)`)}
|
|
414
2084
|
`);
|
|
415
2085
|
process.exit(1);
|
|
416
2086
|
}
|
|
@@ -419,9 +2089,9 @@ async function runAddUser(args) {
|
|
|
419
2089
|
const roleInput = args[1];
|
|
420
2090
|
if (!userId || !roleInput) {
|
|
421
2091
|
process.stderr.write(
|
|
422
|
-
`${
|
|
2092
|
+
`${pc2.red("error:")} \`noy-db user add\` requires <userId> <role>
|
|
423
2093
|
|
|
424
|
-
Example: noy-db add
|
|
2094
|
+
Example: noy-db user add ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw
|
|
425
2095
|
`
|
|
426
2096
|
);
|
|
427
2097
|
process.exit(2);
|
|
@@ -430,7 +2100,7 @@ Example: noy-db add user ann operator --dir ./data --vault demo-co --user owner-
|
|
|
430
2100
|
try {
|
|
431
2101
|
role = assertRole(roleInput);
|
|
432
2102
|
} catch (err) {
|
|
433
|
-
process.stderr.write(`${
|
|
2103
|
+
process.stderr.write(`${pc2.red("error:")} ${err.message}
|
|
434
2104
|
`);
|
|
435
2105
|
process.exit(2);
|
|
436
2106
|
return;
|
|
@@ -452,11 +2122,11 @@ Example: noy-db add user ann operator --dir ./data --vault demo-co --user owner-
|
|
|
452
2122
|
try {
|
|
453
2123
|
const result = await addUser(opts);
|
|
454
2124
|
process.stdout.write(
|
|
455
|
-
`${
|
|
2125
|
+
`${pc2.green("\u2714")} Granted ${pc2.bold(result.role)} access to ${pc2.cyan(result.userId)} in vault ${pc2.cyan(vault)}.
|
|
456
2126
|
`
|
|
457
2127
|
);
|
|
458
2128
|
} catch (err) {
|
|
459
|
-
process.stderr.write(`${
|
|
2129
|
+
process.stderr.write(`${pc2.red("error:")} ${err.message}
|
|
460
2130
|
`);
|
|
461
2131
|
process.exit(1);
|
|
462
2132
|
}
|
|
@@ -472,15 +2142,15 @@ async function runRotate(args) {
|
|
|
472
2142
|
try {
|
|
473
2143
|
const result = await rotate(opts);
|
|
474
2144
|
process.stdout.write(
|
|
475
|
-
`${
|
|
2145
|
+
`${pc2.green("\u2714")} Rotated ${pc2.bold(String(result.rotated.length))} collection(s) in ${pc2.cyan(vault)}:
|
|
476
2146
|
`
|
|
477
2147
|
);
|
|
478
2148
|
for (const name of result.rotated) {
|
|
479
|
-
process.stdout.write(` ${
|
|
2149
|
+
process.stdout.write(` ${pc2.dim("\u2192")} ${name}
|
|
480
2150
|
`);
|
|
481
2151
|
}
|
|
482
2152
|
} catch (err) {
|
|
483
|
-
process.stderr.write(`${
|
|
2153
|
+
process.stderr.write(`${pc2.red("error:")} ${err.message}
|
|
484
2154
|
`);
|
|
485
2155
|
process.exit(1);
|
|
486
2156
|
}
|
|
@@ -489,7 +2159,7 @@ async function runBackup(args) {
|
|
|
489
2159
|
const target = args[0];
|
|
490
2160
|
if (!target) {
|
|
491
2161
|
process.stderr.write(
|
|
492
|
-
`${
|
|
2162
|
+
`${pc2.red("error:")} \`noy-db backup\` requires a target path
|
|
493
2163
|
|
|
494
2164
|
Example: noy-db backup ./backups/demo.json --dir ./data --vault demo-co --user owner-alice
|
|
495
2165
|
`
|
|
@@ -503,11 +2173,11 @@ Example: noy-db backup ./backups/demo.json --dir ./data --vault demo-co --user o
|
|
|
503
2173
|
try {
|
|
504
2174
|
const result = await backup({ dir, vault, user, target });
|
|
505
2175
|
process.stdout.write(
|
|
506
|
-
`${
|
|
2176
|
+
`${pc2.green("\u2714")} Wrote backup: ${pc2.cyan(result.path)} ${pc2.dim(`(${result.bytes} bytes)`)}
|
|
507
2177
|
`
|
|
508
2178
|
);
|
|
509
2179
|
} catch (err) {
|
|
510
|
-
process.stderr.write(`${
|
|
2180
|
+
process.stderr.write(`${pc2.red("error:")} ${err.message}
|
|
511
2181
|
`);
|
|
512
2182
|
process.exit(1);
|
|
513
2183
|
}
|
|
@@ -531,10 +2201,8 @@ function parseFlags(args) {
|
|
|
531
2201
|
function requireFlag(flags, name) {
|
|
532
2202
|
const value = flags[name];
|
|
533
2203
|
if (!value) {
|
|
534
|
-
process.stderr.write(
|
|
535
|
-
|
|
536
|
-
`
|
|
537
|
-
);
|
|
2204
|
+
process.stderr.write(`${pc2.red("error:")} missing required flag --${name}
|
|
2205
|
+
`);
|
|
538
2206
|
process.exit(2);
|
|
539
2207
|
}
|
|
540
2208
|
return value;
|
|
@@ -545,17 +2213,18 @@ function parsePermissions(input) {
|
|
|
545
2213
|
for (const pair of input.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
546
2214
|
const [name, mode] = pair.split(":");
|
|
547
2215
|
if (!name || !mode || mode !== "rw" && mode !== "ro") {
|
|
548
|
-
throw new Error(
|
|
549
|
-
`Invalid --collections entry "${pair}" \u2014 expected "name:rw" or "name:ro"`
|
|
550
|
-
);
|
|
2216
|
+
throw new Error(`Invalid --collections entry "${pair}" \u2014 expected "name:rw" or "name:ro"`);
|
|
551
2217
|
}
|
|
552
2218
|
out[name] = mode;
|
|
553
2219
|
}
|
|
554
2220
|
return Object.keys(out).length > 0 ? out : null;
|
|
555
2221
|
}
|
|
556
2222
|
main().catch((err) => {
|
|
557
|
-
process.stderr.write(`${
|
|
2223
|
+
process.stderr.write(`${pc2.red("fatal:")} ${err.message}
|
|
558
2224
|
`);
|
|
559
2225
|
process.exit(1);
|
|
560
2226
|
});
|
|
2227
|
+
export {
|
|
2228
|
+
runTuiInspect
|
|
2229
|
+
};
|
|
561
2230
|
//# sourceMappingURL=noy-db.js.map
|