create-noy-db 0.3.1-pre.3 → 0.3.1-pre.5
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 +17 -9
- package/dist/bin/noy-db.js +18 -18
- package/dist/bin/noy-db.js.map +1 -1
- package/dist/index.d.ts +27 -27
- package/dist/index.js +18 -18
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/templates/electron/README.md +1 -1
- package/templates/electron/src/App.vue +2 -2
- package/templates/electron/src/main.ts +4 -4
- package/templates/vanilla/README.md +3 -3
- package/templates/vanilla/index.html +2 -2
- package/templates/vanilla/src/main.ts +7 -7
- package/templates/vite-vue/README.md +2 -2
- package/templates/vite-vue/src/App.vue +2 -2
- package/templates/vite-vue/src/main.ts +6 -6
package/README.md
CHANGED
|
@@ -26,7 +26,16 @@ yarn create noy-db my-app
|
|
|
26
26
|
bun create noy-db my-app
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
The wizard asks at most
|
|
29
|
+
The wizard asks at most 5 questions — project name, adapter, sample data, starter template, and sync adapter — then writes a complete starter into `./my-app/`. Nothing is installed automatically: pick your package manager and run it yourself (the wizard closes by printing `cd <project>`, `pnpm install`, `pnpm dev`).
|
|
30
|
+
|
|
31
|
+
Four starter templates ship in the package:
|
|
32
|
+
|
|
33
|
+
| Template | Stack | Why pick it |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `nuxt-default` | Nuxt 4 + Pinia | SSR + file-based routing — **the default** |
|
|
36
|
+
| `vite-vue` | Vite + Vue 3 + Pinia | Client-side SPA, shorter install graph |
|
|
37
|
+
| `vanilla` | Vite + TypeScript | No framework, smallest footprint |
|
|
38
|
+
| `electron` | Electron + Vue 3 | Desktop app with the `to-file` adapter — USB workflow |
|
|
30
39
|
|
|
31
40
|
Skip the prompts with `--yes`:
|
|
32
41
|
|
|
@@ -136,7 +145,7 @@ All commands that touch real compartments use the **file adapter** and require t
|
|
|
136
145
|
- `--compartment <name>` — the compartment (tenant) name. Required.
|
|
137
146
|
- `--user <id>` — your own user id in the compartment. Required.
|
|
138
147
|
|
|
139
|
-
You'll be prompted for your
|
|
148
|
+
You'll be prompted for your secret at runtime. Secrets are never echoed, never logged, never written to disk, and cleared from process memory when the command exits.
|
|
140
149
|
|
|
141
150
|
### `noy-db add <collection>`
|
|
142
151
|
|
|
@@ -153,7 +162,7 @@ Refuses to overwrite existing files — if either target already exists, the com
|
|
|
153
162
|
|
|
154
163
|
### `noy-db add user <userId> <role> [options]`
|
|
155
164
|
|
|
156
|
-
Grants a new user access to a compartment. Two
|
|
165
|
+
Grants a new user access to a compartment. Two secret prompts: yours (caller), then the new user's (with confirmation).
|
|
157
166
|
|
|
158
167
|
```bash
|
|
159
168
|
pnpm exec noy-db add user accountant-ann operator \
|
|
@@ -201,7 +210,7 @@ pnpm exec noy-db rotate --dir ./data --compartment demo-co --user owner-alice \
|
|
|
201
210
|
|
|
202
211
|
Use cases:
|
|
203
212
|
|
|
204
|
-
- **Suspected key leak**: an operator lost a laptop, a developer accidentally pasted a
|
|
213
|
+
- **Suspected key leak**: an operator lost a laptop, a developer accidentally pasted a secret into a Slack channel, a USB stick went missing. Rotating is cheap insurance.
|
|
205
214
|
- **Scheduled rotation**: some compliance regimes require periodic key rotation regardless of exposure. This command makes rotation scriptable from cron or a CI job.
|
|
206
215
|
|
|
207
216
|
Different from `noydb.revoke({ rotateKeys: true })` in that it doesn't kick anyone out — it's the "just rotate" path.
|
|
@@ -222,7 +231,7 @@ Target paths:
|
|
|
222
231
|
|
|
223
232
|
Parent directories are created on demand, so `./backups/2026/04/demo.json` works even if `./backups/2026/04/` doesn't exist yet.
|
|
224
233
|
|
|
225
|
-
Unsupported schemes (`s3://`, `https://`, etc.) are rejected **before** the
|
|
234
|
+
Unsupported schemes (`s3://`, `https://`, etc.) are rejected **before** the secret prompt so a typo doesn't waste a secret entry.
|
|
226
235
|
|
|
227
236
|
### `noy-db help`
|
|
228
237
|
|
|
@@ -234,11 +243,11 @@ Prints the full usage message for all subcommands.
|
|
|
234
243
|
|
|
235
244
|
Every command that touches real compartments follows these rules:
|
|
236
245
|
|
|
237
|
-
1. **
|
|
238
|
-
2. **
|
|
246
|
+
1. **Secret via `@clack/prompts` `password()`** — never echoes to the terminal, never logged.
|
|
247
|
+
2. **Secret never leaves the local closure** — no file writes, no error messages, no telemetry.
|
|
239
248
|
3. **Ctrl-C at the prompt aborts before any I/O happens** — cancelling doesn't leave the system in a half-mutated state.
|
|
240
249
|
4. **`finally { db.close() }`** — KEK is cleared from process memory on exit, success or failure.
|
|
241
|
-
5. **Unsupported backup schemes are rejected before the prompt** — a typo doesn't waste a
|
|
250
|
+
5. **Unsupported backup schemes are rejected before the prompt** — a typo doesn't waste a secret entry.
|
|
242
251
|
|
|
243
252
|
---
|
|
244
253
|
|
|
@@ -269,7 +278,6 @@ Everything stored is encrypted with AES-256-GCM before it touches the adapter. T
|
|
|
269
278
|
These are explicit non-goals:
|
|
270
279
|
|
|
271
280
|
- **Thai i18n** of the wizard prompts ([#36](https://github.com/vLannaAi/noy-db/issues/36))
|
|
272
|
-
- **Non-Nuxt templates** — no Vite/Vue standalone, no Electron, no vanilla ([#39](https://github.com/vLannaAi/noy-db/issues/39))
|
|
273
281
|
- **`noy-db seed`** — needs a design decision about how seed scripts authenticate
|
|
274
282
|
- **S3 backup targets** — would bundle `@aws-sdk` into this package and break the zero-runtime-deps story; lives in a companion package instead
|
|
275
283
|
- **`noy-db restore <file>`** — paired with the existing `compartment.load()` + integrity check; deferred so it can be designed alongside future identity/session work
|
package/dist/bin/noy-db.js
CHANGED
|
@@ -106,11 +106,11 @@ async function verifyIntegrity() {
|
|
|
106
106
|
const db = await createNoydb({
|
|
107
107
|
store: memory(),
|
|
108
108
|
user: "noy-db-verify",
|
|
109
|
-
// The
|
|
109
|
+
// The secret here is throwaway — the in-memory adapter never
|
|
110
110
|
// persists anything, and the KEK is destroyed when we call close()
|
|
111
111
|
// a few lines down. We use a non-trivial value just to exercise
|
|
112
112
|
// PBKDF2 properly.
|
|
113
|
-
secret: "noy-db-verify-
|
|
113
|
+
secret: "noy-db-verify-secret-2026"
|
|
114
114
|
});
|
|
115
115
|
const company = await db.openVault("verify-co");
|
|
116
116
|
const collection = company.collection("verify");
|
|
@@ -150,13 +150,13 @@ import { jsonFile } from "@noy-db/to-file";
|
|
|
150
150
|
// src/commands/shared.ts
|
|
151
151
|
import { password, isCancel, cancel } from "@clack/prompts";
|
|
152
152
|
var VALID_ROLES = ["owner", "admin", "operator", "viewer", "client"];
|
|
153
|
-
var
|
|
153
|
+
var defaultReadSecret = async (label) => {
|
|
154
154
|
const value = await password({
|
|
155
155
|
message: label,
|
|
156
156
|
// Basic sanity: reject empty strings up front. We don't enforce
|
|
157
157
|
// length here because the caller's KEK-derivation step will
|
|
158
|
-
// reject weak
|
|
159
|
-
validate: (v) => v.length === 0 ? "
|
|
158
|
+
// reject weak secrets with its own, richer error.
|
|
159
|
+
validate: (v) => v.length === 0 ? "Secret cannot be empty" : void 0
|
|
160
160
|
});
|
|
161
161
|
if (isCancel(value)) {
|
|
162
162
|
cancel("Cancelled.");
|
|
@@ -180,10 +180,10 @@ function parseCollectionList(input) {
|
|
|
180
180
|
|
|
181
181
|
// src/commands/rotate.ts
|
|
182
182
|
async function rotate(options) {
|
|
183
|
-
const
|
|
183
|
+
const readSecret = options.readSecret ?? defaultReadSecret;
|
|
184
184
|
const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }));
|
|
185
185
|
const createDb = options.createDb ?? createNoydb2;
|
|
186
|
-
const secret = await
|
|
186
|
+
const secret = await readSecret(`Secret for ${options.user}`);
|
|
187
187
|
let db = null;
|
|
188
188
|
try {
|
|
189
189
|
db = await createDb({
|
|
@@ -212,7 +212,7 @@ import { withTeam as withTeam2 } from "@noy-db/hub/team";
|
|
|
212
212
|
import { createNoydb as createNoydb3 } from "@noy-db/hub";
|
|
213
213
|
import { jsonFile as jsonFile2 } from "@noy-db/to-file";
|
|
214
214
|
async function addUser(options) {
|
|
215
|
-
const
|
|
215
|
+
const readSecret = options.readSecret ?? defaultReadSecret;
|
|
216
216
|
const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile2({ dir }));
|
|
217
217
|
const createDb = options.createDb ?? createNoydb3;
|
|
218
218
|
if ((options.role === "operator" || options.role === "client") && (!options.permissions || Object.keys(options.permissions).length === 0)) {
|
|
@@ -220,17 +220,17 @@ async function addUser(options) {
|
|
|
220
220
|
`Role "${options.role}" requires explicit --collections \u2014 e.g. --collections invoices:rw,clients:ro`
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
|
-
const callerSecret = await
|
|
224
|
-
`Your
|
|
223
|
+
const callerSecret = await readSecret(
|
|
224
|
+
`Your secret (${options.callerUser})`
|
|
225
225
|
);
|
|
226
|
-
const newSecret = await
|
|
227
|
-
`New
|
|
226
|
+
const newSecret = await readSecret(
|
|
227
|
+
`New secret for ${options.newUserId}`
|
|
228
228
|
);
|
|
229
|
-
const confirmSecret = await
|
|
230
|
-
`Confirm
|
|
229
|
+
const confirmSecret = await readSecret(
|
|
230
|
+
`Confirm secret for ${options.newUserId}`
|
|
231
231
|
);
|
|
232
232
|
if (newSecret !== confirmSecret) {
|
|
233
|
-
throw new Error(`
|
|
233
|
+
throw new Error(`Secrets do not match \u2014 grant aborted.`);
|
|
234
234
|
}
|
|
235
235
|
let db = null;
|
|
236
236
|
try {
|
|
@@ -246,7 +246,7 @@ async function addUser(options) {
|
|
|
246
246
|
userId: options.newUserId,
|
|
247
247
|
displayName: options.newUserDisplayName ?? options.newUserId,
|
|
248
248
|
role: options.role,
|
|
249
|
-
|
|
249
|
+
secret: newSecret,
|
|
250
250
|
...options.permissions ? { permissions: options.permissions } : {}
|
|
251
251
|
};
|
|
252
252
|
await db.grant(options.vault, grantOpts);
|
|
@@ -277,11 +277,11 @@ function resolveBackupTarget(target, cwd = process.cwd()) {
|
|
|
277
277
|
return path2.resolve(cwd, raw);
|
|
278
278
|
}
|
|
279
279
|
async function backup(options) {
|
|
280
|
-
const
|
|
280
|
+
const readSecret = options.readSecret ?? defaultReadSecret;
|
|
281
281
|
const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile3({ dir }));
|
|
282
282
|
const createDb = options.createDb ?? createNoydb4;
|
|
283
283
|
const absolutePath = resolveBackupTarget(options.target);
|
|
284
|
-
const secret = await
|
|
284
|
+
const secret = await readSecret(`Secret for ${options.user}`);
|
|
285
285
|
let db = null;
|
|
286
286
|
try {
|
|
287
287
|
db = await createDb({
|
package/dist/bin/noy-db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/bin/noy-db.ts","../../src/commands/add.ts","../../src/commands/verify.ts","../../src/commands/rotate.ts","../../src/commands/shared.ts","../../src/commands/add-user.ts","../../src/commands/backup.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `noy-db` bin. Invoked from inside an existing project by\n * `pnpm exec noy-db <command>`, `npx noy-db <command>`, etc.\n *\n * Subcommands:\n *\n * add <collection> Scaffold store + page files\n * add user <id> <role> [--collections…] Grant a new user access\n * verify In-memory integrity check\n * rotate --vault … --user … Rotate DEKs for a vault\n * backup <target> --vault … … Dump a vault to a file\n * help Show usage\n *\n * Future subcommands (deferred to a follow-up):\n * seed Re-run the project's seeder script (needs\n * a design decision on how seed scripts auth)\n * backup s3://... S3 targets (would bundle @aws-sdk; lives in\n * an optional companion package instead)\n * restore <file> Load a dumped backup into a vault\n *\n * ## Dispatcher design\n *\n * The dispatcher is intentionally dead simple: read argv[2], pick\n * a handler, pass the remaining args. No flag DSL, no auto-help\n * generation. Each subcommand's argv parser lives next to the\n * subcommand itself so the wiring is obvious and the pure\n * subcommand functions stay testable in isolation.\n *\n * ## Passphrase handling invariants\n *\n * Every subcommand that unlocks a vault goes through the\n * shared `defaultReadPassphrase` helper (see `commands/shared.ts`).\n * That helper:\n *\n * - Uses @clack/prompts `password()` so nothing echoes to stdout\n * - Never logs the returned value\n * - Validates \"not empty\" up front; the real strength check\n * happens inside the core's KEK derivation\n * - Aborts the process on Ctrl-C before any I/O happens\n *\n * The passphrase never leaves the closure in which it was read —\n * every subcommand closes the Noydb instance in a `finally` block\n * so the KEK is cleared from memory on the way out.\n */\n\nimport pc from 'picocolors'\nimport { addCollection } from '../commands/add.js'\nimport { verifyIntegrity } from '../commands/verify.js'\nimport { rotate, type RotateOptions } from '../commands/rotate.js'\nimport { addUser, type AddUserOptions } from '../commands/add-user.js'\nimport { backup } from '../commands/backup.js'\nimport { assertRole, parseCollectionList } from '../commands/shared.js'\n\nconst HELP = `Usage: noy-db <command> [args]\n\nCommands:\n add <collection> Scaffold a new collection store + page\n add user <userId> <role> [options] Grant a new user access to a vault\n verify In-memory crypto integrity check\n rotate [options] Rotate DEKs for a vault\n backup <target> [options] Dump a vault to a file\n help Show this message\n\nCommon options for rotate / add user / backup:\n --dir <path> Data directory (file adapter). Default: ./data\n --vault <name> Vault (tenant) name. Required.\n --user <id> Your user id in the vault. Required.\n --collections <list> Comma-separated collection list. Format:\n rotate: invoices,clients\n add user: invoices:rw,clients:ro (operator/client only)\n\nExamples:\n noy-db add invoices\n noy-db add user accountant-ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw,clients:ro\n noy-db verify\n noy-db rotate --dir ./data --vault demo-co --user owner-alice\n noy-db backup ./backups/demo-co-2026-04-07.json --dir ./data --vault demo-co --user owner-alice\n\nRun from the root of a project that already has a noy-db file\nadapter directory in place. For new projects, use\n\\`npm create noy-db\\` instead.\n`\n\nasync function main(): Promise<void> {\n const [, , command, ...rest] = process.argv\n\n switch (command) {\n case undefined:\n case 'help':\n case '-h':\n case '--help':\n process.stdout.write(HELP)\n return\n\n case 'add':\n await runAdd(rest)\n return\n\n case 'verify':\n await runVerify()\n return\n\n case 'rotate':\n await runRotate(rest)\n return\n\n case 'backup':\n await runBackup(rest)\n return\n\n default:\n process.stderr.write(`${pc.red('error:')} unknown command '${command}'\\n\\n${HELP}`)\n process.exit(2)\n }\n}\n\n// ─── `add` dispatcher — branches between `add <collection>` and `add user …` ─\n\nasync function runAdd(args: string[]): Promise<void> {\n const first = args[0]\n if (first === 'user') {\n await runAddUser(args.slice(1))\n return\n }\n // Legacy path: `noy-db add <collection>` scaffolds store + page files.\n if (!first) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db add\\` requires either a collection name or the \\`user\\` subcommand\\n\\n` +\n `Examples:\\n noy-db add invoices\\n noy-db add user alice operator ...\\n`,\n )\n process.exit(2)\n }\n try {\n const result = await addCollection({ name: first })\n process.stdout.write(`${pc.green('✔')} Created:\\n`)\n for (const file of result.files) {\n process.stdout.write(` ${pc.dim('→')} ${file}\\n`)\n }\n process.stdout.write(\n `\\nNext: visit ${pc.cyan(`/${first}`)} in your dev server, then edit ${pc.bold(`app/stores/${first}.ts`)} to match your domain.\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `verify` ──────────────────────────────────────────────────────────\n\nasync function runVerify(): Promise<void> {\n process.stdout.write(`${pc.dim('▸ Running noy-db integrity check…')}\\n`)\n const result = await verifyIntegrity()\n if (result.ok) {\n process.stdout.write(`${pc.green('✔')} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}\\n`)\n return\n }\n process.stderr.write(`${pc.red('✘')} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}\\n`)\n process.exit(1)\n}\n\n// ─── `add user` ────────────────────────────────────────────────────────\n\nasync function runAddUser(args: string[]): Promise<void> {\n // Positional: userId, role. Then flag bag.\n const userId = args[0]\n const roleInput = args[1]\n if (!userId || !roleInput) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db add user\\` requires <userId> <role>\\n\\n` +\n `Example: noy-db add user ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw\\n`,\n )\n process.exit(2)\n }\n\n let role\n try {\n role = assertRole(roleInput)\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(2)\n return\n }\n\n const flags = parseFlags(args.slice(2))\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const callerUser = requireFlag(flags, 'user')\n\n const permissions = parsePermissions(flags['collections'])\n\n const opts: AddUserOptions = {\n dir,\n vault,\n callerUser,\n newUserId: userId,\n role,\n }\n if (flags['display-name']) opts.newUserDisplayName = flags['display-name']\n if (permissions) opts.permissions = permissions\n\n try {\n const result = await addUser(opts)\n process.stdout.write(\n `${pc.green('✔')} Granted ${pc.bold(result.role)} access to ${pc.cyan(result.userId)} in vault ${pc.cyan(vault)}.\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `rotate` ──────────────────────────────────────────────────────────\n\nasync function runRotate(args: string[]): Promise<void> {\n const flags = parseFlags(args)\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const user = requireFlag(flags, 'user')\n\n const opts: RotateOptions = { dir, vault, user }\n const collections = parseCollectionList(flags['collections'])\n if (collections) opts.collections = collections\n\n try {\n const result = await rotate(opts)\n process.stdout.write(\n `${pc.green('✔')} Rotated ${pc.bold(String(result.rotated.length))} collection(s) in ${pc.cyan(vault)}:\\n`,\n )\n for (const name of result.rotated) {\n process.stdout.write(` ${pc.dim('→')} ${name}\\n`)\n }\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `backup` ──────────────────────────────────────────────────────────\n\nasync function runBackup(args: string[]): Promise<void> {\n const target = args[0]\n if (!target) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db backup\\` requires a target path\\n\\n` +\n `Example: noy-db backup ./backups/demo.json --dir ./data --vault demo-co --user owner-alice\\n`,\n )\n process.exit(2)\n }\n\n const flags = parseFlags(args.slice(1))\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const user = requireFlag(flags, 'user')\n\n try {\n const result = await backup({ dir, vault, user, target })\n process.stdout.write(\n `${pc.green('✔')} Wrote backup: ${pc.cyan(result.path)} ${pc.dim(`(${result.bytes} bytes)`)}\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── Shared argv helpers ───────────────────────────────────────────────\n\n/**\n * Parse a sequence of `--key value` flag pairs into a record.\n * Boolean flags (`--flag` with no value) become `\"true\"`. Unknown\n * shapes (positional args after the known set) are ignored — the\n * caller has already peeled positionals before passing in.\n */\nfunction parseFlags(args: string[]): Record<string, string> {\n const out: Record<string, string> = {}\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]\n if (!arg || !arg.startsWith('--')) continue\n const key = arg.slice(2)\n const next = args[i + 1]\n if (next && !next.startsWith('--')) {\n out[key] = next\n i++\n } else {\n out[key] = 'true'\n }\n }\n return out\n}\n\nfunction requireFlag(flags: Record<string, string>, name: string): string {\n const value = flags[name]\n if (!value) {\n process.stderr.write(\n `${pc.red('error:')} missing required flag --${name}\\n`,\n )\n process.exit(2)\n }\n return value\n}\n\n/**\n * Parse `--collections invoices:rw,clients:ro` into a\n * `{ invoices: 'rw', clients: 'ro' }` record. Returns `null` when\n * the input is empty or undefined.\n */\nfunction parsePermissions(\n input: string | undefined,\n): Record<string, 'rw' | 'ro'> | null {\n if (!input) return null\n const out: Record<string, 'rw' | 'ro'> = {}\n for (const pair of input.split(',').map((s) => s.trim()).filter(Boolean)) {\n const [name, mode] = pair.split(':')\n if (!name || !mode || (mode !== 'rw' && mode !== 'ro')) {\n throw new Error(\n `Invalid --collections entry \"${pair}\" — expected \"name:rw\" or \"name:ro\"`,\n )\n }\n out[name] = mode\n }\n return Object.keys(out).length > 0 ? out : null\n}\n\nmain().catch((err: unknown) => {\n process.stderr.write(`${pc.red('fatal:')} ${(err as Error).message}\\n`)\n process.exit(1)\n})\n","/**\n * `noy-db add <collection>` — scaffold a new collection inside an existing\n * Nuxt 4 project that already has `@noy-db/in-nuxt` configured.\n *\n * The command writes two files:\n *\n * 1. `app/stores/<collection>.ts` — a `defineNoydbStore<T>()` call with\n * a placeholder `T` interface and one example field. The user fills\n * in the real shape after the file is created.\n *\n * 2. `app/pages/<collection>.vue` — a minimal CRUD page that lists,\n * adds, and deletes records. The store ID and collection name are\n * derived from the argument; everything else is boilerplate.\n *\n * The command refuses to overwrite existing files. If either target\n * already exists it logs which one and exits non-zero — the user has to\n * delete or move the file first. There's no `--force` because forcing an\n * overwrite of generated UI code is almost always a footgun in disguise.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface AddCollectionOptions {\n /** The collection name. Must be a lowercase identifier. */\n name: string\n /** Project root. Defaults to `process.cwd()`. */\n cwd?: string\n /** Compartment id to embed in the generated store. Defaults to `default`. */\n vault?: string\n}\n\n/**\n * Result returned to callers (the bin entry uses this to format output;\n * tests assert on the file paths).\n */\nexport interface AddCollectionResult {\n /** Files written, in the order they were created. */\n files: string[]\n}\n\n/**\n * Lowercase identifier check, narrower than the project-name check —\n * collection names become Vue component names, Pinia store IDs, AND TS\n * symbols, so we keep them simple: letters, digits, hyphens, must start\n * with a letter.\n */\nexport function validateCollectionName(name: string): string | null {\n if (!name) return 'Collection name is required'\n if (!/^[a-z][a-z0-9-]*$/.test(name)) {\n return 'Collection name must start with a lowercase letter and contain only lowercase letters, digits, or hyphens'\n }\n return null\n}\n\nexport async function addCollection(\n options: AddCollectionOptions,\n): Promise<AddCollectionResult> {\n const err = validateCollectionName(options.name)\n if (err) throw new Error(err)\n\n const cwd = options.cwd ?? process.cwd()\n const vault = options.vault ?? 'default'\n const name = options.name\n\n // Convert kebab-case to PascalCase for the TS interface name and the\n // Vue helper variable.\n const PascalName = name\n .split('-')\n .map((s) => (s.length === 0 ? s : (s[0]?.toUpperCase() ?? '') + s.slice(1)))\n .join('')\n const useFnName = `use${PascalName}`\n\n const storePath = path.join(cwd, 'app', 'stores', `${name}.ts`)\n const pagePath = path.join(cwd, 'app', 'pages', `${name}.vue`)\n\n // Refuse to overwrite. Doing both checks before writing either file\n // means a partial write is impossible — either both files land or\n // neither does.\n for (const target of [storePath, pagePath]) {\n if (await pathExists(target)) {\n throw new Error(`Refusing to overwrite existing file: ${target}`)\n }\n }\n\n await fs.mkdir(path.dirname(storePath), { recursive: true })\n await fs.mkdir(path.dirname(pagePath), { recursive: true })\n\n await fs.writeFile(storePath, renderStore(name, PascalName, useFnName, vault), 'utf8')\n await fs.writeFile(pagePath, renderPage(name, PascalName, useFnName), 'utf8')\n\n return { files: [storePath, pagePath] }\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Renders the `app/stores/<name>.ts` file. The interface is intentionally\n * minimal — `id` and `name` are the only fields. The user is expected to\n * extend it after generation; we'd rather not lock them into a domain\n * shape we guessed.\n */\nfunction renderStore(\n name: string,\n PascalName: string,\n useFnName: string,\n vault: string,\n): string {\n return `// Generated by \\`noy-db add ${name}\\`.\n//\n// Edit the ${PascalName} interface to match your domain, then call\n// \\`${useFnName}()\\` from any component.\n//\n// defineNoydbStore is auto-imported by @noy-db/in-nuxt. The vault\n// id is the tenant/company namespace — change it if you have multiple.\n\nexport interface ${PascalName} {\n id: string\n name: string\n}\n\nexport const ${useFnName} = defineNoydbStore<${PascalName}>('${name}', {\n vault: '${vault}',\n})\n`\n}\n\n/**\n * Renders the `app/pages/<name>.vue` page — list + add + delete. Picks\n * the simplest possible template-renderer pattern (`v-for`, no fancy\n * components) so the generated file is easy to read and modify.\n */\nfunction renderPage(name: string, PascalName: string, useFnName: string): string {\n return `<!--\n Generated by \\`noy-db add ${name}\\`.\n Visit /${name} in your dev server.\n-->\n<script setup lang=\"ts\">\nconst ${name} = ${useFnName}()\nawait ${name}.$ready\n\nfunction addOne() {\n ${name}.add({\n id: crypto.randomUUID(),\n name: 'New ${PascalName}',\n })\n}\n\nfunction removeOne(id: string) {\n ${name}.remove(id)\n}\n</script>\n\n<template>\n <main>\n <h1>${PascalName}</h1>\n <button @click=\"addOne\">Add ${PascalName}</button>\n <ul>\n <li v-for=\"item in ${name}.items\" :key=\"item.id\">\n {{ item.name }}\n <button @click=\"removeOne(item.id)\">Delete</button>\n </li>\n </ul>\n </main>\n</template>\n`\n}\n","/**\n * `noy-db verify` — end-to-end integrity check.\n *\n * Opens an in-memory NOYDB instance, writes a record, reads it back,\n * decrypts it, and asserts the round-trip is byte-identical. The check\n * exercises the full crypto path (PBKDF2 → KEK → DEK → AES-GCM) without\n * touching any user data on disk.\n *\n * Why an in-memory check is the right scope:\n * - It validates that @noy-db/core, @noy-db/memory, and the user's\n * installed Node version all agree on Web Crypto. That's the most\n * common silent failure for first-time installers.\n * - It cannot accidentally corrupt user data because there isn't any.\n * - It runs in well under one second, so users actually run it.\n *\n * What this command does NOT do (intentionally):\n * - Open the user's actual vault file/dynamo/s3/browser store.\n * That requires the user's passphrase — not something we want a CLI\n * `verify` command to prompt for. The full passphrase-driven verify\n * belongs in `nuxi noydb verify` once the auth story for CLIs lands\n * in. For now `noy-db verify` is the dependency-graph smoke test.\n */\n\nimport { createNoydb } from '@noy-db/hub'\nimport { memory } from '@noy-db/to-memory'\n\nexport interface VerifyResult {\n /** `true` if the round-trip succeeded; `false` if anything diverged. */\n ok: boolean\n /** Human-readable status. Always set, even on success. */\n message: string\n /** Wall-clock time the integrity check took, in ms. */\n durationMs: number\n}\n\n/**\n * Runs the end-to-end check. Pure function — no console output, no\n * `process.exit`. The bin wrapper handles formatting and exit codes so\n * the function is trivial to call from tests.\n */\nexport async function verifyIntegrity(): Promise<VerifyResult> {\n const start = performance.now()\n try {\n const db = await createNoydb({\n store: memory(),\n user: 'noy-db-verify',\n // The passphrase here is throwaway — the in-memory adapter never\n // persists anything, and the KEK is destroyed when we call close()\n // a few lines down. We use a non-trivial value just to exercise\n // PBKDF2 properly.\n secret: 'noy-db-verify-passphrase-2026',\n })\n const company = await db.openVault('verify-co')\n const collection = company.collection<{ id: string; n: number }>('verify')\n\n // Round-trip a single record. We pick a value that's small enough\n // to print on failure but large enough to ensure encryption isn't\n // accidentally a no-op.\n const original = { id: 'verify-1', n: 42 }\n await collection.put('verify-1', original)\n const got = await collection.get('verify-1')\n if (!got || got.id !== original.id || got.n !== original.n) {\n return fail(start, `Round-trip mismatch: got ${JSON.stringify(got)}`)\n }\n\n // Make sure the query DSL works too — this catches the case where\n // the user's @noy-db/core install is at (no query DSL) but the\n // CLI was updated to.\n const found = collection.query().where('n', '==', 42).toArray()\n if (found.length !== 1) {\n return fail(start, `Query DSL mismatch: expected 1 result, got ${found.length}`)\n }\n\n db.close()\n\n return {\n ok: true,\n message: 'noy-db integrity check passed',\n durationMs: Math.round(performance.now() - start),\n }\n } catch (err) {\n return fail(start, `Integrity check threw: ${(err as Error).message}`)\n }\n}\n\nfunction fail(start: number, message: string): VerifyResult {\n return {\n ok: false,\n message,\n durationMs: Math.round(performance.now() - start),\n }\n}\n","/**\n * `noy-db rotate` — rotate the DEKs for one or more collections in\n * a vault.\n *\n * What it does\n * ------------\n * For each target collection:\n *\n * 1. Generate a fresh DEK\n * 2. Decrypt every record with the old DEK\n * 3. Re-encrypt every record with the new DEK\n * 4. Re-wrap the new DEK into every remaining user's keyring\n *\n * The old DEKs become unreachable as soon as the keyring files are\n * updated. This is the \"just rotate\" path — nobody is revoked,\n * everybody keeps their current permissions, but the key material\n * is replaced.\n *\n * Why expose this as a CLI command\n * --------------------------------\n * Two real-world scenarios:\n *\n * 1. **Suspected key leak.** An operator lost a laptop, a\n * developer accidentally pasted a passphrase into a Slack\n * channel, a USB stick went missing. Even if you think the\n * passphrase is safe, rotating is cheap insurance.\n *\n * 2. **Scheduled rotation.** Some compliance regimes require\n * periodic key rotation regardless of exposure. A CLI makes\n * this scriptable from cron or a CI job.\n *\n * This module is test-first: all inputs are plain options, the\n * passphrase reader is injected, and the Noydb factory is\n * injectable. The production bin is a thin wrapper that defaults\n * those injections to their real implementations.\n */\n\nimport { withTeam } from '@noy-db/hub/team'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface RotateOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to rotate keys in. */\n vault: string\n /** The user id of the operator running the rotate. */\n user: string\n /**\n * Explicit list of collections to rotate. When undefined, the\n * rotation targets every collection the user has a DEK for —\n * resolved at run time by reading the vault snapshot.\n */\n collections?: string[]\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /**\n * Injected Noydb factory. Production code leaves this undefined\n * and gets `createNoydb`; tests pass a constructor that builds\n * against an in-memory adapter.\n */\n createDb?: typeof createNoydb\n /**\n * Injected adapter factory. Production code leaves this undefined\n * and gets `jsonFile`; tests pass one that returns the shared\n * in-memory adapter their fixture used.\n */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface RotateResult {\n /** The collections that were actually rotated. */\n rotated: string[]\n}\n\n/**\n * Run the rotate flow against a file-adapter vault. Returns\n * the list of collections that were rotated so callers can display\n * it to the user.\n *\n * Throws `Error` on any auth/adapter/rotate failure. The bin\n * catches these and prints a friendly message; direct callers\n * (tests) can inspect the error message to assert specific\n * failure modes.\n */\nexport async function rotate(options: RotateOptions): Promise<RotateResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Read the passphrase BEFORE opening the database. This way a\n // cancelled prompt (Ctrl-C at the password entry) leaves the\n // adapter completely untouched — no files opened, no locks held.\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Key rotation is the opt-in team capability (#267).\n teamStrategy: withTeam(),\n })\n\n // Resolve \"all collections\" by asking the vault. This\n // happens BEFORE rotate() is called, so the list is stable\n // across the operation — adding a new collection mid-rotate\n // would be a race we're not guarding against (single-writer\n // assumption applies).\n const vault = await db.openVault(options.vault)\n const targets = options.collections && options.collections.length > 0\n ? options.collections\n : await vault.collections()\n\n if (targets.length === 0) {\n throw new Error(\n `Vault \"${options.vault}\" has no collections to rotate.`,\n )\n }\n\n await db.rotate(options.vault, targets)\n return { rotated: targets }\n } finally {\n // Always close the DB on exit — success or failure. Close()\n // clears the KEK and DEKs from process memory, which is the\n // final line of defense if the passphrase somehow leaked\n // into a log line above this block.\n db?.close()\n }\n}\n","/**\n * Shared primitives for the interactive `noy-db` subcommands that\n * need to unlock a real vault.\n *\n * Three things live here:\n *\n * 1. `ReadPassphrase` — a tiny interface for \"prompt the user for\n * a passphrase\", with a test-friendly default. Subcommands take\n * this as an injected dependency so tests can short-circuit\n * the prompt without spawning a pty.\n *\n * 2. `defaultReadPassphrase` — the production implementation,\n * built on `@clack/prompts` `password()`. Never echoes the\n * value to the terminal, never logs it, clears it from the\n * returned promise after the caller consumes it.\n *\n * 3. `assertRole` — narrow unknown string input to the Role type\n * with a consistent error message.\n *\n * ## Why pull this out\n *\n * `rotate`, `addUser`, and `backup` all need the same \"prompt for\n * a passphrase\" shape and the same \"open a file adapter and get\n * back a Noydb instance\" shape. Duplicating it in three files would\n * drift over time; centralizing means one place to audit the\n * passphrase-handling contract (never log, never persist, clear\n * local variables after use).\n */\n\nimport { password, isCancel, cancel } from '@clack/prompts'\nimport type { Role } from '@noy-db/hub'\n\nconst VALID_ROLES = ['owner', 'admin', 'operator', 'viewer', 'client'] as const\n\n/**\n * Asynchronous passphrase reader. Production code passes\n * `defaultReadPassphrase`; tests pass a stub that returns a fixed\n * string without touching stdin.\n *\n * The `label` is shown to the user as the prompt message. It\n * should never contain the expected passphrase or any secret.\n */\nexport type ReadPassphrase = (label: string) => Promise<string>\n\n/**\n * Clack-based passphrase prompt. Cancellation (Ctrl-C) aborts the\n * process with exit code 1 — prompts are always the first thing to\n * fire in a subcommand, so aborting here doesn't leave the system\n * in a half-mutated state.\n */\nexport const defaultReadPassphrase: ReadPassphrase = async (label) => {\n const value = await password({\n message: label,\n // Basic sanity: reject empty strings up front. We don't enforce\n // length here because the caller's KEK-derivation step will\n // reject weak passphrases with its own, richer error.\n validate: (v) => (v.length === 0 ? 'Passphrase cannot be empty' : undefined),\n })\n if (isCancel(value)) {\n cancel('Cancelled.')\n process.exit(1)\n }\n return value\n}\n\n/**\n * Narrow an unknown string to the `Role` type from @noy-db/core.\n * Used by the `add user` subcommand to validate the role argument\n * before passing it to `noydb.grant()`.\n */\nexport function assertRole(input: string): Role {\n if (!(VALID_ROLES as readonly string[]).includes(input)) {\n throw new Error(\n `Invalid role \"${input}\" — must be one of: ${VALID_ROLES.join(', ')}`,\n )\n }\n return input as Role\n}\n\n/**\n * Split a comma-separated collection list into an array of names,\n * trimming whitespace and dropping empties. Returns null if the\n * input itself is empty or undefined — the caller decides whether\n * that means \"all collections\" or \"error\".\n */\nexport function parseCollectionList(input: string | undefined): string[] | null {\n if (!input) return null\n const parts = input\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n return parts.length > 0 ? parts : null\n}\n","/**\n * `noy-db add user <id> <role>` — grant a new user access to a\n * vault.\n *\n * What it does\n * ------------\n * Wraps `noydb.grant()` in the CLI's auth-prompt ritual:\n *\n * 1. Prompt the caller for their own passphrase (to unlock the\n * caller's keyring and derive the wrapping key).\n * 2. Prompt for the new user's passphrase.\n * 3. Prompt for confirmation of the new passphrase.\n * 4. Reject on mismatch.\n * 5. Call `noydb.grant(vault, { userId, role, passphrase, permissions })`.\n *\n * For owner/admin/viewer roles, every collection is granted\n * automatically (the core keyring.ts grant logic handles that via\n * the `permissions` field). For operator/client, the caller must\n * pass a `--collections` list because those roles need explicit\n * per-collection permissions.\n *\n * ## What this does NOT do\n *\n * - No email/invite flow — is about local-CLI key management,\n * not out-of-band user enrollment.\n * - No rollback on partial failure — `grant()` is atomic at the\n * core level (keyring file writes last, after DEK wrapping), so\n * partial-state-on-crash is already handled.\n */\n\nimport { withTeam } from '@noy-db/hub/team'\nimport { createNoydb, type Noydb, type NoydbStore, type Role } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface AddUserOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to grant access to. */\n vault: string\n /** The user id of the caller running the grant. */\n callerUser: string\n /** The new user's id (must not already exist in the vault keyring). */\n newUserId: string\n /** The new user's display name — shown in UI and audit logs. Defaults to `newUserId`. */\n newUserDisplayName?: string\n /** The new user's role. */\n role: Role\n /**\n * Per-collection permissions. Required when `role` is operator or\n * client; ignored for owner/admin/viewer (they get everything\n * via the core's resolvePermissions logic).\n *\n * Shape: `{ invoices: 'rw', clients: 'ro' }`. CLI callers pass\n * `--collections invoices:rw,clients:ro` and the argv parser\n * converts it to this shape.\n */\n permissions?: Record<string, 'rw' | 'ro'>\n /** Injected passphrase reader. Defaults to the clack implementation. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface AddUserResult {\n /** The userId that was granted access. */\n userId: string\n /** The role they were granted. */\n role: Role\n}\n\n/**\n * Run the grant flow. Two passphrase prompts: caller's, then new\n * user's (twice for confirmation). Calls `noydb.grant()` with the\n * collected values.\n */\nexport async function addUser(options: AddUserOptions): Promise<AddUserResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Operator/client roles NEED explicit permissions. Reject here\n // rather than in the middle of the grant, so the caller sees the\n // problem before any I/O happens.\n if (\n (options.role === 'operator' || options.role === 'client') &&\n (!options.permissions || Object.keys(options.permissions).length === 0)\n ) {\n throw new Error(\n `Role \"${options.role}\" requires explicit --collections — e.g. --collections invoices:rw,clients:ro`,\n )\n }\n\n const callerSecret = await readPassphrase(\n `Your passphrase (${options.callerUser})`,\n )\n const newSecret = await readPassphrase(\n `New passphrase for ${options.newUserId}`,\n )\n const confirmSecret = await readPassphrase(\n `Confirm passphrase for ${options.newUserId}`,\n )\n\n if (newSecret !== confirmSecret) {\n throw new Error(`Passphrases do not match — grant aborted.`)\n }\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.callerUser,\n secret: callerSecret,\n // Multi-user grant is the opt-in team capability (#267) — the CLI\n // add-user command is by definition a multi-user operation.\n teamStrategy: withTeam(),\n })\n\n // Build the grant options. Only include `permissions` when the\n // caller actually supplied them — otherwise the core's\n // resolvePermissions fills in the role defaults. The spread\n // (rather than post-assignment) keeps the object literal\n // compatible with `GrantOptions`'s readonly `permissions`.\n const grantOpts: Parameters<Noydb['grant']>[1] = {\n userId: options.newUserId,\n displayName: options.newUserDisplayName ?? options.newUserId,\n role: options.role,\n passphrase: newSecret,\n ...(options.permissions ? { permissions: options.permissions } : {}),\n }\n\n await db.grant(options.vault, grantOpts)\n\n return {\n userId: options.newUserId,\n role: options.role,\n }\n } finally {\n db?.close()\n }\n}\n","/**\n * `noy-db backup <target>` — dump a vault to a local file.\n *\n * What it does\n * ------------\n * Wraps `vault.dump()` in the CLI's auth-prompt ritual, then\n * writes the serialized backup to the requested path. As of,\n * `dump()` already produces a verifiable backup (embedded\n * ledgerHead, full `_ledger` / `_ledger_deltas` snapshots) — the\n * CLI just moves bytes; the integrity guarantees come from core.\n *\n * ## Target URI support\n *\n * ships **`file://` only** (or a plain filesystem path).\n * The issue spec originally called for `s3://` as well, but\n * wiring @aws-sdk into create-noy-db would defeat the\n * zero-runtime-deps story for the CLI package. S3 backup is\n * deferred to a follow-up that can live in @noy-db/s3-cli or a\n * similar optional companion package.\n *\n * Accepted forms:\n * - `file:///absolute/path.json`\n * - `file://./relative/path.json`\n * - `/absolute/path.json` (treated as `file://`)\n * - `./relative/path.json` (treated as `file://`)\n *\n * ## What this does NOT do\n *\n * - No encryption of the backup BEYOND what noy-db already does.\n * The dumped file is a valid noy-db backup, which means\n * individual records are still encrypted but the keyring is\n * included (wrapped with each user's KEK). Anyone who loads\n * the backup still needs the correct passphrase to read.\n * - No restore — that's a separate subcommand tracked as a\n * follow-up. For now users can restore via\n * `vault.load(backupString)` from their own app code.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { withHistory } from '@noy-db/hub/history'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadPassphrase } from './shared.js'\nimport { defaultReadPassphrase } from './shared.js'\n\nexport interface BackupOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to back up. */\n vault: string\n /** The user id of the operator running the backup. */\n user: string\n /**\n * Where to write the backup. Accepts a `file://` URI or a plain\n * filesystem path. Relative paths resolve against `process.cwd()`.\n */\n target: string\n /** Injected passphrase reader. */\n readPassphrase?: ReadPassphrase\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface BackupResult {\n /** Absolute filesystem path the backup was written to. */\n path: string\n /** Size of the serialized backup in bytes. */\n bytes: number\n}\n\n/**\n * Parse a backup target into an absolute filesystem path. Rejects\n * unsupported URI schemes (s3://, https://, etc.) early so the\n * caller doesn't silently write to the wrong place.\n */\nexport function resolveBackupTarget(target: string, cwd: string = process.cwd()): string {\n // Strip the `file://` prefix if present. The rest of the string\n // is treated as a filesystem path. We accept both `file:///abs`\n // (three slashes, absolute) and `file://./rel` (two slashes,\n // relative) because real-world users write both.\n let raw = target\n if (target.startsWith('file://')) {\n raw = target.slice('file://'.length)\n } else if (target.includes('://')) {\n // Any other scheme is unsupported.\n throw new Error(\n `Unsupported backup target scheme: \"${target.split('://')[0]}://\". ` +\n `Only file:// and plain filesystem paths are supported in. ` +\n `S3 backups will land in a follow-up.`,\n )\n }\n return path.resolve(cwd, raw)\n}\n\nexport async function backup(options: BackupOptions): Promise<BackupResult> {\n const readPassphrase = options.readPassphrase ?? defaultReadPassphrase\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Resolve the target FIRST so a bad URI fails before any\n // passphrase is collected. This keeps the UX clean: a typo in\n // `s3://bucket/x` rejects without asking for a secret the user\n // would then have to type again.\n const absolutePath = resolveBackupTarget(options.target)\n\n const secret = await readPassphrase(`Passphrase for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Opt into the history strategy so `vault.dump()` embeds\n // `ledgerHead` for tamper-evident verification at restore.\n // Source vaults that were never written with history simply\n // have an empty ledger; the embedded head is omitted in that\n // case and the backup is treated as legacy on load.\n historyStrategy: withHistory(),\n })\n const vault = await db.openVault(options.vault)\n const serialized = await vault.dump()\n\n // Make sure the parent directory exists. If the user passed\n // `./backups/2026/demo.json` and `./backups/2026` doesn't\n // exist yet, we create it. This is the common case for\n // scripted rotations dropping into a date-based folder.\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, serialized, 'utf8')\n\n return {\n path: absolutePath,\n bytes: Buffer.byteLength(serialized, 'utf8'),\n }\n } finally {\n db?.close()\n }\n}\n"],"mappings":"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","names":["createNoydb","createNoydb","withTeam","createNoydb","jsonFile","jsonFile","createNoydb","withTeam","fs","path","createNoydb","jsonFile","path","jsonFile","createNoydb","fs"]}
|
|
1
|
+
{"version":3,"sources":["../../src/bin/noy-db.ts","../../src/commands/add.ts","../../src/commands/verify.ts","../../src/commands/rotate.ts","../../src/commands/shared.ts","../../src/commands/add-user.ts","../../src/commands/backup.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `noy-db` bin. Invoked from inside an existing project by\n * `pnpm exec noy-db <command>`, `npx noy-db <command>`, etc.\n *\n * Subcommands:\n *\n * add <collection> Scaffold store + page files\n * add user <id> <role> [--collections…] Grant a new user access\n * verify In-memory integrity check\n * rotate --vault … --user … Rotate DEKs for a vault\n * backup <target> --vault … … Dump a vault to a file\n * help Show usage\n *\n * Future subcommands (deferred to a follow-up):\n * seed Re-run the project's seeder script (needs\n * a design decision on how seed scripts auth)\n * backup s3://... S3 targets (would bundle @aws-sdk; lives in\n * an optional companion package instead)\n * restore <file> Load a dumped backup into a vault\n *\n * ## Dispatcher design\n *\n * The dispatcher is intentionally dead simple: read argv[2], pick\n * a handler, pass the remaining args. No flag DSL, no auto-help\n * generation. Each subcommand's argv parser lives next to the\n * subcommand itself so the wiring is obvious and the pure\n * subcommand functions stay testable in isolation.\n *\n * ## Secret handling invariants\n *\n * Every subcommand that unlocks a vault goes through the\n * shared `defaultReadSecret` helper (see `commands/shared.ts`).\n * That helper:\n *\n * - Uses @clack/prompts `password()` so nothing echoes to stdout\n * - Never logs the returned value\n * - Validates \"not empty\" up front; the real strength check\n * happens inside the core's KEK derivation\n * - Aborts the process on Ctrl-C before any I/O happens\n *\n * The secret never leaves the closure in which it was read —\n * every subcommand closes the Noydb instance in a `finally` block\n * so the KEK is cleared from memory on the way out.\n */\n\nimport pc from 'picocolors'\nimport { addCollection } from '../commands/add.js'\nimport { verifyIntegrity } from '../commands/verify.js'\nimport { rotate, type RotateOptions } from '../commands/rotate.js'\nimport { addUser, type AddUserOptions } from '../commands/add-user.js'\nimport { backup } from '../commands/backup.js'\nimport { assertRole, parseCollectionList } from '../commands/shared.js'\n\nconst HELP = `Usage: noy-db <command> [args]\n\nCommands:\n add <collection> Scaffold a new collection store + page\n add user <userId> <role> [options] Grant a new user access to a vault\n verify In-memory crypto integrity check\n rotate [options] Rotate DEKs for a vault\n backup <target> [options] Dump a vault to a file\n help Show this message\n\nCommon options for rotate / add user / backup:\n --dir <path> Data directory (file adapter). Default: ./data\n --vault <name> Vault (tenant) name. Required.\n --user <id> Your user id in the vault. Required.\n --collections <list> Comma-separated collection list. Format:\n rotate: invoices,clients\n add user: invoices:rw,clients:ro (operator/client only)\n\nExamples:\n noy-db add invoices\n noy-db add user accountant-ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw,clients:ro\n noy-db verify\n noy-db rotate --dir ./data --vault demo-co --user owner-alice\n noy-db backup ./backups/demo-co-2026-04-07.json --dir ./data --vault demo-co --user owner-alice\n\nRun from the root of a project that already has a noy-db file\nadapter directory in place. For new projects, use\n\\`npm create noy-db\\` instead.\n`\n\nasync function main(): Promise<void> {\n const [, , command, ...rest] = process.argv\n\n switch (command) {\n case undefined:\n case 'help':\n case '-h':\n case '--help':\n process.stdout.write(HELP)\n return\n\n case 'add':\n await runAdd(rest)\n return\n\n case 'verify':\n await runVerify()\n return\n\n case 'rotate':\n await runRotate(rest)\n return\n\n case 'backup':\n await runBackup(rest)\n return\n\n default:\n process.stderr.write(`${pc.red('error:')} unknown command '${command}'\\n\\n${HELP}`)\n process.exit(2)\n }\n}\n\n// ─── `add` dispatcher — branches between `add <collection>` and `add user …` ─\n\nasync function runAdd(args: string[]): Promise<void> {\n const first = args[0]\n if (first === 'user') {\n await runAddUser(args.slice(1))\n return\n }\n // Legacy path: `noy-db add <collection>` scaffolds store + page files.\n if (!first) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db add\\` requires either a collection name or the \\`user\\` subcommand\\n\\n` +\n `Examples:\\n noy-db add invoices\\n noy-db add user alice operator ...\\n`,\n )\n process.exit(2)\n }\n try {\n const result = await addCollection({ name: first })\n process.stdout.write(`${pc.green('✔')} Created:\\n`)\n for (const file of result.files) {\n process.stdout.write(` ${pc.dim('→')} ${file}\\n`)\n }\n process.stdout.write(\n `\\nNext: visit ${pc.cyan(`/${first}`)} in your dev server, then edit ${pc.bold(`app/stores/${first}.ts`)} to match your domain.\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `verify` ──────────────────────────────────────────────────────────\n\nasync function runVerify(): Promise<void> {\n process.stdout.write(`${pc.dim('▸ Running noy-db integrity check…')}\\n`)\n const result = await verifyIntegrity()\n if (result.ok) {\n process.stdout.write(`${pc.green('✔')} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}\\n`)\n return\n }\n process.stderr.write(`${pc.red('✘')} ${result.message} ${pc.dim(`(${result.durationMs}ms)`)}\\n`)\n process.exit(1)\n}\n\n// ─── `add user` ────────────────────────────────────────────────────────\n\nasync function runAddUser(args: string[]): Promise<void> {\n // Positional: userId, role. Then flag bag.\n const userId = args[0]\n const roleInput = args[1]\n if (!userId || !roleInput) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db add user\\` requires <userId> <role>\\n\\n` +\n `Example: noy-db add user ann operator --dir ./data --vault demo-co --user owner-alice --collections invoices:rw\\n`,\n )\n process.exit(2)\n }\n\n let role\n try {\n role = assertRole(roleInput)\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(2)\n return\n }\n\n const flags = parseFlags(args.slice(2))\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const callerUser = requireFlag(flags, 'user')\n\n const permissions = parsePermissions(flags['collections'])\n\n const opts: AddUserOptions = {\n dir,\n vault,\n callerUser,\n newUserId: userId,\n role,\n }\n if (flags['display-name']) opts.newUserDisplayName = flags['display-name']\n if (permissions) opts.permissions = permissions\n\n try {\n const result = await addUser(opts)\n process.stdout.write(\n `${pc.green('✔')} Granted ${pc.bold(result.role)} access to ${pc.cyan(result.userId)} in vault ${pc.cyan(vault)}.\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `rotate` ──────────────────────────────────────────────────────────\n\nasync function runRotate(args: string[]): Promise<void> {\n const flags = parseFlags(args)\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const user = requireFlag(flags, 'user')\n\n const opts: RotateOptions = { dir, vault, user }\n const collections = parseCollectionList(flags['collections'])\n if (collections) opts.collections = collections\n\n try {\n const result = await rotate(opts)\n process.stdout.write(\n `${pc.green('✔')} Rotated ${pc.bold(String(result.rotated.length))} collection(s) in ${pc.cyan(vault)}:\\n`,\n )\n for (const name of result.rotated) {\n process.stdout.write(` ${pc.dim('→')} ${name}\\n`)\n }\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── `backup` ──────────────────────────────────────────────────────────\n\nasync function runBackup(args: string[]): Promise<void> {\n const target = args[0]\n if (!target) {\n process.stderr.write(\n `${pc.red('error:')} \\`noy-db backup\\` requires a target path\\n\\n` +\n `Example: noy-db backup ./backups/demo.json --dir ./data --vault demo-co --user owner-alice\\n`,\n )\n process.exit(2)\n }\n\n const flags = parseFlags(args.slice(1))\n const dir = flags.dir ?? './data'\n const vault = requireFlag(flags, 'vault')\n const user = requireFlag(flags, 'user')\n\n try {\n const result = await backup({ dir, vault, user, target })\n process.stdout.write(\n `${pc.green('✔')} Wrote backup: ${pc.cyan(result.path)} ${pc.dim(`(${result.bytes} bytes)`)}\\n`,\n )\n } catch (err) {\n process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\\n`)\n process.exit(1)\n }\n}\n\n// ─── Shared argv helpers ───────────────────────────────────────────────\n\n/**\n * Parse a sequence of `--key value` flag pairs into a record.\n * Boolean flags (`--flag` with no value) become `\"true\"`. Unknown\n * shapes (positional args after the known set) are ignored — the\n * caller has already peeled positionals before passing in.\n */\nfunction parseFlags(args: string[]): Record<string, string> {\n const out: Record<string, string> = {}\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]\n if (!arg || !arg.startsWith('--')) continue\n const key = arg.slice(2)\n const next = args[i + 1]\n if (next && !next.startsWith('--')) {\n out[key] = next\n i++\n } else {\n out[key] = 'true'\n }\n }\n return out\n}\n\nfunction requireFlag(flags: Record<string, string>, name: string): string {\n const value = flags[name]\n if (!value) {\n process.stderr.write(\n `${pc.red('error:')} missing required flag --${name}\\n`,\n )\n process.exit(2)\n }\n return value\n}\n\n/**\n * Parse `--collections invoices:rw,clients:ro` into a\n * `{ invoices: 'rw', clients: 'ro' }` record. Returns `null` when\n * the input is empty or undefined.\n */\nfunction parsePermissions(\n input: string | undefined,\n): Record<string, 'rw' | 'ro'> | null {\n if (!input) return null\n const out: Record<string, 'rw' | 'ro'> = {}\n for (const pair of input.split(',').map((s) => s.trim()).filter(Boolean)) {\n const [name, mode] = pair.split(':')\n if (!name || !mode || (mode !== 'rw' && mode !== 'ro')) {\n throw new Error(\n `Invalid --collections entry \"${pair}\" — expected \"name:rw\" or \"name:ro\"`,\n )\n }\n out[name] = mode\n }\n return Object.keys(out).length > 0 ? out : null\n}\n\nmain().catch((err: unknown) => {\n process.stderr.write(`${pc.red('fatal:')} ${(err as Error).message}\\n`)\n process.exit(1)\n})\n","/**\n * `noy-db add <collection>` — scaffold a new collection inside an existing\n * Nuxt 4 project that already has `@noy-db/in-nuxt` configured.\n *\n * The command writes two files:\n *\n * 1. `app/stores/<collection>.ts` — a `defineNoydbStore<T>()` call with\n * a placeholder `T` interface and one example field. The user fills\n * in the real shape after the file is created.\n *\n * 2. `app/pages/<collection>.vue` — a minimal CRUD page that lists,\n * adds, and deletes records. The store ID and collection name are\n * derived from the argument; everything else is boilerplate.\n *\n * The command refuses to overwrite existing files. If either target\n * already exists it logs which one and exits non-zero — the user has to\n * delete or move the file first. There's no `--force` because forcing an\n * overwrite of generated UI code is almost always a footgun in disguise.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface AddCollectionOptions {\n /** The collection name. Must be a lowercase identifier. */\n name: string\n /** Project root. Defaults to `process.cwd()`. */\n cwd?: string\n /** Compartment id to embed in the generated store. Defaults to `default`. */\n vault?: string\n}\n\n/**\n * Result returned to callers (the bin entry uses this to format output;\n * tests assert on the file paths).\n */\nexport interface AddCollectionResult {\n /** Files written, in the order they were created. */\n files: string[]\n}\n\n/**\n * Lowercase identifier check, narrower than the project-name check —\n * collection names become Vue component names, Pinia store IDs, AND TS\n * symbols, so we keep them simple: letters, digits, hyphens, must start\n * with a letter.\n */\nexport function validateCollectionName(name: string): string | null {\n if (!name) return 'Collection name is required'\n if (!/^[a-z][a-z0-9-]*$/.test(name)) {\n return 'Collection name must start with a lowercase letter and contain only lowercase letters, digits, or hyphens'\n }\n return null\n}\n\nexport async function addCollection(\n options: AddCollectionOptions,\n): Promise<AddCollectionResult> {\n const err = validateCollectionName(options.name)\n if (err) throw new Error(err)\n\n const cwd = options.cwd ?? process.cwd()\n const vault = options.vault ?? 'default'\n const name = options.name\n\n // Convert kebab-case to PascalCase for the TS interface name and the\n // Vue helper variable.\n const PascalName = name\n .split('-')\n .map((s) => (s.length === 0 ? s : (s[0]?.toUpperCase() ?? '') + s.slice(1)))\n .join('')\n const useFnName = `use${PascalName}`\n\n const storePath = path.join(cwd, 'app', 'stores', `${name}.ts`)\n const pagePath = path.join(cwd, 'app', 'pages', `${name}.vue`)\n\n // Refuse to overwrite. Doing both checks before writing either file\n // means a partial write is impossible — either both files land or\n // neither does.\n for (const target of [storePath, pagePath]) {\n if (await pathExists(target)) {\n throw new Error(`Refusing to overwrite existing file: ${target}`)\n }\n }\n\n await fs.mkdir(path.dirname(storePath), { recursive: true })\n await fs.mkdir(path.dirname(pagePath), { recursive: true })\n\n await fs.writeFile(storePath, renderStore(name, PascalName, useFnName, vault), 'utf8')\n await fs.writeFile(pagePath, renderPage(name, PascalName, useFnName), 'utf8')\n\n return { files: [storePath, pagePath] }\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Renders the `app/stores/<name>.ts` file. The interface is intentionally\n * minimal — `id` and `name` are the only fields. The user is expected to\n * extend it after generation; we'd rather not lock them into a domain\n * shape we guessed.\n */\nfunction renderStore(\n name: string,\n PascalName: string,\n useFnName: string,\n vault: string,\n): string {\n return `// Generated by \\`noy-db add ${name}\\`.\n//\n// Edit the ${PascalName} interface to match your domain, then call\n// \\`${useFnName}()\\` from any component.\n//\n// defineNoydbStore is auto-imported by @noy-db/in-nuxt. The vault\n// id is the tenant/company namespace — change it if you have multiple.\n\nexport interface ${PascalName} {\n id: string\n name: string\n}\n\nexport const ${useFnName} = defineNoydbStore<${PascalName}>('${name}', {\n vault: '${vault}',\n})\n`\n}\n\n/**\n * Renders the `app/pages/<name>.vue` page — list + add + delete. Picks\n * the simplest possible template-renderer pattern (`v-for`, no fancy\n * components) so the generated file is easy to read and modify.\n */\nfunction renderPage(name: string, PascalName: string, useFnName: string): string {\n return `<!--\n Generated by \\`noy-db add ${name}\\`.\n Visit /${name} in your dev server.\n-->\n<script setup lang=\"ts\">\nconst ${name} = ${useFnName}()\nawait ${name}.$ready\n\nfunction addOne() {\n ${name}.add({\n id: crypto.randomUUID(),\n name: 'New ${PascalName}',\n })\n}\n\nfunction removeOne(id: string) {\n ${name}.remove(id)\n}\n</script>\n\n<template>\n <main>\n <h1>${PascalName}</h1>\n <button @click=\"addOne\">Add ${PascalName}</button>\n <ul>\n <li v-for=\"item in ${name}.items\" :key=\"item.id\">\n {{ item.name }}\n <button @click=\"removeOne(item.id)\">Delete</button>\n </li>\n </ul>\n </main>\n</template>\n`\n}\n","/**\n * `noy-db verify` — end-to-end integrity check.\n *\n * Opens an in-memory NOYDB instance, writes a record, reads it back,\n * decrypts it, and asserts the round-trip is byte-identical. The check\n * exercises the full crypto path (PBKDF2 → KEK → DEK → AES-GCM) without\n * touching any user data on disk.\n *\n * Why an in-memory check is the right scope:\n * - It validates that @noy-db/core, @noy-db/memory, and the user's\n * installed Node version all agree on Web Crypto. That's the most\n * common silent failure for first-time installers.\n * - It cannot accidentally corrupt user data because there isn't any.\n * - It runs in well under one second, so users actually run it.\n *\n * What this command does NOT do (intentionally):\n * - Open the user's actual vault file/dynamo/s3/browser store.\n * That requires the user's secret — not something we want a CLI\n * `verify` command to prompt for. The full secret-driven verify\n * belongs in `nuxi noydb verify` once the auth story for CLIs lands\n * in. For now `noy-db verify` is the dependency-graph smoke test.\n */\n\nimport { createNoydb } from '@noy-db/hub'\nimport { memory } from '@noy-db/to-memory'\n\nexport interface VerifyResult {\n /** `true` if the round-trip succeeded; `false` if anything diverged. */\n ok: boolean\n /** Human-readable status. Always set, even on success. */\n message: string\n /** Wall-clock time the integrity check took, in ms. */\n durationMs: number\n}\n\n/**\n * Runs the end-to-end check. Pure function — no console output, no\n * `process.exit`. The bin wrapper handles formatting and exit codes so\n * the function is trivial to call from tests.\n */\nexport async function verifyIntegrity(): Promise<VerifyResult> {\n const start = performance.now()\n try {\n const db = await createNoydb({\n store: memory(),\n user: 'noy-db-verify',\n // The secret here is throwaway — the in-memory adapter never\n // persists anything, and the KEK is destroyed when we call close()\n // a few lines down. We use a non-trivial value just to exercise\n // PBKDF2 properly.\n secret: 'noy-db-verify-secret-2026',\n })\n const company = await db.openVault('verify-co')\n const collection = company.collection<{ id: string; n: number }>('verify')\n\n // Round-trip a single record. We pick a value that's small enough\n // to print on failure but large enough to ensure encryption isn't\n // accidentally a no-op.\n const original = { id: 'verify-1', n: 42 }\n await collection.put('verify-1', original)\n const got = await collection.get('verify-1')\n if (!got || got.id !== original.id || got.n !== original.n) {\n return fail(start, `Round-trip mismatch: got ${JSON.stringify(got)}`)\n }\n\n // Make sure the query DSL works too — this catches the case where\n // the user's @noy-db/core install is at (no query DSL) but the\n // CLI was updated to.\n const found = collection.query().where('n', '==', 42).toArray()\n if (found.length !== 1) {\n return fail(start, `Query DSL mismatch: expected 1 result, got ${found.length}`)\n }\n\n db.close()\n\n return {\n ok: true,\n message: 'noy-db integrity check passed',\n durationMs: Math.round(performance.now() - start),\n }\n } catch (err) {\n return fail(start, `Integrity check threw: ${(err as Error).message}`)\n }\n}\n\nfunction fail(start: number, message: string): VerifyResult {\n return {\n ok: false,\n message,\n durationMs: Math.round(performance.now() - start),\n }\n}\n","/**\n * `noy-db rotate` — rotate the DEKs for one or more collections in\n * a vault.\n *\n * What it does\n * ------------\n * For each target collection:\n *\n * 1. Generate a fresh DEK\n * 2. Decrypt every record with the old DEK\n * 3. Re-encrypt every record with the new DEK\n * 4. Re-wrap the new DEK into every remaining user's keyring\n *\n * The old DEKs become unreachable as soon as the keyring files are\n * updated. This is the \"just rotate\" path — nobody is revoked,\n * everybody keeps their current permissions, but the key material\n * is replaced.\n *\n * Why expose this as a CLI command\n * --------------------------------\n * Two real-world scenarios:\n *\n * 1. **Suspected key leak.** An operator lost a laptop, a\n * developer accidentally pasted a secret into a Slack\n * channel, a USB stick went missing. Even if you think the\n * secret is safe, rotating is cheap insurance.\n *\n * 2. **Scheduled rotation.** Some compliance regimes require\n * periodic key rotation regardless of exposure. A CLI makes\n * this scriptable from cron or a CI job.\n *\n * This module is test-first: all inputs are plain options, the\n * secret reader is injected, and the Noydb factory is\n * injectable. The production bin is a thin wrapper that defaults\n * those injections to their real implementations.\n */\n\nimport { withTeam } from '@noy-db/hub/team'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadSecret } from './shared.js'\nimport { defaultReadSecret } from './shared.js'\n\nexport interface RotateOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to rotate keys in. */\n vault: string\n /** The user id of the operator running the rotate. */\n user: string\n /**\n * Explicit list of collections to rotate. When undefined, the\n * rotation targets every collection the user has a DEK for —\n * resolved at run time by reading the vault snapshot.\n */\n collections?: string[]\n /** Injected secret reader. Defaults to the clack implementation. */\n readSecret?: ReadSecret\n /**\n * Injected Noydb factory. Production code leaves this undefined\n * and gets `createNoydb`; tests pass a constructor that builds\n * against an in-memory adapter.\n */\n createDb?: typeof createNoydb\n /**\n * Injected adapter factory. Production code leaves this undefined\n * and gets `jsonFile`; tests pass one that returns the shared\n * in-memory adapter their fixture used.\n */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface RotateResult {\n /** The collections that were actually rotated. */\n rotated: string[]\n}\n\n/**\n * Run the rotate flow against a file-adapter vault. Returns\n * the list of collections that were rotated so callers can display\n * it to the user.\n *\n * Throws `Error` on any auth/adapter/rotate failure. The bin\n * catches these and prints a friendly message; direct callers\n * (tests) can inspect the error message to assert specific\n * failure modes.\n */\nexport async function rotate(options: RotateOptions): Promise<RotateResult> {\n const readSecret = options.readSecret ?? defaultReadSecret\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Read the secret BEFORE opening the database. This way a\n // cancelled prompt (Ctrl-C at the password entry) leaves the\n // adapter completely untouched — no files opened, no locks held.\n const secret = await readSecret(`Secret for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Key rotation is the opt-in team capability (#267).\n teamStrategy: withTeam(),\n })\n\n // Resolve \"all collections\" by asking the vault. This\n // happens BEFORE rotate() is called, so the list is stable\n // across the operation — adding a new collection mid-rotate\n // would be a race we're not guarding against (single-writer\n // assumption applies).\n const vault = await db.openVault(options.vault)\n const targets = options.collections && options.collections.length > 0\n ? options.collections\n : await vault.collections()\n\n if (targets.length === 0) {\n throw new Error(\n `Vault \"${options.vault}\" has no collections to rotate.`,\n )\n }\n\n await db.rotate(options.vault, targets)\n return { rotated: targets }\n } finally {\n // Always close the DB on exit — success or failure. Close()\n // clears the KEK and DEKs from process memory, which is the\n // final line of defense if the secret somehow leaked\n // into a log line above this block.\n db?.close()\n }\n}\n","/**\n * Shared primitives for the interactive `noy-db` subcommands that\n * need to unlock a real vault.\n *\n * Three things live here:\n *\n * 1. `ReadSecret` — a tiny interface for \"prompt the user for\n * a secret\", with a test-friendly default. Subcommands take\n * this as an injected dependency so tests can short-circuit\n * the prompt without spawning a pty.\n *\n * 2. `defaultReadSecret` — the production implementation,\n * built on `@clack/prompts` `password()`. Never echoes the\n * value to the terminal, never logs it, clears it from the\n * returned promise after the caller consumes it.\n *\n * 3. `assertRole` — narrow unknown string input to the Role type\n * with a consistent error message.\n *\n * ## Why pull this out\n *\n * `rotate`, `addUser`, and `backup` all need the same \"prompt for\n * a secret\" shape and the same \"open a file adapter and get\n * back a Noydb instance\" shape. Duplicating it in three files would\n * drift over time; centralizing means one place to audit the\n * secret-handling contract (never log, never persist, clear\n * local variables after use).\n */\n\nimport { password, isCancel, cancel } from '@clack/prompts'\nimport type { Role } from '@noy-db/hub'\n\nconst VALID_ROLES = ['owner', 'admin', 'operator', 'viewer', 'client'] as const\n\n/**\n * Asynchronous secret reader. Production code passes\n * `defaultReadSecret`; tests pass a stub that returns a fixed\n * string without touching stdin.\n *\n * The `label` is shown to the user as the prompt message. It\n * should never contain the expected secret or any secret.\n */\nexport type ReadSecret = (label: string) => Promise<string>\n\n/**\n * Clack-based secret prompt. Cancellation (Ctrl-C) aborts the\n * process with exit code 1 — prompts are always the first thing to\n * fire in a subcommand, so aborting here doesn't leave the system\n * in a half-mutated state.\n */\nexport const defaultReadSecret: ReadSecret = async (label) => {\n const value = await password({\n message: label,\n // Basic sanity: reject empty strings up front. We don't enforce\n // length here because the caller's KEK-derivation step will\n // reject weak secrets with its own, richer error.\n validate: (v) => (v.length === 0 ? 'Secret cannot be empty' : undefined),\n })\n if (isCancel(value)) {\n cancel('Cancelled.')\n process.exit(1)\n }\n return value\n}\n\n/**\n * Narrow an unknown string to the `Role` type from @noy-db/core.\n * Used by the `add user` subcommand to validate the role argument\n * before passing it to `noydb.grant()`.\n */\nexport function assertRole(input: string): Role {\n if (!(VALID_ROLES as readonly string[]).includes(input)) {\n throw new Error(\n `Invalid role \"${input}\" — must be one of: ${VALID_ROLES.join(', ')}`,\n )\n }\n return input as Role\n}\n\n/**\n * Split a comma-separated collection list into an array of names,\n * trimming whitespace and dropping empties. Returns null if the\n * input itself is empty or undefined — the caller decides whether\n * that means \"all collections\" or \"error\".\n */\nexport function parseCollectionList(input: string | undefined): string[] | null {\n if (!input) return null\n const parts = input\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n return parts.length > 0 ? parts : null\n}\n","/**\n * `noy-db add user <id> <role>` — grant a new user access to a\n * vault.\n *\n * What it does\n * ------------\n * Wraps `noydb.grant()` in the CLI's auth-prompt ritual:\n *\n * 1. Prompt the caller for their own secret (to unlock the\n * caller's keyring and derive the wrapping key).\n * 2. Prompt for the new user's secret.\n * 3. Prompt for confirmation of the new secret.\n * 4. Reject on mismatch.\n * 5. Call `noydb.grant(vault, { userId, role, secret, permissions })`.\n *\n * For owner/admin/viewer roles, every collection is granted\n * automatically (the core keyring.ts grant logic handles that via\n * the `permissions` field). For operator/client, the caller must\n * pass a `--collections` list because those roles need explicit\n * per-collection permissions.\n *\n * ## What this does NOT do\n *\n * - No email/invite flow — is about local-CLI key management,\n * not out-of-band user enrollment.\n * - No rollback on partial failure — `grant()` is atomic at the\n * core level (keyring file writes last, after DEK wrapping), so\n * partial-state-on-crash is already handled.\n */\n\nimport { withTeam } from '@noy-db/hub/team'\nimport { createNoydb, type Noydb, type NoydbStore, type Role } from '@noy-db/hub'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadSecret } from './shared.js'\nimport { defaultReadSecret } from './shared.js'\n\nexport interface AddUserOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to grant access to. */\n vault: string\n /** The user id of the caller running the grant. */\n callerUser: string\n /** The new user's id (must not already exist in the vault keyring). */\n newUserId: string\n /** The new user's display name — shown in UI and audit logs. Defaults to `newUserId`. */\n newUserDisplayName?: string\n /** The new user's role. */\n role: Role\n /**\n * Per-collection permissions. Required when `role` is operator or\n * client; ignored for owner/admin/viewer (they get everything\n * via the core's resolvePermissions logic).\n *\n * Shape: `{ invoices: 'rw', clients: 'ro' }`. CLI callers pass\n * `--collections invoices:rw,clients:ro` and the argv parser\n * converts it to this shape.\n */\n permissions?: Record<string, 'rw' | 'ro'>\n /** Injected secret reader. Defaults to the clack implementation. */\n readSecret?: ReadSecret\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface AddUserResult {\n /** The userId that was granted access. */\n userId: string\n /** The role they were granted. */\n role: Role\n}\n\n/**\n * Run the grant flow. Two secret prompts: caller's, then new\n * user's (twice for confirmation). Calls `noydb.grant()` with the\n * collected values.\n */\nexport async function addUser(options: AddUserOptions): Promise<AddUserResult> {\n const readSecret = options.readSecret ?? defaultReadSecret\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Operator/client roles NEED explicit permissions. Reject here\n // rather than in the middle of the grant, so the caller sees the\n // problem before any I/O happens.\n if (\n (options.role === 'operator' || options.role === 'client') &&\n (!options.permissions || Object.keys(options.permissions).length === 0)\n ) {\n throw new Error(\n `Role \"${options.role}\" requires explicit --collections — e.g. --collections invoices:rw,clients:ro`,\n )\n }\n\n const callerSecret = await readSecret(\n `Your secret (${options.callerUser})`,\n )\n const newSecret = await readSecret(\n `New secret for ${options.newUserId}`,\n )\n const confirmSecret = await readSecret(\n `Confirm secret for ${options.newUserId}`,\n )\n\n if (newSecret !== confirmSecret) {\n throw new Error(`Secrets do not match — grant aborted.`)\n }\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.callerUser,\n secret: callerSecret,\n // Multi-user grant is the opt-in team capability (#267) — the CLI\n // add-user command is by definition a multi-user operation.\n teamStrategy: withTeam(),\n })\n\n // Build the grant options. Only include `permissions` when the\n // caller actually supplied them — otherwise the core's\n // resolvePermissions fills in the role defaults. The spread\n // (rather than post-assignment) keeps the object literal\n // compatible with `GrantOptions`'s readonly `permissions`.\n const grantOpts: Parameters<Noydb['grant']>[1] = {\n userId: options.newUserId,\n displayName: options.newUserDisplayName ?? options.newUserId,\n role: options.role,\n secret: newSecret,\n ...(options.permissions ? { permissions: options.permissions } : {}),\n }\n\n await db.grant(options.vault, grantOpts)\n\n return {\n userId: options.newUserId,\n role: options.role,\n }\n } finally {\n db?.close()\n }\n}\n","/**\n * `noy-db backup <target>` — dump a vault to a local file.\n *\n * What it does\n * ------------\n * Wraps `vault.dump()` in the CLI's auth-prompt ritual, then\n * writes the serialized backup to the requested path. As of,\n * `dump()` already produces a verifiable backup (embedded\n * ledgerHead, full `_ledger` / `_ledger_deltas` snapshots) — the\n * CLI just moves bytes; the integrity guarantees come from core.\n *\n * ## Target URI support\n *\n * ships **`file://` only** (or a plain filesystem path).\n * The issue spec originally called for `s3://` as well, but\n * wiring @aws-sdk into create-noy-db would defeat the\n * zero-runtime-deps story for the CLI package. S3 backup is\n * deferred to a follow-up that can live in @noy-db/s3-cli or a\n * similar optional companion package.\n *\n * Accepted forms:\n * - `file:///absolute/path.json`\n * - `file://./relative/path.json`\n * - `/absolute/path.json` (treated as `file://`)\n * - `./relative/path.json` (treated as `file://`)\n *\n * ## What this does NOT do\n *\n * - No encryption of the backup BEYOND what noy-db already does.\n * The dumped file is a valid noy-db backup, which means\n * individual records are still encrypted but the keyring is\n * included (wrapped with each user's KEK). Anyone who loads\n * the backup still needs the correct secret to read.\n * - No restore — that's a separate subcommand tracked as a\n * follow-up. For now users can restore via\n * `vault.load(backupString)` from their own app code.\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createNoydb, type Noydb, type NoydbStore } from '@noy-db/hub'\nimport { withHistory } from '@noy-db/hub/history'\nimport { jsonFile } from '@noy-db/to-file'\nimport type { ReadSecret } from './shared.js'\nimport { defaultReadSecret } from './shared.js'\n\nexport interface BackupOptions {\n /** Directory containing the vault data (file adapter only). */\n dir: string\n /** Vault (tenant) name to back up. */\n vault: string\n /** The user id of the operator running the backup. */\n user: string\n /**\n * Where to write the backup. Accepts a `file://` URI or a plain\n * filesystem path. Relative paths resolve against `process.cwd()`.\n */\n target: string\n /** Injected secret reader. */\n readSecret?: ReadSecret\n /** Injected Noydb factory. */\n createDb?: typeof createNoydb\n /** Injected adapter factory. */\n buildAdapter?: (dir: string) => NoydbStore\n}\n\nexport interface BackupResult {\n /** Absolute filesystem path the backup was written to. */\n path: string\n /** Size of the serialized backup in bytes. */\n bytes: number\n}\n\n/**\n * Parse a backup target into an absolute filesystem path. Rejects\n * unsupported URI schemes (s3://, https://, etc.) early so the\n * caller doesn't silently write to the wrong place.\n */\nexport function resolveBackupTarget(target: string, cwd: string = process.cwd()): string {\n // Strip the `file://` prefix if present. The rest of the string\n // is treated as a filesystem path. We accept both `file:///abs`\n // (three slashes, absolute) and `file://./rel` (two slashes,\n // relative) because real-world users write both.\n let raw = target\n if (target.startsWith('file://')) {\n raw = target.slice('file://'.length)\n } else if (target.includes('://')) {\n // Any other scheme is unsupported.\n throw new Error(\n `Unsupported backup target scheme: \"${target.split('://')[0]}://\". ` +\n `Only file:// and plain filesystem paths are supported in. ` +\n `S3 backups will land in a follow-up.`,\n )\n }\n return path.resolve(cwd, raw)\n}\n\nexport async function backup(options: BackupOptions): Promise<BackupResult> {\n const readSecret = options.readSecret ?? defaultReadSecret\n const buildAdapter = options.buildAdapter ?? ((dir) => jsonFile({ dir }))\n const createDb = options.createDb ?? createNoydb\n\n // Resolve the target FIRST so a bad URI fails before any\n // secret is collected. This keeps the UX clean: a typo in\n // `s3://bucket/x` rejects without asking for a secret the user\n // would then have to type again.\n const absolutePath = resolveBackupTarget(options.target)\n\n const secret = await readSecret(`Secret for ${options.user}`)\n\n let db: Noydb | null = null\n try {\n db = await createDb({\n store: buildAdapter(options.dir),\n user: options.user,\n secret,\n // Opt into the history strategy so `vault.dump()` embeds\n // `ledgerHead` for tamper-evident verification at restore.\n // Source vaults that were never written with history simply\n // have an empty ledger; the embedded head is omitted in that\n // case and the backup is treated as legacy on load.\n historyStrategy: withHistory(),\n })\n const vault = await db.openVault(options.vault)\n const serialized = await vault.dump()\n\n // Make sure the parent directory exists. If the user passed\n // `./backups/2026/demo.json` and `./backups/2026` doesn't\n // exist yet, we create it. This is the common case for\n // scripted rotations dropping into a date-based folder.\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, serialized, 'utf8')\n\n return {\n path: absolutePath,\n bytes: Buffer.byteLength(serialized, 'utf8'),\n }\n } finally {\n db?.close()\n }\n}\n"],"mappings":"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","names":["createNoydb","createNoydb","withTeam","createNoydb","jsonFile","jsonFile","createNoydb","withTeam","fs","path","createNoydb","jsonFile","path","jsonFile","createNoydb","fs"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -397,8 +397,8 @@ declare function addCollection(options: AddCollectionOptions): Promise<AddCollec
|
|
|
397
397
|
*
|
|
398
398
|
* What this command does NOT do (intentionally):
|
|
399
399
|
* - Open the user's actual vault file/dynamo/s3/browser store.
|
|
400
|
-
* That requires the user's
|
|
401
|
-
* `verify` command to prompt for. The full
|
|
400
|
+
* That requires the user's secret — not something we want a CLI
|
|
401
|
+
* `verify` command to prompt for. The full secret-driven verify
|
|
402
402
|
* belongs in `nuxi noydb verify` once the auth story for CLIs lands
|
|
403
403
|
* in. For now `noy-db verify` is the dependency-graph smoke test.
|
|
404
404
|
*/
|
|
@@ -423,12 +423,12 @@ declare function verifyIntegrity(): Promise<VerifyResult>;
|
|
|
423
423
|
*
|
|
424
424
|
* Three things live here:
|
|
425
425
|
*
|
|
426
|
-
* 1. `
|
|
427
|
-
* a
|
|
426
|
+
* 1. `ReadSecret` — a tiny interface for "prompt the user for
|
|
427
|
+
* a secret", with a test-friendly default. Subcommands take
|
|
428
428
|
* this as an injected dependency so tests can short-circuit
|
|
429
429
|
* the prompt without spawning a pty.
|
|
430
430
|
*
|
|
431
|
-
* 2. `
|
|
431
|
+
* 2. `defaultReadSecret` — the production implementation,
|
|
432
432
|
* built on `@clack/prompts` `password()`. Never echoes the
|
|
433
433
|
* value to the terminal, never logs it, clears it from the
|
|
434
434
|
* returned promise after the caller consumes it.
|
|
@@ -439,22 +439,22 @@ declare function verifyIntegrity(): Promise<VerifyResult>;
|
|
|
439
439
|
* ## Why pull this out
|
|
440
440
|
*
|
|
441
441
|
* `rotate`, `addUser`, and `backup` all need the same "prompt for
|
|
442
|
-
* a
|
|
442
|
+
* a secret" shape and the same "open a file adapter and get
|
|
443
443
|
* back a Noydb instance" shape. Duplicating it in three files would
|
|
444
444
|
* drift over time; centralizing means one place to audit the
|
|
445
|
-
*
|
|
445
|
+
* secret-handling contract (never log, never persist, clear
|
|
446
446
|
* local variables after use).
|
|
447
447
|
*/
|
|
448
448
|
|
|
449
449
|
/**
|
|
450
|
-
* Asynchronous
|
|
451
|
-
* `
|
|
450
|
+
* Asynchronous secret reader. Production code passes
|
|
451
|
+
* `defaultReadSecret`; tests pass a stub that returns a fixed
|
|
452
452
|
* string without touching stdin.
|
|
453
453
|
*
|
|
454
454
|
* The `label` is shown to the user as the prompt message. It
|
|
455
|
-
* should never contain the expected
|
|
455
|
+
* should never contain the expected secret or any secret.
|
|
456
456
|
*/
|
|
457
|
-
type
|
|
457
|
+
type ReadSecret = (label: string) => Promise<string>;
|
|
458
458
|
/**
|
|
459
459
|
* Narrow an unknown string to the `Role` type from @noy-db/core.
|
|
460
460
|
* Used by the `add user` subcommand to validate the role argument
|
|
@@ -492,16 +492,16 @@ declare function parseCollectionList(input: string | undefined): string[] | null
|
|
|
492
492
|
* Two real-world scenarios:
|
|
493
493
|
*
|
|
494
494
|
* 1. **Suspected key leak.** An operator lost a laptop, a
|
|
495
|
-
* developer accidentally pasted a
|
|
495
|
+
* developer accidentally pasted a secret into a Slack
|
|
496
496
|
* channel, a USB stick went missing. Even if you think the
|
|
497
|
-
*
|
|
497
|
+
* secret is safe, rotating is cheap insurance.
|
|
498
498
|
*
|
|
499
499
|
* 2. **Scheduled rotation.** Some compliance regimes require
|
|
500
500
|
* periodic key rotation regardless of exposure. A CLI makes
|
|
501
501
|
* this scriptable from cron or a CI job.
|
|
502
502
|
*
|
|
503
503
|
* This module is test-first: all inputs are plain options, the
|
|
504
|
-
*
|
|
504
|
+
* secret reader is injected, and the Noydb factory is
|
|
505
505
|
* injectable. The production bin is a thin wrapper that defaults
|
|
506
506
|
* those injections to their real implementations.
|
|
507
507
|
*/
|
|
@@ -519,8 +519,8 @@ interface RotateOptions {
|
|
|
519
519
|
* resolved at run time by reading the vault snapshot.
|
|
520
520
|
*/
|
|
521
521
|
collections?: string[];
|
|
522
|
-
/** Injected
|
|
523
|
-
|
|
522
|
+
/** Injected secret reader. Defaults to the clack implementation. */
|
|
523
|
+
readSecret?: ReadSecret;
|
|
524
524
|
/**
|
|
525
525
|
* Injected Noydb factory. Production code leaves this undefined
|
|
526
526
|
* and gets `createNoydb`; tests pass a constructor that builds
|
|
@@ -558,12 +558,12 @@ declare function rotate(options: RotateOptions): Promise<RotateResult>;
|
|
|
558
558
|
* ------------
|
|
559
559
|
* Wraps `noydb.grant()` in the CLI's auth-prompt ritual:
|
|
560
560
|
*
|
|
561
|
-
* 1. Prompt the caller for their own
|
|
561
|
+
* 1. Prompt the caller for their own secret (to unlock the
|
|
562
562
|
* caller's keyring and derive the wrapping key).
|
|
563
|
-
* 2. Prompt for the new user's
|
|
564
|
-
* 3. Prompt for confirmation of the new
|
|
563
|
+
* 2. Prompt for the new user's secret.
|
|
564
|
+
* 3. Prompt for confirmation of the new secret.
|
|
565
565
|
* 4. Reject on mismatch.
|
|
566
|
-
* 5. Call `noydb.grant(vault, { userId, role,
|
|
566
|
+
* 5. Call `noydb.grant(vault, { userId, role, secret, permissions })`.
|
|
567
567
|
*
|
|
568
568
|
* For owner/admin/viewer roles, every collection is granted
|
|
569
569
|
* automatically (the core keyring.ts grant logic handles that via
|
|
@@ -603,8 +603,8 @@ interface AddUserOptions {
|
|
|
603
603
|
* converts it to this shape.
|
|
604
604
|
*/
|
|
605
605
|
permissions?: Record<string, 'rw' | 'ro'>;
|
|
606
|
-
/** Injected
|
|
607
|
-
|
|
606
|
+
/** Injected secret reader. Defaults to the clack implementation. */
|
|
607
|
+
readSecret?: ReadSecret;
|
|
608
608
|
/** Injected Noydb factory. */
|
|
609
609
|
createDb?: typeof createNoydb;
|
|
610
610
|
/** Injected adapter factory. */
|
|
@@ -617,7 +617,7 @@ interface AddUserResult {
|
|
|
617
617
|
role: Role;
|
|
618
618
|
}
|
|
619
619
|
/**
|
|
620
|
-
* Run the grant flow. Two
|
|
620
|
+
* Run the grant flow. Two secret prompts: caller's, then new
|
|
621
621
|
* user's (twice for confirmation). Calls `noydb.grant()` with the
|
|
622
622
|
* collected values.
|
|
623
623
|
*/
|
|
@@ -655,7 +655,7 @@ declare function addUser(options: AddUserOptions): Promise<AddUserResult>;
|
|
|
655
655
|
* The dumped file is a valid noy-db backup, which means
|
|
656
656
|
* individual records are still encrypted but the keyring is
|
|
657
657
|
* included (wrapped with each user's KEK). Anyone who loads
|
|
658
|
-
* the backup still needs the correct
|
|
658
|
+
* the backup still needs the correct secret to read.
|
|
659
659
|
* - No restore — that's a separate subcommand tracked as a
|
|
660
660
|
* follow-up. For now users can restore via
|
|
661
661
|
* `vault.load(backupString)` from their own app code.
|
|
@@ -673,8 +673,8 @@ interface BackupOptions {
|
|
|
673
673
|
* filesystem path. Relative paths resolve against `process.cwd()`.
|
|
674
674
|
*/
|
|
675
675
|
target: string;
|
|
676
|
-
/** Injected
|
|
677
|
-
|
|
676
|
+
/** Injected secret reader. */
|
|
677
|
+
readSecret?: ReadSecret;
|
|
678
678
|
/** Injected Noydb factory. */
|
|
679
679
|
createDb?: typeof createNoydb;
|
|
680
680
|
/** Injected adapter factory. */
|
|
@@ -694,4 +694,4 @@ interface BackupResult {
|
|
|
694
694
|
declare function resolveBackupTarget(target: string, cwd?: string): string;
|
|
695
695
|
declare function backup(options: BackupOptions): Promise<BackupResult>;
|
|
696
696
|
|
|
697
|
-
export { type AddCollectionOptions, type AddUserOptions, type AddUserResult, type BackupOptions, type BackupResult, type Locale, type
|
|
697
|
+
export { type AddCollectionOptions, type AddUserOptions, type AddUserResult, type BackupOptions, type BackupResult, type Locale, type ReadSecret, type RotateOptions, type RotateResult, SUPPORTED_LOCALES, type VerifyResult, type WizardMessages, type WizardOptions, type WizardResult, addCollection, addUser, assertRole, backup, detectLocale, loadMessages, parseCollectionList, parseLocaleFlag, resolveBackupTarget, rotate, runWizard, verifyIntegrity };
|