backend-manager 5.11.7 → 5.13.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.
Files changed (23) hide show
  1. package/docs/ai-library.md +3 -1
  2. package/docs/email-system.md +4 -2
  3. package/docs/usage-rate-limiting.md +2 -0
  4. package/package.json +1 -1
  5. package/src/manager/functions/core/actions/api/admin/send-email.js +0 -8
  6. package/src/manager/helpers/usage.js +9 -2
  7. package/src/manager/libraries/ai/providers/test.js +15 -1
  8. package/src/manager/libraries/email/data/disposable-domains.json +342 -1
  9. package/src/manager/libraries/email/generators/lib/templates/plain.js +1 -2
  10. package/src/manager/libraries/email/prepare.js +3 -12
  11. package/src/manager/routes/user/signup/post.js +10 -28
  12. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/metadata.json +14 -0
  13. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/newsletter.html +486 -0
  14. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/newsletter.md +41 -0
  15. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/newsletter.mjml +97 -0
  16. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/structure.json +42 -0
  17. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/summary.md +1 -0
  18. package/src/test/fixtures/firebase-project/.temp/test-mode.json +1 -1
  19. package/src/test/fixtures/firebase-project/database-debug.log +8 -8
  20. package/src/test/fixtures/firebase-project/firestore-debug.log +61 -55
  21. package/src/test/fixtures/firebase-project/pubsub-debug.log +3 -3
  22. package/test/helpers/ai-test-provider.js +24 -0
  23. package/test/helpers/usage-limits.js +85 -0
@@ -96,7 +96,9 @@ URL citations live in the returned `output` (message content) as `annotations` o
96
96
 
97
97
  `provider: 'test'` is the AI analog of the `test` payment processor: a first-class provider that suites drive with directives in the LAST user message, so consumer routes exercise their full loop (Firestore writes, usage, locks, tool execution) against the real emulator with zero paid API calls. It **refuses to run outside development/testing**.
98
98
 
99
- Directives form a sequence consumed across loop turns (call N executes directive N-1, indexed by assistant turns after the last user turn). Directive values must not contain `]]` internally (a trailing JSON `]` is fine):
99
+ Directives form a sequence consumed across loop turns (call N executes directive N-1, indexed by assistant turns after the last user turn). Directive values must not contain `]]` internally (a trailing JSON `]` is fine).
100
+
101
+ The directive source is resolved in order: the last `messages[]` user turn → `message.content` → **`message.settings` values** (flattened raw). That last fallback makes path-based routes (`message: { path, settings }` — the idiomatic BEM prompt-template style) scriptable as-is: embed the directive in whatever request field gets interpolated into the template (e.g. a chat's `message`, a quiz's `topic`).
100
102
 
101
103
  | Directive | Behavior |
102
104
  |---|---|
@@ -33,7 +33,7 @@ Both transactional and marketing paths share the same preparation layer:
33
33
  | `resolveBrand(Manager)` | Clones brand config, sanitizes images (SVG→PNG via CDN naming) |
34
34
  | `resolveSender({ sender, from, group }, brand, brandDomain)` | Resolves sender from/display-name/ASM group by category key |
35
35
  | `renderContent({ content, html }, utmOptions)` | Markdown→HTML via markdown-it, applies UTM link tagging |
36
- | `resolveSignoff(signoff)` | Fills personal signoff defaults (headshot, name, URL) when `type: 'personal'` |
36
+ | `resolveSignoff(signoff)` | Defaults `type` to `'team'`; personal signoffs render only with caller-provided details (name/image/url) no built-in personal defaults |
37
37
  | `buildCategories(type, brandId, extra)` | Builds categories array: `['transactional', brandId, ...extra]` |
38
38
  | `buildUnsubscribeUrl({ email, groupId, template, websiteUrl })` | HMAC-signed one-click unsubscribe URL |
39
39
  | `buildTemplateData({ brand, subject, ... })` | Deep-merges system defaults with caller data into the template data tree |
@@ -96,11 +96,13 @@ await email.send({
96
96
  categories: ['account/welcome'],
97
97
  data: {
98
98
  content: { title: 'Welcome!', message: '# Hello!\n\nMarkdown here.' },
99
- signoff: { type: 'personal' },
99
+ signoff: { type: 'personal', name: 'Jane Doe, CEO', image: 'https://cdn.example.com/headshot.jpg', url: 'https://example.com', urlText: '@janedoe' },
100
100
  },
101
101
  });
102
102
  ```
103
103
 
104
+ A `personal` signoff without a `name` falls back to the team rendering — there are no built-in personal defaults.
105
+
104
106
  ### Per-template `data.content` shapes
105
107
 
106
108
  | Template | `data.content` fields |
@@ -27,6 +27,8 @@ For anonymous-to-owner billing, call `setUser()` BEFORE `validate()` — validat
27
27
 
28
28
  Limits are always specified as **monthly** values in product config (e.g., `limits.requests = 100` means 100/month).
29
29
 
30
+ **Negative limits are unlimited.** `limits.requests = -1` means the metric is never rate-limited for that product — `validate()` always resolves and no daily caps apply. A limit of `0` (or a metric missing from `limits`) always rejects.
31
+
30
32
  By default, limits are enforced with **daily caps** to prevent users from burning their entire monthly quota in a single day. Two checks are applied:
31
33
 
32
34
  1. **Flat daily cap**: `ceil(monthlyLimit / daysInMonth)` — max uses per day
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.11.7",
3
+ "version": "5.13.0",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "files": [
@@ -171,14 +171,6 @@ Module.prototype.defaultize = function () {
171
171
  email.dynamicTemplateData.signoff = options?.data?.signoff || {};
172
172
  email.dynamicTemplateData.signoff.type = options?.data?.signoff?.type || 'team';
173
173
 
174
- if (email.dynamicTemplateData.signoff.type === 'personal') {
175
- email.dynamicTemplateData.signoff.image = options?.data?.signoff?.image
176
- || 'https://cdn.itwcreativeworks.com/assets/ian-wiedenman/images/website/ian-wiedenman-headshot-2021-color-1024x1024.jpg';
177
- email.dynamicTemplateData.signoff.name = options?.data?.signoff?.name || 'Ian Wiedenman, CEO';
178
- email.dynamicTemplateData.signoff.url = options?.data?.signoff?.url || 'https://ianwiedenman.com';
179
- email.dynamicTemplateData.signoff.urlText = options?.data?.signoff?.urlText || '@ianwieds';
180
- }
181
-
182
174
  email.dynamicTemplateData.user = Manager.User(options.user).properties;
183
175
 
184
176
  // Get brand configuration from Manager.config.brand (backend-manager-config.json)
@@ -170,6 +170,13 @@ Usage.prototype.validate = function (name, options) {
170
170
  return _reject();
171
171
  }
172
172
 
173
+ // Negative limits are unlimited (product config convention: -1)
174
+ if (allowed < 0) {
175
+ self.log(`Usage.validate(): Unlimited limit (${allowed}) for ${name}`);
176
+
177
+ return resolve(true);
178
+ }
179
+
173
180
  // Check if they have a white list key
174
181
  const hasWhitelistKey = self.options.whitelistKeys.some((key) => key && key === self?.user?.api?.privateKey);
175
182
  if (hasWhitelistKey) {
@@ -367,9 +374,9 @@ Usage.prototype.getDailyAllowance = function (name) {
367
374
  return null;
368
375
  }
369
376
 
370
- // Get the monthly limit
377
+ // Get the monthly limit (negative limits are unlimited — no daily cap)
371
378
  const monthlyLimit = self.getLimit(name);
372
- if (!monthlyLimit) {
379
+ if (!monthlyLimit || monthlyLimit < 0) {
373
380
  return null;
374
381
  }
375
382
 
@@ -55,7 +55,12 @@ TestProvider.prototype.request = async function (options) {
55
55
 
56
56
  const messages = Array.isArray(options.messages) ? options.messages : [];
57
57
  const lastUserMessage = [...messages].reverse().find((m) => m.role === 'user' && typeof m.content === 'string');
58
- const scriptSource = lastUserMessage?.content || stringifyLoose(options.message?.content) || '';
58
+ // Path-based messages ({ path, settings }) are rendered inside the real
59
+ // providers, so directives arrive via the settings values — read them too.
60
+ const scriptSource = lastUserMessage?.content
61
+ || stringifyLoose(options.message?.content)
62
+ || stringifyLoose(options.message?.settings)
63
+ || '';
59
64
 
60
65
  const { steps, cleanText } = parseScript(scriptSource);
61
66
 
@@ -230,6 +235,15 @@ function stringifyLoose(content) {
230
235
  return content.map((c) => c?.text || '').join('\n');
231
236
  }
232
237
 
238
+ // Plain objects (e.g. prompt-template settings) flatten to their raw values
239
+ // so directives embedded in them stay parseable (JSON.stringify would escape
240
+ // the quotes inside a directive's JSON payload)
241
+ if (content && typeof content === 'object') {
242
+ return Object.values(content)
243
+ .map((value) => (typeof value === 'string' ? value : `${value}`))
244
+ .join('\n');
245
+ }
246
+
233
247
  return content ? String(content) : '';
234
248
  }
235
249