backend-manager 5.2.0 → 5.2.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
@@ -14,6 +14,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ # [5.2.2] - 2026-05-21
18
+
19
+ ### Added
20
+
21
+ - **`consent` is now a protected user field.** `templates/firestore.rules` includes `consent` in `isWritingProtectedUserField()` so a logged-in user cannot retroactively forge their own consent record from the client — only the signup route + webhook processors can mutate it server-side. New rule test in `test/rules/user.js`.
22
+ - **`BaseCommand.getLogsPath(name)` / `getTempPath(name)`** in `src/cli/commands/base-command.js`. Two explicit helpers so the folder convention is the SSOT and easy to change later. `getLogsPath()` writes human-readable logs (`serve.log`, `emulator.log`, `test.log`, `logs.log`) to `<projectDir>/functions/` alongside firebase-tools' own `*-debug.log` files. `getTempPath()` writes transient internal-only stuff (`*.log.reset` sentinels, `bem-reload-trigger.js`, `test-mode.json`) to `<projectDir>/.temp/`.
23
+ - **`BaseCommand.sweepStaleLogs()`** wipes BEM's own `.log` files in `functions/` and `.reset` sentinels in `.temp/` on every emulator/serve boot and on `npx mgr setup`. Deliberately does NOT touch firebase-tools' debug logs (`firestore-debug.log`, `database-debug.log`, etc.) so users can grep them after a crash.
24
+ - **`npx mgr setup` cleanup step.** `cleanupGeneratedArtifacts()` now removes the watch trigger file (existing behavior) plus calls `sweepStaleLogs()` to clean up old BEM-owned logs/sentinels from previous runs.
25
+
26
+ # [5.2.1] - 2026-05-21
27
+
28
+ ### Added
29
+
30
+ - **`BACKEND_MANAGER_WEBHOOK_KEY` in `templates/_.env`.** The `.env` scaffold the setup CLI copies into consumer projects now declares the webhook key alongside `BACKEND_MANAGER_KEY`. Required for the new `/marketing/webhook` + `/marketing/webhook/forward` routes shipped in 5.2.0. Existing consumers should add it to their own `.env` manually.
31
+
17
32
  # [5.2.0] - 2026-05-21
18
33
 
19
34
  ### Added
package/README.md CHANGED
@@ -850,12 +850,13 @@ npx mgr test user/ admin/ # Multiple paths
850
850
 
851
851
  ### Log Files
852
852
 
853
- BEM CLI commands automatically save output to log files in the project directory:
854
- - **`emulator.log`** — Full emulator + Cloud Functions output (`npx mgr emulator`)
855
- - **`test.log`** — Test runner output (`npx mgr test`, when running against an existing emulator)
856
- - **`logs.log`** — Cloud Function logs (`npx mgr logs:read` or `npx mgr logs:tail`)
853
+ BEM CLI commands automatically save output to log files in the project's `functions/` directory (alongside firebase-tools' own `*-debug.log` files so everything is grep-able from one place):
854
+ - **`functions/serve.log`** — Output from `npx mgr serve`
855
+ - **`functions/emulator.log`** — Full emulator + Cloud Functions output (`npx mgr emulator`)
856
+ - **`functions/test.log`** — Test runner output (`npx mgr test`, when running against an existing emulator)
857
+ - **`functions/logs.log`** — Cloud Function logs (`npx mgr logs:read` or `npx mgr logs:tail`)
857
858
 
858
- Logs are overwritten on each run. Use them to debug failing tests or review function output.
859
+ Logs are overwritten on each run and gitignored via `*.log`. Use them to debug failing tests or review function output. Transient internal artifacts (reset sentinels, watch trigger, `test-mode.json`) live separately in `<projectDir>/.temp/`.
859
860
 
860
861
  ### Test Locations
861
862
 
package/docs/testing.md CHANGED
@@ -94,7 +94,7 @@ npx mgr test ... # ← this still flips it back
94
94
 
95
95
  ## Log Files
96
96
 
97
- BEM CLI commands automatically save all output to log files in `functions/` while still streaming to the console:
97
+ BEM CLI commands automatically save all output to log files in `<projectDir>/functions/` while still streaming to the console — co-located with firebase-tools' own `*-debug.log` files so everything can be grepped from one directory:
98
98
  - **`functions/serve.log`** — Output from `npx mgr serve` (Firebase serve)
99
99
  - **`functions/emulator.log`** — Full emulator output (Firebase emulator + Cloud Functions logs)
100
100
  - **`functions/test.log`** — Test runner output (when running against an existing emulator)
@@ -102,7 +102,7 @@ BEM CLI commands automatically save all output to log files in `functions/` whil
102
102
 
103
103
  When `npx mgr test` starts its own emulator, logs go to `emulator.log` (since it delegates to the emulator command). When running against an already-running emulator, logs go to `test.log`.
104
104
 
105
- These files are overwritten on each run and are gitignored (`*.log`). Use them to search for errors, debug webhook pipelines, or review full function output after a test run.
105
+ These files are overwritten on each run and are gitignored via `*.log`. Reset sentinels (`*.log.reset`), the watch trigger file, and `test-mode.json` live separately in `<projectDir>/.temp/` because they're transient internal signals with no debugging value.
106
106
 
107
107
  ## Filtering Tests
108
108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.2.0",
3
+ "version": "5.2.2",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -16,6 +16,73 @@ class BaseCommand {
16
16
  throw new Error('Execute method must be implemented');
17
17
  }
18
18
 
19
+ /**
20
+ * Resolve a path inside the consumer project's `.temp/` directory. Used for
21
+ * TRULY internal artifacts that have no debugging value: reset sentinels
22
+ * (*.log.reset), the watch command's reload trigger, and `test-mode.json`.
23
+ *
24
+ * For human-readable log files, use `getLogsPath()` instead — those live in
25
+ * `functions/` next to firebase-tools' own *-debug.log files so all log
26
+ * output can be grepped from one directory.
27
+ *
28
+ * Ensures the directory exists.
29
+ * @param {string} [filename] - File name to append (omit to get the dir path).
30
+ * @returns {string} Absolute path.
31
+ */
32
+ getTempPath(filename) {
33
+ const projectDir = this.main.firebaseProjectPath;
34
+ const tempDir = path.join(projectDir, '.temp');
35
+ jetpack.dir(tempDir);
36
+ return filename ? path.join(tempDir, filename) : tempDir;
37
+ }
38
+
39
+ /**
40
+ * Resolve a path for a human-readable log file. BEM-owned logs (serve.log,
41
+ * emulator.log, test.log, logs.log) live in `functions/` alongside
42
+ * firebase-tools' own *-debug.log files so all log output is grep-able from
43
+ * one place. Reset sentinels and other internal-only artifacts use
44
+ * `getTempPath()` instead.
45
+ *
46
+ * @param {string} [filename] - File name to append (omit to get the dir path).
47
+ * @returns {string} Absolute path.
48
+ */
49
+ getLogsPath(filename) {
50
+ const projectDir = this.main.firebaseProjectPath;
51
+ const logsDir = path.join(projectDir, 'functions');
52
+ return filename ? path.join(logsDir, filename) : logsDir;
53
+ }
54
+
55
+ /**
56
+ * Sweep stale BEM-owned logs out of `functions/`. Catches `.log` files
57
+ * from previous runs so each emulator/serve/test boot starts with a clean
58
+ * slate. Also catches stale `.reset` sentinels in `.temp/` that a crashed
59
+ * process may have left behind.
60
+ *
61
+ * Firebase-tools writes its own debug logs (firestore-debug.log,
62
+ * database-debug.log, pubsub-debug.log, firebase-debug.log, ui-debug.log) to
63
+ * cwd and we can't redirect them — we deliberately do NOT touch those, so
64
+ * users can grep them after a crash.
65
+ */
66
+ sweepStaleLogs() {
67
+ const logFiles = [
68
+ 'serve.log',
69
+ 'emulator.log',
70
+ 'test.log',
71
+ 'logs.log',
72
+ ];
73
+ const resetSentinels = [
74
+ 'serve.log.reset',
75
+ 'emulator.log.reset',
76
+ ];
77
+
78
+ for (const name of logFiles) {
79
+ try { jetpack.remove(this.getLogsPath(name)); } catch (e) { /* best-effort */ }
80
+ }
81
+ for (const name of resetSentinels) {
82
+ try { jetpack.remove(this.getTempPath(name)); } catch (e) { /* best-effort */ }
83
+ }
84
+ }
85
+
19
86
  log(...args) {
20
87
  console.log(...args);
21
88
  }
@@ -73,6 +73,10 @@ class EmulatorCommand extends BaseCommand {
73
73
  throw new Error('Port conflicts could not be resolved');
74
74
  }
75
75
 
76
+ // Wipe stale firebase-tools debug logs + any leftover BEM logs from older
77
+ // versions. Keeps the project tree clean across runs.
78
+ this.sweepStaleLogs();
79
+
76
80
  // BEM_TESTING=true is passed so Functions skip external API calls (emails, SendGrid)
77
81
  // hosting is included so localhost:5002 rewrites work (e.g., /backend-manager -> bm_api)
78
82
  // pubsub is included so scheduled functions (bm_cronDaily) can be triggered in tests
@@ -87,8 +91,8 @@ class EmulatorCommand extends BaseCommand {
87
91
  // by touching emulator.log.reset — the watcher below detects it, closes the
88
92
  // current stream, reopens with flags: 'w' (truncating cleanly from our process'
89
93
  // perspective, no sparse-file issue), and deletes the sentinel.
90
- const logPath = path.join(projectDir, 'functions', 'emulator.log');
91
- const resetSentinelPath = `${logPath}.reset`;
94
+ const logPath = this.getLogsPath('emulator.log');
95
+ const resetSentinelPath = this.getTempPath('emulator.log.reset');
92
96
  const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
93
97
 
94
98
  let currentStream = fs.createWriteStream(logPath, { flags: 'w' });
@@ -72,9 +72,8 @@ class LogsCommand extends BaseCommand {
72
72
  `--order=${order}`,
73
73
  ].filter(Boolean).join(' ');
74
74
 
75
- // Set up log file in the project directory
76
- const projectDir = this.main.firebaseProjectPath;
77
- const logPath = path.join(projectDir, 'functions', 'logs.log');
75
+ // Set up log file in the project's functions/ directory
76
+ const logPath = this.getLogsPath('logs.log');
78
77
 
79
78
  this.log(chalk.gray(` Filter: ${filter || '(none)'}`));
80
79
  this.log(chalk.gray(` Limit: ${limit}`));
@@ -126,9 +125,8 @@ class LogsCommand extends BaseCommand {
126
125
  const seenIds = new Set();
127
126
  let stopped = false;
128
127
 
129
- // Set up log file in the project directory
130
- const projectDir = this.main.firebaseProjectPath;
131
- const logPath = path.join(projectDir, 'functions', 'logs.log');
128
+ // Set up log file in the project's functions/ directory
129
+ const logPath = this.getLogsPath('logs.log');
132
130
  const logStream = fs.createWriteStream(logPath, { flags: 'w' });
133
131
 
134
132
  const filter = this.buildFilter(argv, { excludeTimestamp: true });
@@ -18,6 +18,10 @@ class ServeCommand extends BaseCommand {
18
18
  throw new Error('Port conflicts could not be resolved');
19
19
  }
20
20
 
21
+ // Wipe stale firebase-tools debug logs + any leftover BEM logs from older
22
+ // versions. Keeps the project tree clean across runs.
23
+ this.sweepStaleLogs();
24
+
21
25
  // Start BEM watcher in background
22
26
  const watcher = new WatchCommand(self);
23
27
  watcher.startBackground();
@@ -40,8 +44,8 @@ class ServeCommand extends BaseCommand {
40
44
  // lines on the first cycle (subsequent cycles route those elsewhere), so
41
45
  // we'd end up with a near-empty log. Rolling at the START of the cycle
42
46
  // lets us capture whatever firebase-tools does emit, complete-or-not.
43
- const logPath = path.join(projectDir, 'functions', 'serve.log');
44
- const resetSentinelPath = `${logPath}.reset`;
47
+ const logPath = this.getLogsPath('serve.log');
48
+ const resetSentinelPath = this.getTempPath('serve.log.reset');
45
49
  // Match any node version: "Using node@22 from host.", "Using node@20 from host.", etc.
46
50
  const RELOAD_MARKER = /Using node@\d+ from host\./;
47
51
  const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
@@ -104,8 +104,8 @@ class SetupCommand extends BaseCommand {
104
104
  throw new Error('Missing <engines.node> in package.json');
105
105
  }
106
106
 
107
- // Clean up leftover trigger files from watch command
108
- this.cleanupTriggerFiles();
107
+ // Clean up leftover trigger files + stale log files from older BEM versions
108
+ this.cleanupGeneratedArtifacts();
109
109
 
110
110
  // Copy / merge defaults into consumer project root (matches EM/BXM/UJM pattern).
111
111
  // Runs BEFORE tests so any test that inspects scaffolded files sees the merged state.
@@ -219,13 +219,19 @@ class SetupCommand extends BaseCommand {
219
219
  }
220
220
  }
221
221
 
222
- cleanupTriggerFiles() {
222
+ cleanupGeneratedArtifacts() {
223
223
  const self = this.main;
224
- const triggerFile = `${self.firebaseProjectPath}/functions/bem-reload-trigger.js`;
225
224
 
225
+ // Remove the BEM reload-trigger file (transient artifact from `npx mgr watch`)
226
+ const triggerFile = `${self.firebaseProjectPath}/functions/bem-reload-trigger.js`;
226
227
  if (jetpack.exists(triggerFile)) {
227
228
  jetpack.remove(triggerFile);
228
229
  }
230
+
231
+ // Sweep stale firebase-tools debug logs + leftover BEM logs from older
232
+ // versions (pre-5.2.2 they lived in functions/; now in .temp/). Shared
233
+ // implementation in base-command.js so emulator/serve boot also runs it.
234
+ this.sweepStaleLogs();
229
235
  }
230
236
 
231
237
  async runTests() {
@@ -199,8 +199,7 @@ class TestCommand extends BaseCommand {
199
199
  * log just won't be reset for this run.
200
200
  */
201
201
  async requestEmulatorLogReset(projectDir) {
202
- const logPath = path.join(projectDir, 'functions', 'emulator.log');
203
- const sentinelPath = `${logPath}.reset`;
202
+ const sentinelPath = this.getTempPath('emulator.log.reset');
204
203
 
205
204
  try {
206
205
  fs.writeFileSync(sentinelPath, '');
@@ -242,8 +241,8 @@ class TestCommand extends BaseCommand {
242
241
  this.log(chalk.gray(` Auth: 127.0.0.1:${emulatorPorts.auth}`));
243
242
  this.log(chalk.gray(` UI: http://127.0.0.1:${emulatorPorts.ui}`));
244
243
 
245
- // Set up log file in the project directory
246
- const logPath = path.join(projectDir, 'functions', 'test.log');
244
+ // Set up log file in the project's functions/ directory (alongside firebase-tools logs)
245
+ const logPath = this.getLogsPath('test.log');
247
246
  const logStream = fs.createWriteStream(logPath, { flags: 'w' });
248
247
  const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
249
248
 
@@ -60,8 +60,8 @@ class WatchCommand extends BaseCommand {
60
60
  // command isn't watching, the file is harmless and gets cleaned up by the next
61
61
  // boot's stale-sentinel sweep.
62
62
  const triggerFile = config.triggerFile;
63
- const serveLogResetPath = path.join(config.functionsDir, 'serve.log.reset');
64
- const emulatorLogResetPath = path.join(config.functionsDir, 'emulator.log.reset');
63
+ const serveLogResetPath = this.getTempPath('serve.log.reset');
64
+ const emulatorLogResetPath = this.getTempPath('emulator.log.reset');
65
65
  const nodemon = spawn(nodemonPath, [
66
66
  '--on-change-only',
67
67
  '--delay', '1',
@@ -104,8 +104,8 @@ class WatchCommand extends BaseCommand {
104
104
  // Use nodemon to watch the BEM src directory and touch the trigger file on changes.
105
105
  // Also drop <log>.reset sentinels so any sibling serve/emulator command rolls its
106
106
  // log file on each reload (mirrors the test runner's log-roll pattern).
107
- const serveLogResetPath = path.join(config.functionsDir, 'serve.log.reset');
108
- const emulatorLogResetPath = path.join(config.functionsDir, 'emulator.log.reset');
107
+ const serveLogResetPath = this.getTempPath('serve.log.reset');
108
+ const emulatorLogResetPath = this.getTempPath('emulator.log.reset');
109
109
  const nodemon = spawn(nodemonPath, [
110
110
  '--watch', config.bemSrcDir,
111
111
  '--ext', 'js,json',
package/templates/_.env CHANGED
@@ -1,6 +1,7 @@
1
1
  # ========== Default Values ==========
2
2
  # Backend Manager
3
3
  BACKEND_MANAGER_KEY=""
4
+ BACKEND_MANAGER_WEBHOOK_KEY=""
4
5
  BACKEND_MANAGER_NAMESPACE=""
5
6
  BACKEND_MANAGER_OPENAI_API_KEY=""
6
7
  BACKEND_MANAGER_ANTHROPIC_API_KEY=""
@@ -69,7 +69,8 @@ service cloud.firestore {
69
69
  || isWritingField('affiliate')
70
70
  || isWritingField('api')
71
71
  || isWritingField('metadata')
72
- || isWritingField('usage');
72
+ || isWritingField('usage')
73
+ || isWritingField('consent');
73
74
  }
74
75
 
75
76
  function isCreatingField(field) {
@@ -6,7 +6,7 @@
6
6
  * - Users can read their own document
7
7
  * - Users can write to their own document (non-protected fields only)
8
8
  * - Users cannot read/write other users' documents
9
- * - Protected fields (auth, roles, flags, subscription, affiliate, api, usage) cannot be written by users
9
+ * - Protected fields (auth, roles, flags, subscription, affiliate, api, metadata, usage, consent) cannot be written by users
10
10
  *
11
11
  * @see templates/firestore.rules
12
12
  */
@@ -234,6 +234,28 @@ module.exports = {
234
234
  },
235
235
  },
236
236
 
237
+ // Test 11.5: User cannot write 'consent' field (protected - server-only)
238
+ {
239
+ name: 'user-cannot-write-consent-field',
240
+ auth: 'none',
241
+
242
+ async run({ rules, accounts }) {
243
+ const uid = accounts.basic.uid;
244
+ const db = rules.asAccount('basic');
245
+
246
+ // Should fail - consent is protected (only signup route + webhook
247
+ // processors can mutate it server-side; a client write would let a
248
+ // user retroactively forge their own consent record).
249
+ await rules.expectFailure(
250
+ db.doc(`users/${uid}`).set({
251
+ consent: {
252
+ marketing: { status: 'granted' },
253
+ },
254
+ }, { merge: true })
255
+ );
256
+ },
257
+ },
258
+
237
259
  // Test 12: User cannot write to another user's document
238
260
  {
239
261
  name: 'user-cannot-write-other-doc',