@noy-db/create 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/bin/create.d.ts +1 -0
- package/dist/bin/create.js +724 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/bin/noy-db.d.ts +1 -0
- package/dist/bin/noy-db.js +548 -0
- package/dist/bin/noy-db.js.map +1 -0
- package/dist/index.d.ts +665 -0
- package/dist/index.js +902 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
- package/templates/nuxt-default/README.md +38 -0
- package/templates/nuxt-default/_gitignore +32 -0
- package/templates/nuxt-default/app/app.vue +37 -0
- package/templates/nuxt-default/app/pages/index.vue +21 -0
- package/templates/nuxt-default/app/pages/invoices.vue +62 -0
- package/templates/nuxt-default/app/stores/invoices.ts +23 -0
- package/templates/nuxt-default/nuxt.config.ts +30 -0
- package/templates/nuxt-default/package.json +28 -0
- package/templates/nuxt-default/tsconfig.json +3 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { Role, createNoydb, NoydbAdapter } from '@noy-db/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internationalization types for the `@noy-db/create` wizard.
|
|
5
|
+
*
|
|
6
|
+
* `WizardMessages` is the full set of user-facing strings the
|
|
7
|
+
* wizard emits: prompt labels, note titles, note bodies, outro
|
|
8
|
+
* messages, confirmation questions. Every locale bundle exports
|
|
9
|
+
* a `WizardMessages` constant; the key-parity test ensures they
|
|
10
|
+
* all have the exact same set of keys so we never ship a locale
|
|
11
|
+
* that's missing a string.
|
|
12
|
+
*
|
|
13
|
+
* ## What's translated, what's not
|
|
14
|
+
*
|
|
15
|
+
* **Translated:** prompts, note titles, confirmation messages,
|
|
16
|
+
* success banners, and the short summaries shown before each
|
|
17
|
+
* major step. These are the load-bearing "does the user
|
|
18
|
+
* understand what's happening?" strings.
|
|
19
|
+
*
|
|
20
|
+
* **Not translated:** validation error messages ("Project name
|
|
21
|
+
* cannot be empty"), diagnostic output, stack traces, structured
|
|
22
|
+
* errors from `@noy-db/core`. These stay in English so bug
|
|
23
|
+
* reports from any locale look the same in an issue tracker.
|
|
24
|
+
* Thai developers filing bugs with English error messages can
|
|
25
|
+
* get help from English-speaking maintainers; the reverse is
|
|
26
|
+
* harder.
|
|
27
|
+
*
|
|
28
|
+
* ## Why a flat shape instead of nested namespaces
|
|
29
|
+
*
|
|
30
|
+
* Flat keys are the simplest thing that can work. With ~30
|
|
31
|
+
* strings, namespacing (e.g., `prompts.projectName`) would just
|
|
32
|
+
* add ceremony without helping discoverability. If the set grows
|
|
33
|
+
* past ~100 strings we can revisit.
|
|
34
|
+
*/
|
|
35
|
+
type Locale = 'en' | 'th';
|
|
36
|
+
interface WizardMessages {
|
|
37
|
+
/** Banner shown under the intro badge in fresh-project mode. */
|
|
38
|
+
wizardIntro: string;
|
|
39
|
+
/** "Project name" prompt label. */
|
|
40
|
+
promptProjectName: string;
|
|
41
|
+
/** Placeholder shown inside the project-name input. */
|
|
42
|
+
promptProjectNamePlaceholder: string;
|
|
43
|
+
/** "Storage adapter" select prompt label. */
|
|
44
|
+
promptAdapter: string;
|
|
45
|
+
/** Label for the browser adapter option. */
|
|
46
|
+
adapterBrowserLabel: string;
|
|
47
|
+
/** Label for the file adapter option. */
|
|
48
|
+
adapterFileLabel: string;
|
|
49
|
+
/** Label for the memory adapter option. */
|
|
50
|
+
adapterMemoryLabel: string;
|
|
51
|
+
/** "Include sample invoice records?" confirm label. */
|
|
52
|
+
promptSampleData: string;
|
|
53
|
+
/** Title of the "Next steps" note block. */
|
|
54
|
+
freshNextStepsTitle: string;
|
|
55
|
+
/** Success banner shown after the fresh project is created. */
|
|
56
|
+
freshOutroDone: string;
|
|
57
|
+
/** Title of the "augment mode detected" note block. */
|
|
58
|
+
augmentModeTitle: string;
|
|
59
|
+
/** First line of the augment-mode intro body — followed by the path. */
|
|
60
|
+
augmentDetectedPrefix: string;
|
|
61
|
+
/** Second/third lines explaining what augment mode will do. */
|
|
62
|
+
augmentDescription: string;
|
|
63
|
+
/** Title of the diff preview note block. */
|
|
64
|
+
augmentProposedChangesTitle: string;
|
|
65
|
+
/** Question shown at the confirm prompt. */
|
|
66
|
+
augmentApplyConfirm: string;
|
|
67
|
+
/** Title when the config is already configured. */
|
|
68
|
+
augmentAlreadyConfiguredTitle: string;
|
|
69
|
+
/** Prefix for the "already configured" reason line. */
|
|
70
|
+
augmentNothingToDo: string;
|
|
71
|
+
/** Success banner when there's nothing to do. */
|
|
72
|
+
augmentAlreadyOutro: string;
|
|
73
|
+
/** Cancel message when the user declines the confirm prompt. */
|
|
74
|
+
augmentAborted: string;
|
|
75
|
+
/** Success banner on dry-run success. */
|
|
76
|
+
augmentDryRunOutro: string;
|
|
77
|
+
/** Title of the "install these packages next" note block. */
|
|
78
|
+
augmentNextStepTitle: string;
|
|
79
|
+
/** Prose line above the install command. */
|
|
80
|
+
augmentInstallIntro: string;
|
|
81
|
+
/** Dim hint under the install command. */
|
|
82
|
+
augmentInstallPmHint: string;
|
|
83
|
+
/** Success banner after a real augmentation write. */
|
|
84
|
+
augmentDoneOutro: string;
|
|
85
|
+
/** Prefix for the "unsupported shape" error message. */
|
|
86
|
+
augmentUnsupportedPrefix: string;
|
|
87
|
+
/** Cancellation message used by Ctrl-C handlers. */
|
|
88
|
+
cancelled: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Types shared between the wizard, the bins, and the test harness.
|
|
93
|
+
*
|
|
94
|
+
* `WizardOptions` is the input shape — both the prompt UI and the test
|
|
95
|
+
* helper accept the same object so tests can skip the interactive prompts
|
|
96
|
+
* by passing answers up front.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Which built-in adapter to wire into the generated `nuxt.config.ts`.
|
|
101
|
+
*
|
|
102
|
+
* - `browser` — localStorage / IndexedDB. The recommended default for
|
|
103
|
+
* v0.3 because it makes the generated app a real PWA-friendly demo.
|
|
104
|
+
* - `file` — JSON files on disk. Useful for Electron / Tauri wraps and
|
|
105
|
+
* for the USB-stick workflow.
|
|
106
|
+
* - `memory` — no persistence. Mostly useful for tests and demos. Picked
|
|
107
|
+
* automatically when running in CI to avoid touching the test runner's
|
|
108
|
+
* localStorage.
|
|
109
|
+
*/
|
|
110
|
+
type WizardAdapter = 'browser' | 'file' | 'memory';
|
|
111
|
+
/**
|
|
112
|
+
* Inputs to `runWizard()`. All fields are optional — when a field is
|
|
113
|
+
* omitted the wizard prompts for it. Tests pass everything to skip
|
|
114
|
+
* prompts entirely.
|
|
115
|
+
*/
|
|
116
|
+
interface WizardOptions {
|
|
117
|
+
/**
|
|
118
|
+
* Project directory name. The wizard creates `<cwd>/<projectName>/`
|
|
119
|
+
* and refuses to overwrite an existing non-empty directory.
|
|
120
|
+
*/
|
|
121
|
+
projectName?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Adapter to use in the generated `nuxt.config.ts`. See `WizardAdapter`.
|
|
124
|
+
*/
|
|
125
|
+
adapter?: WizardAdapter;
|
|
126
|
+
/**
|
|
127
|
+
* Whether to include the seed-data invoices in the generated app. When
|
|
128
|
+
* `true`, the page renders pre-filled records on first load so the user
|
|
129
|
+
* sees something immediately. When `false`, the page starts empty and
|
|
130
|
+
* waits for the user to click "Add invoice".
|
|
131
|
+
*/
|
|
132
|
+
sampleData?: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Working directory the project should be created in. Defaults to
|
|
135
|
+
* `process.cwd()`. Tests pass a temp directory.
|
|
136
|
+
*/
|
|
137
|
+
cwd?: string;
|
|
138
|
+
/**
|
|
139
|
+
* When `true`, skip ALL interactive prompts and use only the values
|
|
140
|
+
* supplied above. Missing values become defaults (`browser`, `true`,
|
|
141
|
+
* a generated project name). This is the path tests take.
|
|
142
|
+
*/
|
|
143
|
+
yes?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Augment mode: show the proposed diff against an existing
|
|
146
|
+
* `nuxt.config.ts` but do not write the file. Only meaningful
|
|
147
|
+
* when the wizard detects an existing Nuxt project in `cwd`. A
|
|
148
|
+
* no-op in fresh-project mode.
|
|
149
|
+
*/
|
|
150
|
+
dryRun?: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Force fresh-project mode even when cwd looks like an existing
|
|
153
|
+
* Nuxt project. Useful for CI tests that create a scratch
|
|
154
|
+
* directory inside a parent that happens to have a nuxt.config.
|
|
155
|
+
*/
|
|
156
|
+
forceFresh?: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Locale for the wizard's user-facing prompts and notes. When
|
|
159
|
+
* omitted, the wizard auto-detects from `LC_ALL` / `LANG` env
|
|
160
|
+
* vars and falls back to `'en'`. Tests pin a value to make
|
|
161
|
+
* snapshot output deterministic.
|
|
162
|
+
*
|
|
163
|
+
* Validation/error messages are NOT translated — they stay in
|
|
164
|
+
* English so bug reports look the same across locales.
|
|
165
|
+
*/
|
|
166
|
+
locale?: Locale;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Output of `runWizard()` in fresh-project mode. The augment-mode
|
|
170
|
+
* path uses `WizardAugmentResult` instead; the caller narrows on
|
|
171
|
+
* the `kind` discriminator.
|
|
172
|
+
*/
|
|
173
|
+
interface WizardFreshResult {
|
|
174
|
+
readonly kind: 'fresh';
|
|
175
|
+
/** Resolved options after prompts/defaults. */
|
|
176
|
+
readonly options: {
|
|
177
|
+
readonly projectName: string;
|
|
178
|
+
readonly adapter: WizardAdapter;
|
|
179
|
+
readonly sampleData: boolean;
|
|
180
|
+
readonly cwd: string;
|
|
181
|
+
};
|
|
182
|
+
/** Absolute path of the created project directory. */
|
|
183
|
+
readonly projectPath: string;
|
|
184
|
+
/** Relative paths of every file the wizard wrote, sorted alphabetically. */
|
|
185
|
+
readonly files: string[];
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Output of `runWizard()` in augment mode. Carries the outcome of
|
|
189
|
+
* the magicast-based config mutation — either the file was
|
|
190
|
+
* actually written (`changed: true`), the file was already
|
|
191
|
+
* configured (`changed: false, reason: 'already-configured'`),
|
|
192
|
+
* or the user cancelled at the confirmation prompt (`changed: false,
|
|
193
|
+
* reason: 'cancelled'`), or we were in dry-run (`changed: false,
|
|
194
|
+
* reason: 'dry-run'`).
|
|
195
|
+
*/
|
|
196
|
+
interface WizardAugmentResult {
|
|
197
|
+
readonly kind: 'augment';
|
|
198
|
+
readonly configPath: string;
|
|
199
|
+
readonly adapter: WizardAdapter;
|
|
200
|
+
readonly changed: boolean;
|
|
201
|
+
readonly reason: 'written' | 'already-configured' | 'cancelled' | 'dry-run' | 'unsupported-shape';
|
|
202
|
+
/** The unified diff that was shown to the user, if any. */
|
|
203
|
+
readonly diff?: string;
|
|
204
|
+
}
|
|
205
|
+
type WizardResult = WizardFreshResult | WizardAugmentResult;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The wizard entry point — `runWizard()`.
|
|
209
|
+
*
|
|
210
|
+
* Two modes:
|
|
211
|
+
*
|
|
212
|
+
* 1. **Interactive (default).** Uses `@clack/prompts` to ask the user
|
|
213
|
+
* for project name, adapter, and sample-data inclusion. Cancellation
|
|
214
|
+
* at any prompt aborts cleanly with a non-zero exit code.
|
|
215
|
+
*
|
|
216
|
+
* 2. **Non-interactive (`yes: true`).** Skips every prompt and uses the
|
|
217
|
+
* values supplied in `WizardOptions`. Missing values become defaults.
|
|
218
|
+
* This is the path tests take — no terminal needed, fully scriptable.
|
|
219
|
+
*
|
|
220
|
+
* The function never spawns child processes (no `npm install` etc.). It
|
|
221
|
+
* only writes files and returns. The shell wrapper around `npm create` is
|
|
222
|
+
* responsible for installing — we keep this layer pure so it's trivially
|
|
223
|
+
* testable and so adding a `--no-install` flag later is a no-op.
|
|
224
|
+
*/
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Main entry point. Detects whether `cwd` is an existing Nuxt 4
|
|
228
|
+
* project and routes to one of two modes:
|
|
229
|
+
*
|
|
230
|
+
* - **Fresh mode** (the original v0.3.1 behavior): prompts for
|
|
231
|
+
* project name, creates a new directory, renders the Nuxt 4
|
|
232
|
+
* starter template. Returns a `WizardFreshResult`.
|
|
233
|
+
*
|
|
234
|
+
* - **Augment mode** (new in v0.5, #37): patches the existing
|
|
235
|
+
* `nuxt.config.ts` via magicast to add `@noy-db/nuxt` to the
|
|
236
|
+
* modules array and a `noydb:` config key. Shows a unified
|
|
237
|
+
* diff and asks for confirmation before writing. Supports
|
|
238
|
+
* `--dry-run`. Returns a `WizardAugmentResult`.
|
|
239
|
+
*
|
|
240
|
+
* The auto-detection rule: if cwd has both a `nuxt.config.ts`
|
|
241
|
+
* (or `.js`/`.mjs`) AND a `package.json` that lists `nuxt` in any
|
|
242
|
+
* dependency section, augment mode fires. Otherwise fresh mode.
|
|
243
|
+
* Users can force fresh mode via `forceFresh: true` (CLI:
|
|
244
|
+
* `--force-fresh`) when they want to create a sub-project inside
|
|
245
|
+
* an existing Nuxt workspace.
|
|
246
|
+
*
|
|
247
|
+
* Both modes refuse to clobber existing work: fresh mode rejects
|
|
248
|
+
* non-empty target dirs; augment mode rejects unsupported config
|
|
249
|
+
* shapes (opaque exports, non-array modules, etc.).
|
|
250
|
+
*/
|
|
251
|
+
declare function runWizard(options?: WizardOptions): Promise<WizardResult>;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* i18n entrypoint for the `@noy-db/create` wizard.
|
|
255
|
+
*
|
|
256
|
+
* Three responsibilities:
|
|
257
|
+
*
|
|
258
|
+
* 1. Re-export `Locale` and `WizardMessages` so callers don't
|
|
259
|
+
* need to know about the bundle layout.
|
|
260
|
+
* 2. `detectLocale(env)` — pure function that maps Unix-style
|
|
261
|
+
* `LC_ALL` / `LANG` / `LANGUAGE` env vars to a supported
|
|
262
|
+
* `Locale`. Returns `'en'` for anything we don't recognise.
|
|
263
|
+
* 3. `loadMessages(locale)` — synchronous lookup that returns
|
|
264
|
+
* the message bundle for a locale. Synchronous (not dynamic
|
|
265
|
+
* `import()`) on purpose: bundles are tiny (< 2 KB each), the
|
|
266
|
+
* wizard reads them on every prompt, and async would force
|
|
267
|
+
* every caller to be async. tsup tree-shakes unused locales
|
|
268
|
+
* out of the bin only if we use top-level `import`s.
|
|
269
|
+
*
|
|
270
|
+
* ## Why env-var detection instead of `Intl.DateTimeFormat().resolvedOptions().locale`
|
|
271
|
+
*
|
|
272
|
+
* The Intl approach reads the JS engine's *display* locale, which
|
|
273
|
+
* on most CI runners and Docker images is `en-US` regardless of
|
|
274
|
+
* the user's actual setup. The Unix env vars (`LC_ALL`, `LANG`)
|
|
275
|
+
* are how shells, terminals, and CLI tools have negotiated locale
|
|
276
|
+
* for 30+ years — that's what a Thai-speaking dev's terminal will
|
|
277
|
+
* actually have set. Following that convention also means power
|
|
278
|
+
* users can override per-invocation with `LANG=th_TH.UTF-8 npm
|
|
279
|
+
* create @noy-db`, no flag required.
|
|
280
|
+
*/
|
|
281
|
+
|
|
282
|
+
/** Every locale we ship a bundle for. Used by tests and `--lang` validation. */
|
|
283
|
+
declare const SUPPORTED_LOCALES: readonly Locale[];
|
|
284
|
+
/**
|
|
285
|
+
* Resolve a locale code to its message bundle. Falls back to `en`
|
|
286
|
+
* if the requested locale isn't shipped — defensive, since
|
|
287
|
+
* `Locale` is a union type and TS already prevents this at compile
|
|
288
|
+
* time, but `--lang` parsing comes from user input at runtime.
|
|
289
|
+
*/
|
|
290
|
+
declare function loadMessages(locale: Locale): WizardMessages;
|
|
291
|
+
/**
|
|
292
|
+
* Auto-detect a locale from POSIX env vars. Returns `'en'` when
|
|
293
|
+
* nothing is set or when the value doesn't match a supported
|
|
294
|
+
* locale — never throws.
|
|
295
|
+
*
|
|
296
|
+
* Inspection order matches the POSIX spec:
|
|
297
|
+
* 1. `LC_ALL` (overrides everything)
|
|
298
|
+
* 2. `LC_MESSAGES` (the category we actually care about)
|
|
299
|
+
* 3. `LANG` (system default)
|
|
300
|
+
* 4. `LANGUAGE` (GNU extension, comma-separated preference list)
|
|
301
|
+
*
|
|
302
|
+
* The first non-empty value wins. We then strip the encoding
|
|
303
|
+
* suffix (`th_TH.UTF-8` → `th_TH`) and the region (`th_TH` → `th`)
|
|
304
|
+
* before matching against `SUPPORTED_LOCALES`.
|
|
305
|
+
*/
|
|
306
|
+
declare function detectLocale(env?: NodeJS.ProcessEnv): Locale;
|
|
307
|
+
/**
|
|
308
|
+
* Parse a `--lang` CLI argument into a `Locale`. Throws a clear
|
|
309
|
+
* error for unsupported values — the caller (parse-args) catches
|
|
310
|
+
* and reformats into a usage message.
|
|
311
|
+
*/
|
|
312
|
+
declare function parseLocaleFlag(value: string): Locale;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* `noy-db add <collection>` — scaffold a new collection inside an existing
|
|
316
|
+
* Nuxt 4 project that already has `@noy-db/nuxt` configured.
|
|
317
|
+
*
|
|
318
|
+
* The command writes two files:
|
|
319
|
+
*
|
|
320
|
+
* 1. `app/stores/<collection>.ts` — a `defineNoydbStore<T>()` call with
|
|
321
|
+
* a placeholder `T` interface and one example field. The user fills
|
|
322
|
+
* in the real shape after the file is created.
|
|
323
|
+
*
|
|
324
|
+
* 2. `app/pages/<collection>.vue` — a minimal CRUD page that lists,
|
|
325
|
+
* adds, and deletes records. The store ID and collection name are
|
|
326
|
+
* derived from the argument; everything else is boilerplate.
|
|
327
|
+
*
|
|
328
|
+
* The command refuses to overwrite existing files. If either target
|
|
329
|
+
* already exists it logs which one and exits non-zero — the user has to
|
|
330
|
+
* delete or move the file first. There's no `--force` because forcing an
|
|
331
|
+
* overwrite of generated UI code is almost always a footgun in disguise.
|
|
332
|
+
*/
|
|
333
|
+
interface AddCollectionOptions {
|
|
334
|
+
/** The collection name. Must be a lowercase identifier. */
|
|
335
|
+
name: string;
|
|
336
|
+
/** Project root. Defaults to `process.cwd()`. */
|
|
337
|
+
cwd?: string;
|
|
338
|
+
/** Compartment id to embed in the generated store. Defaults to `default`. */
|
|
339
|
+
compartment?: string;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Result returned to callers (the bin entry uses this to format output;
|
|
343
|
+
* tests assert on the file paths).
|
|
344
|
+
*/
|
|
345
|
+
interface AddCollectionResult {
|
|
346
|
+
/** Files written, in the order they were created. */
|
|
347
|
+
files: string[];
|
|
348
|
+
}
|
|
349
|
+
declare function addCollection(options: AddCollectionOptions): Promise<AddCollectionResult>;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* `noy-db verify` — end-to-end integrity check.
|
|
353
|
+
*
|
|
354
|
+
* Opens an in-memory NOYDB instance, writes a record, reads it back,
|
|
355
|
+
* decrypts it, and asserts the round-trip is byte-identical. The check
|
|
356
|
+
* exercises the full crypto path (PBKDF2 → KEK → DEK → AES-GCM) without
|
|
357
|
+
* touching any user data on disk.
|
|
358
|
+
*
|
|
359
|
+
* Why an in-memory check is the right scope:
|
|
360
|
+
* - It validates that @noy-db/core, @noy-db/memory, and the user's
|
|
361
|
+
* installed Node version all agree on Web Crypto. That's the most
|
|
362
|
+
* common silent failure for first-time installers.
|
|
363
|
+
* - It cannot accidentally corrupt user data because there isn't any.
|
|
364
|
+
* - It runs in well under one second, so users actually run it.
|
|
365
|
+
*
|
|
366
|
+
* What this command does NOT do (intentionally):
|
|
367
|
+
* - Open the user's actual compartment file/dynamo/s3/browser store.
|
|
368
|
+
* That requires the user's passphrase — not something we want a CLI
|
|
369
|
+
* `verify` command to prompt for. The full passphrase-driven verify
|
|
370
|
+
* belongs in `nuxi noydb verify` once the auth story for CLIs lands
|
|
371
|
+
* in v0.4. For now `noy-db verify` is the dependency-graph smoke test.
|
|
372
|
+
*/
|
|
373
|
+
interface VerifyResult {
|
|
374
|
+
/** `true` if the round-trip succeeded; `false` if anything diverged. */
|
|
375
|
+
ok: boolean;
|
|
376
|
+
/** Human-readable status. Always set, even on success. */
|
|
377
|
+
message: string;
|
|
378
|
+
/** Wall-clock time the integrity check took, in ms. */
|
|
379
|
+
durationMs: number;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Runs the end-to-end check. Pure function — no console output, no
|
|
383
|
+
* `process.exit`. The bin wrapper handles formatting and exit codes so
|
|
384
|
+
* the function is trivial to call from tests.
|
|
385
|
+
*/
|
|
386
|
+
declare function verifyIntegrity(): Promise<VerifyResult>;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Shared primitives for the interactive `noy-db` subcommands that
|
|
390
|
+
* need to unlock a real compartment.
|
|
391
|
+
*
|
|
392
|
+
* Three things live here:
|
|
393
|
+
*
|
|
394
|
+
* 1. `ReadPassphrase` — a tiny interface for "prompt the user for
|
|
395
|
+
* a passphrase", with a test-friendly default. Subcommands take
|
|
396
|
+
* this as an injected dependency so tests can short-circuit
|
|
397
|
+
* the prompt without spawning a pty.
|
|
398
|
+
*
|
|
399
|
+
* 2. `defaultReadPassphrase` — the production implementation,
|
|
400
|
+
* built on `@clack/prompts` `password()`. Never echoes the
|
|
401
|
+
* value to the terminal, never logs it, clears it from the
|
|
402
|
+
* returned promise after the caller consumes it.
|
|
403
|
+
*
|
|
404
|
+
* 3. `assertRole` — narrow unknown string input to the Role type
|
|
405
|
+
* with a consistent error message.
|
|
406
|
+
*
|
|
407
|
+
* ## Why pull this out
|
|
408
|
+
*
|
|
409
|
+
* `rotate`, `addUser`, and `backup` all need the same "prompt for
|
|
410
|
+
* a passphrase" shape and the same "open a file adapter and get
|
|
411
|
+
* back a Noydb instance" shape. Duplicating it in three files would
|
|
412
|
+
* drift over time; centralizing means one place to audit the
|
|
413
|
+
* passphrase-handling contract (never log, never persist, clear
|
|
414
|
+
* local variables after use).
|
|
415
|
+
*/
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Asynchronous passphrase reader. Production code passes
|
|
419
|
+
* `defaultReadPassphrase`; tests pass a stub that returns a fixed
|
|
420
|
+
* string without touching stdin.
|
|
421
|
+
*
|
|
422
|
+
* The `label` is shown to the user as the prompt message. It
|
|
423
|
+
* should never contain the expected passphrase or any secret.
|
|
424
|
+
*/
|
|
425
|
+
type ReadPassphrase = (label: string) => Promise<string>;
|
|
426
|
+
/**
|
|
427
|
+
* Narrow an unknown string to the `Role` type from @noy-db/core.
|
|
428
|
+
* Used by the `add user` subcommand to validate the role argument
|
|
429
|
+
* before passing it to `noydb.grant()`.
|
|
430
|
+
*/
|
|
431
|
+
declare function assertRole(input: string): Role;
|
|
432
|
+
/**
|
|
433
|
+
* Split a comma-separated collection list into an array of names,
|
|
434
|
+
* trimming whitespace and dropping empties. Returns null if the
|
|
435
|
+
* input itself is empty or undefined — the caller decides whether
|
|
436
|
+
* that means "all collections" or "error".
|
|
437
|
+
*/
|
|
438
|
+
declare function parseCollectionList(input: string | undefined): string[] | null;
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* `noy-db rotate` — rotate the DEKs for one or more collections in
|
|
442
|
+
* a compartment.
|
|
443
|
+
*
|
|
444
|
+
* What it does
|
|
445
|
+
* ------------
|
|
446
|
+
* For each target collection:
|
|
447
|
+
*
|
|
448
|
+
* 1. Generate a fresh DEK
|
|
449
|
+
* 2. Decrypt every record with the old DEK
|
|
450
|
+
* 3. Re-encrypt every record with the new DEK
|
|
451
|
+
* 4. Re-wrap the new DEK into every remaining user's keyring
|
|
452
|
+
*
|
|
453
|
+
* The old DEKs become unreachable as soon as the keyring files are
|
|
454
|
+
* updated. This is the "just rotate" path — nobody is revoked,
|
|
455
|
+
* everybody keeps their current permissions, but the key material
|
|
456
|
+
* is replaced.
|
|
457
|
+
*
|
|
458
|
+
* Why expose this as a CLI command
|
|
459
|
+
* --------------------------------
|
|
460
|
+
* Two real-world scenarios:
|
|
461
|
+
*
|
|
462
|
+
* 1. **Suspected key leak.** An operator lost a laptop, a
|
|
463
|
+
* developer accidentally pasted a passphrase into a Slack
|
|
464
|
+
* channel, a USB stick went missing. Even if you think the
|
|
465
|
+
* passphrase is safe, rotating is cheap insurance.
|
|
466
|
+
*
|
|
467
|
+
* 2. **Scheduled rotation.** Some compliance regimes require
|
|
468
|
+
* periodic key rotation regardless of exposure. A CLI makes
|
|
469
|
+
* this scriptable from cron or a CI job.
|
|
470
|
+
*
|
|
471
|
+
* This module is test-first: all inputs are plain options, the
|
|
472
|
+
* passphrase reader is injected, and the Noydb factory is
|
|
473
|
+
* injectable. The production bin is a thin wrapper that defaults
|
|
474
|
+
* those injections to their real implementations.
|
|
475
|
+
*/
|
|
476
|
+
|
|
477
|
+
interface RotateOptions {
|
|
478
|
+
/** Directory containing the compartment data (file adapter only). */
|
|
479
|
+
dir: string;
|
|
480
|
+
/** Compartment (tenant) name to rotate keys in. */
|
|
481
|
+
compartment: string;
|
|
482
|
+
/** The user id of the operator running the rotate. */
|
|
483
|
+
user: string;
|
|
484
|
+
/**
|
|
485
|
+
* Explicit list of collections to rotate. When undefined, the
|
|
486
|
+
* rotation targets every collection the user has a DEK for —
|
|
487
|
+
* resolved at run time by reading the compartment snapshot.
|
|
488
|
+
*/
|
|
489
|
+
collections?: string[];
|
|
490
|
+
/** Injected passphrase reader. Defaults to the clack implementation. */
|
|
491
|
+
readPassphrase?: ReadPassphrase;
|
|
492
|
+
/**
|
|
493
|
+
* Injected Noydb factory. Production code leaves this undefined
|
|
494
|
+
* and gets `createNoydb`; tests pass a constructor that builds
|
|
495
|
+
* against an in-memory adapter.
|
|
496
|
+
*/
|
|
497
|
+
createDb?: typeof createNoydb;
|
|
498
|
+
/**
|
|
499
|
+
* Injected adapter factory. Production code leaves this undefined
|
|
500
|
+
* and gets `jsonFile`; tests pass one that returns the shared
|
|
501
|
+
* in-memory adapter their fixture used.
|
|
502
|
+
*/
|
|
503
|
+
buildAdapter?: (dir: string) => NoydbAdapter;
|
|
504
|
+
}
|
|
505
|
+
interface RotateResult {
|
|
506
|
+
/** The collections that were actually rotated. */
|
|
507
|
+
rotated: string[];
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Run the rotate flow against a file-adapter compartment. Returns
|
|
511
|
+
* the list of collections that were rotated so callers can display
|
|
512
|
+
* it to the user.
|
|
513
|
+
*
|
|
514
|
+
* Throws `Error` on any auth/adapter/rotate failure. The bin
|
|
515
|
+
* catches these and prints a friendly message; direct callers
|
|
516
|
+
* (tests) can inspect the error message to assert specific
|
|
517
|
+
* failure modes.
|
|
518
|
+
*/
|
|
519
|
+
declare function rotate(options: RotateOptions): Promise<RotateResult>;
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* `noy-db add user <id> <role>` — grant a new user access to a
|
|
523
|
+
* compartment.
|
|
524
|
+
*
|
|
525
|
+
* What it does
|
|
526
|
+
* ------------
|
|
527
|
+
* Wraps `noydb.grant()` in the CLI's auth-prompt ritual:
|
|
528
|
+
*
|
|
529
|
+
* 1. Prompt the caller for their own passphrase (to unlock the
|
|
530
|
+
* caller's keyring and derive the wrapping key).
|
|
531
|
+
* 2. Prompt for the new user's passphrase.
|
|
532
|
+
* 3. Prompt for confirmation of the new passphrase.
|
|
533
|
+
* 4. Reject on mismatch.
|
|
534
|
+
* 5. Call `noydb.grant(compartment, { userId, role, passphrase, permissions })`.
|
|
535
|
+
*
|
|
536
|
+
* For owner/admin/viewer roles, every collection is granted
|
|
537
|
+
* automatically (the core keyring.ts grant logic handles that via
|
|
538
|
+
* the `permissions` field). For operator/client, the caller must
|
|
539
|
+
* pass a `--collections` list because those roles need explicit
|
|
540
|
+
* per-collection permissions.
|
|
541
|
+
*
|
|
542
|
+
* ## What this does NOT do
|
|
543
|
+
*
|
|
544
|
+
* - No email/invite flow — v0.5 is about local-CLI key management,
|
|
545
|
+
* not out-of-band user enrollment.
|
|
546
|
+
* - No rollback on partial failure — `grant()` is atomic at the
|
|
547
|
+
* core level (keyring file writes last, after DEK wrapping), so
|
|
548
|
+
* partial-state-on-crash is already handled.
|
|
549
|
+
*/
|
|
550
|
+
|
|
551
|
+
interface AddUserOptions {
|
|
552
|
+
/** Directory containing the compartment data (file adapter only). */
|
|
553
|
+
dir: string;
|
|
554
|
+
/** Compartment (tenant) name to grant access to. */
|
|
555
|
+
compartment: string;
|
|
556
|
+
/** The user id of the caller running the grant. */
|
|
557
|
+
callerUser: string;
|
|
558
|
+
/** The new user's id (must not already exist in the compartment keyring). */
|
|
559
|
+
newUserId: string;
|
|
560
|
+
/** The new user's display name — shown in UI and audit logs. Defaults to `newUserId`. */
|
|
561
|
+
newUserDisplayName?: string;
|
|
562
|
+
/** The new user's role. */
|
|
563
|
+
role: Role;
|
|
564
|
+
/**
|
|
565
|
+
* Per-collection permissions. Required when `role` is operator or
|
|
566
|
+
* client; ignored for owner/admin/viewer (they get everything
|
|
567
|
+
* via the core's resolvePermissions logic).
|
|
568
|
+
*
|
|
569
|
+
* Shape: `{ invoices: 'rw', clients: 'ro' }`. CLI callers pass
|
|
570
|
+
* `--collections invoices:rw,clients:ro` and the argv parser
|
|
571
|
+
* converts it to this shape.
|
|
572
|
+
*/
|
|
573
|
+
permissions?: Record<string, 'rw' | 'ro'>;
|
|
574
|
+
/** Injected passphrase reader. Defaults to the clack implementation. */
|
|
575
|
+
readPassphrase?: ReadPassphrase;
|
|
576
|
+
/** Injected Noydb factory. */
|
|
577
|
+
createDb?: typeof createNoydb;
|
|
578
|
+
/** Injected adapter factory. */
|
|
579
|
+
buildAdapter?: (dir: string) => NoydbAdapter;
|
|
580
|
+
}
|
|
581
|
+
interface AddUserResult {
|
|
582
|
+
/** The userId that was granted access. */
|
|
583
|
+
userId: string;
|
|
584
|
+
/** The role they were granted. */
|
|
585
|
+
role: Role;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Run the grant flow. Two passphrase prompts: caller's, then new
|
|
589
|
+
* user's (twice for confirmation). Calls `noydb.grant()` with the
|
|
590
|
+
* collected values.
|
|
591
|
+
*/
|
|
592
|
+
declare function addUser(options: AddUserOptions): Promise<AddUserResult>;
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* `noy-db backup <target>` — dump a compartment to a local file.
|
|
596
|
+
*
|
|
597
|
+
* What it does
|
|
598
|
+
* ------------
|
|
599
|
+
* Wraps `compartment.dump()` in the CLI's auth-prompt ritual, then
|
|
600
|
+
* writes the serialized backup to the requested path. As of v0.4,
|
|
601
|
+
* `dump()` already produces a verifiable backup (embedded
|
|
602
|
+
* ledgerHead, full `_ledger` / `_ledger_deltas` snapshots) — the
|
|
603
|
+
* CLI just moves bytes; the integrity guarantees come from core.
|
|
604
|
+
*
|
|
605
|
+
* ## Target URI support
|
|
606
|
+
*
|
|
607
|
+
* v0.5.0 ships **`file://` only** (or a plain filesystem path).
|
|
608
|
+
* The issue spec originally called for `s3://` as well, but
|
|
609
|
+
* wiring @aws-sdk into @noy-db/create would defeat the
|
|
610
|
+
* zero-runtime-deps story for the CLI package. S3 backup is
|
|
611
|
+
* deferred to a follow-up that can live in @noy-db/s3-cli or a
|
|
612
|
+
* similar optional companion package.
|
|
613
|
+
*
|
|
614
|
+
* Accepted forms:
|
|
615
|
+
* - `file:///absolute/path.json`
|
|
616
|
+
* - `file://./relative/path.json`
|
|
617
|
+
* - `/absolute/path.json` (treated as `file://`)
|
|
618
|
+
* - `./relative/path.json` (treated as `file://`)
|
|
619
|
+
*
|
|
620
|
+
* ## What this does NOT do
|
|
621
|
+
*
|
|
622
|
+
* - No encryption of the backup BEYOND what noy-db already does.
|
|
623
|
+
* The dumped file is a valid noy-db backup, which means
|
|
624
|
+
* individual records are still encrypted but the keyring is
|
|
625
|
+
* included (wrapped with each user's KEK). Anyone who loads
|
|
626
|
+
* the backup still needs the correct passphrase to read.
|
|
627
|
+
* - No restore — that's a separate subcommand tracked as a
|
|
628
|
+
* follow-up. For now users can restore via
|
|
629
|
+
* `compartment.load(backupString)` from their own app code.
|
|
630
|
+
*/
|
|
631
|
+
|
|
632
|
+
interface BackupOptions {
|
|
633
|
+
/** Directory containing the compartment data (file adapter only). */
|
|
634
|
+
dir: string;
|
|
635
|
+
/** Compartment (tenant) name to back up. */
|
|
636
|
+
compartment: string;
|
|
637
|
+
/** The user id of the operator running the backup. */
|
|
638
|
+
user: string;
|
|
639
|
+
/**
|
|
640
|
+
* Where to write the backup. Accepts a `file://` URI or a plain
|
|
641
|
+
* filesystem path. Relative paths resolve against `process.cwd()`.
|
|
642
|
+
*/
|
|
643
|
+
target: string;
|
|
644
|
+
/** Injected passphrase reader. */
|
|
645
|
+
readPassphrase?: ReadPassphrase;
|
|
646
|
+
/** Injected Noydb factory. */
|
|
647
|
+
createDb?: typeof createNoydb;
|
|
648
|
+
/** Injected adapter factory. */
|
|
649
|
+
buildAdapter?: (dir: string) => NoydbAdapter;
|
|
650
|
+
}
|
|
651
|
+
interface BackupResult {
|
|
652
|
+
/** Absolute filesystem path the backup was written to. */
|
|
653
|
+
path: string;
|
|
654
|
+
/** Size of the serialized backup in bytes. */
|
|
655
|
+
bytes: number;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Parse a backup target into an absolute filesystem path. Rejects
|
|
659
|
+
* unsupported URI schemes (s3://, https://, etc.) early so the
|
|
660
|
+
* caller doesn't silently write to the wrong place.
|
|
661
|
+
*/
|
|
662
|
+
declare function resolveBackupTarget(target: string, cwd?: string): string;
|
|
663
|
+
declare function backup(options: BackupOptions): Promise<BackupResult>;
|
|
664
|
+
|
|
665
|
+
export { type AddCollectionOptions, type AddUserOptions, type AddUserResult, type BackupOptions, type BackupResult, type Locale, type ReadPassphrase, 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 };
|