backend-manager 5.0.193 → 5.0.194

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,15 @@ 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.0.194] - 2026-04-08
18
+ ### Fixed
19
+ - Fix email template data merge: caller's `settings.data` is now deep-merged at root of template data tree, removing the broken `data.` prefix indirection that caused empty order confirmation emails since 5.0.185
20
+ ### Added
21
+ - `preview` as a top-level setting on `email.send()` (alongside `subject`)
22
+ - `logs:read` CLI: `--search`, `--order`, `--filter` flags and increased default limit to 300
23
+ ### Changed
24
+ - Email templates now access caller data at root (`{{order.id}}`, `{{body.message}}`) instead of under `data.*`
25
+
17
26
  # [5.0.192] - 2026-04-02
18
27
  ### Added
19
28
  - Setup test to create `hooks/auth/` and `hooks/cron/daily/` directories in consumer projects during `npx bm setup`
package/CLAUDE.md CHANGED
@@ -736,6 +736,8 @@ The forwarding URL is: `http://localhost:{hostingPort}/backend-manager/payments/
736
736
 
737
737
  Quick commands for reading/writing Firestore and managing Auth users directly from the terminal. Works in any BEM consumer project (requires `functions/service-account.json` for production, or `--emulator` for local).
738
738
 
739
+ **IMPORTANT: All CLI commands (`npx mgr ...` / `npx bm ...`) MUST be run from the consumer project's `functions/` subdirectory** (e.g., `cd /path/to/my-project/functions && npx mgr ...`). The `mgr` binary lives in `functions/node_modules/.bin/` — running from the project root or any other directory will fail.
740
+
739
741
  ### Firestore Commands
740
742
 
741
743
  ```bash
@@ -763,24 +765,63 @@ npx bm auth:set-claims <uid-or-email> '<json>' # Set custom claims
763
765
  Fetch or stream Cloud Function logs from Google Cloud Logging. Requires `gcloud` CLI installed and authenticated. Auto-resolves the project ID from `service-account.json`, `.firebaserc`, or `GCLOUD_PROJECT`.
764
766
 
765
767
  ```bash
766
- npx bm logs:read # Read last 1h of logs (default: 50 entries)
768
+ npx bm logs:read # Read last 1h of logs (default: 300 entries, newest first)
767
769
  npx bm logs:read --fn bm_api # Filter by function name
768
770
  npx bm logs:read --fn bm_api --severity ERROR # Filter by severity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
769
771
  npx bm logs:read --since 2d --limit 100 # Custom time range and limit
772
+ npx bm logs:read --search "72.134.242.25" # Search textPayload for a string (IP, email, error, etc.)
773
+ npx bm logs:read --fn bm_authBeforeCreate --search "ian@example.com" --since 7d # Combined filters
774
+ npx bm logs:read --order asc # Oldest first (default: desc/newest first)
775
+ npx bm logs:read --filter 'jsonPayload.level="error"' # Raw gcloud filter passthrough
770
776
  npx bm logs:tail # Stream live logs
771
777
  npx bm logs:tail --fn bm_paymentsWebhookOnWrite # Stream filtered live logs
772
778
  ```
773
779
 
774
780
  Both commands save output to `functions/logs.log` (overwritten on each run). `logs:read` saves raw JSON; `logs:tail` streams text.
775
781
 
782
+ **Cloud Logs vs Local Logs:** These commands query **production** Google Cloud Logging. For **local/dev** logs, read `functions/serve.log` (from `npx bm serve`) or `functions/emulator.log` (from `npx bm test`) directly — they are plain text files, not gcloud.
783
+
776
784
  | Flag | Description | Default | Commands |
777
785
  |------|-------------|---------|----------|
778
- | `--fn <name>` | Filter by Cloud Function name | all | both |
779
- | `--severity <level>` | Minimum severity level | all | both |
786
+ | `--fn <name>` | Filter by Cloud Function name (see table below) | all | both |
787
+ | `--severity <level>` | Minimum severity: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | all | both |
788
+ | `--search <text>` | Search textPayload for a substring (IP, email, uid, error message) | none | both |
789
+ | `--filter <expr>` | Raw gcloud logging filter expression (appended to built-in filters) | none | both |
780
790
  | `--since <duration>` | Time range (`30m`, `1h`, `2d`, `1w`) | `1h` | read only |
781
- | `--limit <n>` | Max entries | `50` | read only |
791
+ | `--limit <n>` | Max entries | `300` | read only |
792
+ | `--order <dir>` | Sort order: `asc` (oldest first) or `desc` (newest first) | `desc` | read only |
793
+ | `--interval <sec>` | Polling interval in seconds | `5` | tail only |
782
794
  | `--raw` | Output raw JSON | false | both |
783
795
 
796
+ #### `--fn` Function Name Reference
797
+
798
+ The `--fn` flag uses the **deployed Cloud Function name**, not the route path.
799
+
800
+ **BEM built-in functions (always deployed):**
801
+
802
+ | Function name | Type | Description |
803
+ |---------------|------|-------------|
804
+ | `bm_api` | HTTPS | Main API router — all consumer routes (GET/POST/PUT/DELETE) go through this |
805
+ | `bm_authBeforeCreate` | Auth blocking | Before user creation: disposable email blocking, IP rate limiting, consumer hooks |
806
+ | `bm_authBeforeSignIn` | Auth blocking | Before sign-in: consumer hooks |
807
+ | `bm_authOnCreate` | Auth event | After user creation: user doc setup |
808
+ | `bm_authOnDelete` | Auth event | After user deletion |
809
+ | `bm_paymentsWebhookOnWrite` | Firestore trigger | Processes payment webhooks |
810
+ | `bm_paymentsDisputeOnWrite` | Firestore trigger | Processes payment disputes |
811
+ | `bm_notificationsOnWrite` | Firestore trigger | Sends push notifications |
812
+ | `bm_cronDaily` | Scheduled | Daily cron (midnight UTC) |
813
+ | `bm_cronFrequent` | Scheduled | Frequent cron (every 10 min) |
814
+
815
+ **Consumer-defined functions** use the export name from `functions/index.js` (e.g., `exports.items = ...` → `--fn items`).
816
+
817
+ **Quick lookup — which function to query:**
818
+ - API route errors → `--fn bm_api`
819
+ - Signup/auth blocked → `--fn bm_authBeforeCreate`
820
+ - Sign-in issues → `--fn bm_authBeforeSignIn`
821
+ - User doc not created → `--fn bm_authOnCreate`
822
+ - Payment not processing → `--fn bm_paymentsWebhookOnWrite`
823
+ - Cron job issues → `--fn bm_cronDaily` or `--fn bm_cronFrequent`
824
+
784
825
  ### Shared Flags
785
826
 
786
827
  | Flag | Description |
package/TODO-2.md CHANGED
@@ -23,6 +23,26 @@ waht about when they request a cancel
23
23
  Read cancellation-requested.js
24
24
  The category is order/cancellation-requested (line 13).
25
25
 
26
+ ----
27
+ add a dedicated BEM JSON field for usage to reset
28
+ * this way we can have clear LIMITS with their definitions like
29
+ * [
30
+ {
31
+ name: 'credits'
32
+ reset: true,
33
+ },
34
+ {
35
+ name: 'agents',
36
+ reset: false,
37
+ }
38
+ ]
39
+ * mirrors: [
40
+ {
41
+ collection: 'agents',
42
+ fields: ['usage.credits.daily', 'runs.replies.daily],
43
+ }
44
+ ]
45
+
26
46
  ---
27
47
  MIRROR settigns in BEM JSON so that usage reset can properly get MIRRED DOCS liek slapform forms or chatsy agents DOCS
28
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.193",
3
+ "version": "5.0.194",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -56,11 +56,12 @@ class LogsCommand extends BaseCommand {
56
56
 
57
57
  /**
58
58
  * Fetch historical logs.
59
- * Usage: npx bm logs:read [--fn bm_api] [--severity ERROR] [--since 1h] [--limit 300]
59
+ * Usage: npx bm logs:read [--fn bm_api] [--severity ERROR] [--since 1h] [--limit 300] [--search "text"] [--order desc] [--filter 'raw gcloud filter']
60
60
  */
61
61
  async read(projectId, argv) {
62
62
  const filter = this.buildFilter(argv);
63
63
  const limit = parseInt(argv.limit, 10) || 300;
64
+ const order = argv.order || 'desc';
64
65
 
65
66
  const cmd = [
66
67
  'gcloud', 'logging', 'read',
@@ -68,7 +69,7 @@ class LogsCommand extends BaseCommand {
68
69
  `--project=${projectId}`,
69
70
  `--limit=${limit}`,
70
71
  '--format=json',
71
- '--order=asc',
72
+ `--order=${order}`,
72
73
  ].filter(Boolean).join(' ');
73
74
 
74
75
  // Set up log file in the project directory
@@ -83,7 +84,7 @@ class LogsCommand extends BaseCommand {
83
84
  const output = execSync(cmd, {
84
85
  encoding: 'utf8',
85
86
  maxBuffer: 10 * 1024 * 1024, // 10MB
86
- timeout: 30000,
87
+ timeout: 60000,
87
88
  });
88
89
 
89
90
  const entries = JSON.parse(output || '[]');
@@ -233,6 +234,16 @@ class LogsCommand extends BaseCommand {
233
234
  parts.push(`severity>=${argv.severity.toUpperCase()}`);
234
235
  }
235
236
 
237
+ // Text search filter (searches textPayload)
238
+ if (argv.search) {
239
+ parts.push(`textPayload:"${argv.search}"`);
240
+ }
241
+
242
+ // Raw filter passthrough (appended as-is)
243
+ if (argv.filter) {
244
+ parts.push(argv.filter);
245
+ }
246
+
236
247
  // Timestamp filter (read only, not tail)
237
248
  if (!options.excludeTimestamp) {
238
249
  const since = argv.since || '1h';
@@ -142,6 +142,8 @@ Transactional.prototype.build = async function (settings) {
142
142
  throw errorWithCode('Parameter subject is required', 400);
143
143
  }
144
144
 
145
+ const preview = settings.preview || settings?.data?.email?.preview || null;
146
+
145
147
  const templateId = TEMPLATES[settings.template] || settings.template || TEMPLATES['default'];
146
148
 
147
149
  // Resolve sender category
@@ -176,7 +178,7 @@ Transactional.prototype.build = async function (settings) {
176
178
  const unsubSig = crypto.createHmac('sha256', process.env.UNSUBSCRIBE_HMAC_KEY).update(to[0].email.toLowerCase()).digest('hex');
177
179
  const unsubscribeUrl = `${Manager.project.websiteUrl}/portal/email-preferences?email=${encode(to[0].email)}&asmId=${encode(groupId)}&templateId=${encode(templateId)}&sig=${unsubSig}`;
178
180
 
179
- // Build signoff
181
+ // Build signoff defaults
180
182
  const signoff = settings?.data?.signoff || {};
181
183
  signoff.type = signoff.type || 'team';
182
184
 
@@ -188,12 +190,12 @@ Transactional.prototype.build = async function (settings) {
188
190
  signoff.urlText = signoff.urlText || '@ianwieds';
189
191
  }
190
192
 
191
- // Build dynamic template data defaults
193
+ // Build dynamic template data — system-generated defaults
192
194
  const dynamicTemplateData = {
193
195
  email: {
194
196
  id: Manager.require('uuid').v4(),
195
197
  subject,
196
- preview: null,
198
+ preview,
197
199
  body: null,
198
200
  unsubscribeUrl,
199
201
  categories,
@@ -209,18 +211,20 @@ Transactional.prototype.build = async function (settings) {
209
211
  signoff,
210
212
  brand: brandData,
211
213
  user: userProperties,
212
- data: {},
213
214
  };
214
215
 
215
- // Deep-merge caller's data on top so they can override any field
216
- // (e.g. email.preview, email.subject, personalization.name, data.body.*, etc.)
216
+ // Deep-merge caller's data on top of defaults.
217
+ // This is the single template data tree — everything the template can access.
218
+ // Callers can override any field (email.preview, signoff.type, etc.)
219
+ // and add custom data (order.*, body.*, abandonedCart.*, etc.) at the root.
220
+ // Templates access all fields at the root: {{order.id}}, {{email.preview}}, {{brand.name}}.
217
221
  if (settings.data) {
218
222
  _.merge(dynamicTemplateData, settings.data);
219
223
  }
220
224
 
221
- // Process markdown in body fields (after merge so all data paths are resolved)
222
- if (dynamicTemplateData.data?.body?.message) {
223
- dynamicTemplateData.data.body.message = md.render(dynamicTemplateData.data.body.message);
225
+ // Process markdown in body fields (after merge so caller data is resolved)
226
+ if (dynamicTemplateData.body?.message) {
227
+ dynamicTemplateData.body.message = md.render(dynamicTemplateData.body.message);
224
228
  }
225
229
  if (dynamicTemplateData.email?.body) {
226
230
  dynamicTemplateData.email.body = md.render(dynamicTemplateData.email.body);
@@ -235,8 +239,8 @@ Transactional.prototype.build = async function (settings) {
235
239
  utm: settings.utm,
236
240
  };
237
241
 
238
- if (dynamicTemplateData.data?.body?.message) {
239
- dynamicTemplateData.data.body.message = tagLinks(dynamicTemplateData.data.body.message, utmOptions);
242
+ if (dynamicTemplateData.body?.message) {
243
+ dynamicTemplateData.body.message = tagLinks(dynamicTemplateData.body.message, utmOptions);
240
244
  }
241
245
  if (dynamicTemplateData.email?.body) {
242
246
  dynamicTemplateData.email.body = tagLinks(dynamicTemplateData.email.body, utmOptions);