firevault 0.1.1-beta.1 → 0.1.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,7 @@
6
6
  - Added init Git safety checks, `--force`, and `--yes`.
7
7
  - Added safe `.gitignore` updates for service account keys, backup output, and emulator logs.
8
8
  - Ensured Firevault can still explicitly commit the configured backup directory even when it is ignored by default.
9
+ - Added a guarded local npm prerelease publish workflow using `gitversionjs`, pack verification, and forbidden-path checks.
9
10
 
10
11
  ## 0.1.0
11
12
 
package/README.md CHANGED
@@ -49,6 +49,20 @@ firevault init
49
49
 
50
50
  `firevault init` asks for your Firebase project ID, service account path, output directory, and collections. It also checks Git state before writing files and appends safety entries to `.gitignore`.
51
51
 
52
+ After you enter a project ID, Firevault prints the direct Firebase Console URL for that project's Admin SDK service account page:
53
+
54
+ ```txt
55
+ Create a Firebase service account key here:
56
+
57
+ https://console.firebase.google.com/project/your-project-id/settings/serviceaccounts/adminsdk
58
+
59
+ Download the JSON key and save it as:
60
+
61
+ ./serviceAccountKey.json
62
+ ```
63
+
64
+ Firevault does not create service accounts, open a browser, run `gcloud`, or authenticate against Firebase during setup.
65
+
52
66
  Generated `firevault.config.json`:
53
67
 
54
68
  ```json
@@ -330,15 +344,36 @@ Covered emulator flows:
330
344
 
331
345
  ## Publishing
332
346
 
333
- Before publishing:
347
+ Firevault uses a local publish script for early prereleases. npm auth must already be configured before publishing; `npm whoami` should succeed for the intended npm account.
348
+
349
+ Calculate the Git-derived prerelease version:
350
+
351
+ ```bash
352
+ npm run version:calculate
353
+ ```
354
+
355
+ Verify the package without publishing:
356
+
357
+ ```bash
358
+ npm run publish:dry-run
359
+ ```
360
+
361
+ Publish the prerelease with the `next` dist-tag:
362
+
363
+ ```bash
364
+ npm run publish:next
365
+ ```
366
+
367
+ Use `npm run publish:next -- --yes` only when you intentionally want to skip the final confirmation prompt.
368
+
369
+ The publish script:
334
370
 
335
- - run `npm run build`,
336
- - run `npm run test:emulator`,
337
- - run `npm pack --dry-run` and review the file list,
338
- - verify `dist/index.js` exists and starts with `#!/usr/bin/env node`,
339
- - do not publish `serviceAccountKey.json`,
340
- - do not publish local `firestore-backups/` output,
341
- - verify logs such as `firestore-debug.log` are not included.
371
+ - requires a clean Git working tree before real publishing,
372
+ - calculates the npm prerelease version with `gitversionjs`,
373
+ - runs clean, build, and emulator tests,
374
+ - runs `npm pack --dry-run --cache /private/tmp/firevault-npm-cache`,
375
+ - rejects forbidden package contents such as `serviceAccountKey.json`, `firestore-backups/`, `firestore-debug.log`, `src/`, `test/`, `firebase.json`, `firestore.rules`, and `.env` files,
376
+ - publishes with `npm publish --access public --tag next --cache /private/tmp/firevault-npm-cache`.
342
377
 
343
378
  The package `bin` points to `./dist/index.js`, so a published or linked package must include compiled output. `prepublishOnly` currently runs clean, build, and emulator tests.
344
379
 
@@ -31,41 +31,61 @@ function parseCollections(value) {
31
31
  .split(",")
32
32
  .map((collection) => collection.trim());
33
33
  }
34
- async function promptForConfig(options) {
34
+ function getServiceAccountUrl(projectId) {
35
+ return `https://console.firebase.google.com/project/${encodeURIComponent(projectId)}/settings/serviceaccounts/adminsdk`;
36
+ }
37
+ function printServiceAccountGuidance(projectId, serviceAccountPath) {
38
+ console.log("");
39
+ console.log("Create a Firebase service account key here:");
40
+ console.log("");
41
+ console.log(getServiceAccountUrl(projectId));
42
+ console.log("");
43
+ console.log("Download the JSON key and save it as:");
44
+ console.log("");
45
+ console.log(serviceAccountPath);
46
+ console.log("");
47
+ }
48
+ function printMissingServiceAccountInfo(serviceAccountPath) {
49
+ if (existsSync(serviceAccountPath)) {
50
+ return;
51
+ }
52
+ console.log(`Service account file does not exist yet: ${serviceAccountPath}`);
53
+ console.log("That is expected before downloading the Firebase Admin SDK key.");
54
+ console.log(`Save the downloaded JSON key at: ${serviceAccountPath}`);
55
+ console.log("");
56
+ }
57
+ async function promptForConfig(options, rl) {
35
58
  if (options.yes) {
36
59
  return defaultConfig;
37
60
  }
38
- const rl = createInterface({ input, output });
39
- try {
40
- const projectId = (await rl.question("Firebase project ID: ")).trim();
41
- const serviceAccountPath = (await rl.question(`Service account path (${defaultConfig.serviceAccountPath}): `)).trim() || defaultConfig.serviceAccountPath;
42
- const outputDir = (await rl.question(`Output directory (${defaultConfig.outputDir}): `)).trim() || defaultConfig.outputDir;
43
- const collectionsInput = (await rl.question("Collections, comma-separated: ")).trim();
44
- return {
45
- projectId,
46
- serviceAccountPath,
47
- outputDir,
48
- collections: parseCollections(collectionsInput),
49
- };
50
- }
51
- finally {
52
- rl.close();
53
- }
61
+ if (!rl) {
62
+ throw new Error("Prompt interface is required for interactive init.");
63
+ }
64
+ const projectId = (await rl.question("Firebase project ID: ")).trim();
65
+ if (projectId !== "") {
66
+ printServiceAccountGuidance(projectId, defaultConfig.serviceAccountPath);
67
+ }
68
+ const serviceAccountPath = (await rl.question(`Service account path (${defaultConfig.serviceAccountPath}): `)).trim() || defaultConfig.serviceAccountPath;
69
+ const outputDir = (await rl.question(`Output directory (${defaultConfig.outputDir}): `)).trim() || defaultConfig.outputDir;
70
+ const collectionsInput = (await rl.question("Collections, comma-separated: ")).trim();
71
+ return {
72
+ projectId,
73
+ serviceAccountPath,
74
+ outputDir,
75
+ collections: parseCollections(collectionsInput),
76
+ };
54
77
  }
55
- async function promptForGitInit(options) {
78
+ async function promptForGitInit(options, rl) {
56
79
  if (options.yes) {
57
80
  return true;
58
81
  }
59
- const rl = createInterface({ input, output });
60
- try {
61
- const answer = (await rl.question("This directory is not a Git repository. Run git init? (Y/n): "))
62
- .trim()
63
- .toLowerCase();
64
- return answer === "" || answer === "y" || answer === "yes";
65
- }
66
- finally {
67
- rl.close();
82
+ if (!rl) {
83
+ throw new Error("Prompt interface is required for interactive init.");
68
84
  }
85
+ const answer = (await rl.question("This directory is not a Git repository. Run git init? (Y/n): "))
86
+ .trim()
87
+ .toLowerCase();
88
+ return answer === "" || answer === "y" || answer === "yes";
69
89
  }
70
90
  function ensureGitignoreEntries(entries) {
71
91
  const gitignorePath = ".gitignore";
@@ -89,39 +109,49 @@ export async function runInit(options) {
89
109
  console.log("");
90
110
  const configPath = "firevault.config.json";
91
111
  const alreadyInGitRepository = isInsideGitRepository();
92
- if (alreadyInGitRepository && hasWorkingTreeChanges() && !options.force) {
93
- throw new Error("Git working tree has changes. Commit, stash, or rerun with --force before init writes files.");
94
- }
95
- if (existsSync(configPath) && !options.force) {
96
- throw new Error("firevault.config.json already exists. Rerun with --force to overwrite it.");
97
- }
98
- const shouldInitGit = alreadyInGitRepository
99
- ? false
100
- : await promptForGitInit(options);
101
- const config = await promptForConfig(options);
102
- config.collections = config.collections.map((collection) => collection.trim());
103
- validateConfig(config);
104
- if (shouldInitGit) {
105
- initGitRepository();
106
- console.log("Initialized Git repository.");
107
- }
108
- if (existsSync(configPath) && options.force) {
109
- console.log("Warning: overwriting existing firevault.config.json.");
110
- }
111
- writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
112
- ensureGitignoreEntries([
113
- "serviceAccountKey.json",
114
- "firestore-backups/",
115
- "firestore-debug.log",
116
- ]);
117
- console.log("Created firevault.config.json.");
118
- console.log("Updated .gitignore safety entries.");
119
- console.log("");
120
- console.log("Next steps:");
121
- console.log(`1. Save your service account key at ${config.serviceAccountPath}`);
122
- console.log("2. Run `firevault snapshot`");
123
- console.log("3. Run `firevault changes`");
124
- console.log("4. Run `firevault restore-preview <path> --from <commit>` before a real restore");
112
+ const rl = options.yes ? undefined : createInterface({ input, output });
113
+ try {
114
+ if (alreadyInGitRepository && hasWorkingTreeChanges() && !options.force) {
115
+ throw new Error("Git working tree has changes. Commit, stash, or rerun with --force before init writes files.");
116
+ }
117
+ if (existsSync(configPath) && !options.force) {
118
+ throw new Error("firevault.config.json already exists. Rerun with --force to overwrite it.");
119
+ }
120
+ const shouldInitGit = alreadyInGitRepository
121
+ ? false
122
+ : await promptForGitInit(options, rl);
123
+ const config = await promptForConfig(options, rl);
124
+ config.collections = config.collections.map((collection) => collection.trim());
125
+ validateConfig(config);
126
+ if (options.yes) {
127
+ printServiceAccountGuidance(config.projectId, config.serviceAccountPath);
128
+ }
129
+ printMissingServiceAccountInfo(config.serviceAccountPath);
130
+ if (shouldInitGit) {
131
+ initGitRepository();
132
+ console.log("Initialized Git repository.");
133
+ }
134
+ if (existsSync(configPath) && options.force) {
135
+ console.log("Warning: overwriting existing firevault.config.json.");
136
+ }
137
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
138
+ ensureGitignoreEntries([
139
+ "serviceAccountKey.json",
140
+ "firestore-backups/",
141
+ "firestore-debug.log",
142
+ ]);
143
+ console.log("Created firevault.config.json.");
144
+ console.log("Updated .gitignore safety entries.");
145
+ console.log("");
146
+ console.log("Next steps:");
147
+ console.log(`1. Save your service account key at ${config.serviceAccountPath}`);
148
+ console.log("2. Run `firevault snapshot`");
149
+ console.log("3. Run `firevault changes`");
150
+ console.log("4. Run `firevault restore-preview <path> --from <commit>` before a real restore");
151
+ }
152
+ finally {
153
+ rl?.close();
154
+ }
125
155
  }
126
156
  export const initCommand = new Command("init")
127
157
  .description("Create a guided Firevault configuration")
@@ -94,10 +94,12 @@ Behavior:
94
94
  - prints the Firevault identity before prompting,
95
95
  - detects whether the current directory is inside a Git repository,
96
96
  - offers `git init` when no repository exists,
97
+ - prints the Firebase Console Admin SDK service account URL for the entered project ID,
98
+ - explains where to save the manually downloaded service account key,
97
99
  - refuses to run in a dirty Git working tree unless `--force` is provided,
98
100
  - refuses to overwrite `firevault.config.json` unless `--force` is provided,
99
101
  - appends `.gitignore` safety entries without duplicating existing lines,
100
- - never commits, pushes, creates GitHub repositories, contacts Firebase, or writes secrets.
102
+ - never creates service accounts, opens browsers, runs `gcloud`, commits, pushes, creates GitHub repositories, contacts Firebase, or writes secrets.
101
103
 
102
104
  ## Firebase Access
103
105
 
@@ -109,7 +111,8 @@ Design constraints:
109
111
 
110
112
  - operate only on existing Firestore projects,
111
113
  - keep credentials local,
112
- - do not introduce hosted auth or account systems,
114
+ - use manually managed service account credentials,
115
+ - do not introduce credential brokerage, hosted auth, or account systems,
113
116
  - avoid committing service account files.
114
117
 
115
118
  ## Emulator Tests
@@ -132,6 +135,12 @@ Current emulator coverage:
132
135
  - collection path rejection,
133
136
  - `--confirm` enforcement.
134
137
 
138
+ ## Publishing
139
+
140
+ Prerelease publishing is intentionally local-script based for now. `scripts/publish.ts` calculates an npm-safe prerelease version from Git state with `gitversionjs`, verifies a clean working tree for real publishes, runs clean/build/emulator tests, inspects `npm pack --dry-run` contents, rejects known unsafe paths, and publishes with the `next` npm dist-tag only after explicit confirmation.
141
+
142
+ There is no GitHub Actions publishing, trusted publishing, or release automation beyond this local guard script yet.
143
+
135
144
  ## Export Pipeline
136
145
 
137
146
  Current backup flow:
package/docs/roadmap.md CHANGED
@@ -24,6 +24,7 @@ Status:
24
24
  - Firestore emulator integration tests cover backup and document restore safety paths.
25
25
  - CLI packaging runs from compiled `dist/index.js`.
26
26
  - npm prerelease packaging is guarded by package file whitelisting and pack verification.
27
+ - Local npm prerelease publishing is guarded by a `gitversionjs`-based publish script with clean-tree, build, emulator-test, pack, and forbidden-path checks.
27
28
  - Public GitHub release docs cover quick start, security, contributing, and issue triage.
28
29
  - Guided `firevault init` validates setup input, checks Git state, and applies `.gitignore` safety entries.
29
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firevault",
3
- "version": "0.1.1-beta.1",
3
+ "version": "0.1.1-beta.2",
4
4
  "description": "Undo button for Firestore. Git-style history, rollback, and recovery for Firestore projects.",
5
5
  "keywords": [
6
6
  "firebase",
@@ -33,6 +33,9 @@
33
33
  "clean": "rm -rf dist",
34
34
  "build": "tsc && chmod +x dist/index.js",
35
35
  "prepublishOnly": "npm run clean && npm run build && npm run test:emulator",
36
+ "version:calculate": "tsx scripts/publish.ts --version-only",
37
+ "publish:dry-run": "tsx scripts/publish.ts --dry-run --allow-dirty",
38
+ "publish:next": "tsx scripts/publish.ts",
36
39
  "test:emulator": "tsx test/integration/run-firestore-emulator.ts",
37
40
  "backup": "tsx src/index.ts backup",
38
41
  "commit": "tsx src/index.ts commit",
@@ -51,6 +54,7 @@
51
54
  "devDependencies": {
52
55
  "@types/node": "^24.0.0",
53
56
  "firebase-tools": "^15.18.0",
57
+ "gitversionjs": "^1.0.15",
54
58
  "tsx": "^4.20.0",
55
59
  "typescript": "^5.8.0"
56
60
  }