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.
- package/docs/ai-library.md +3 -1
- package/docs/email-system.md +4 -2
- package/docs/usage-rate-limiting.md +2 -0
- package/package.json +1 -1
- package/src/manager/functions/core/actions/api/admin/send-email.js +0 -8
- package/src/manager/helpers/usage.js +9 -2
- package/src/manager/libraries/ai/providers/test.js +15 -1
- package/src/manager/libraries/email/data/disposable-domains.json +342 -1
- package/src/manager/libraries/email/generators/lib/templates/plain.js +1 -2
- package/src/manager/libraries/email/prepare.js +3 -12
- package/src/manager/routes/user/signup/post.js +10 -28
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/metadata.json +14 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/newsletter.html +486 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/newsletter.md +41 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/newsletter.mjml +97 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/structure.json +42 -0
- package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-08-05T01-21-40/summary.md +1 -0
- package/src/test/fixtures/firebase-project/.temp/test-mode.json +1 -1
- package/src/test/fixtures/firebase-project/database-debug.log +8 -8
- package/src/test/fixtures/firebase-project/firestore-debug.log +61 -55
- package/src/test/fixtures/firebase-project/pubsub-debug.log +3 -3
- package/test/helpers/ai-test-provider.js +24 -0
- package/test/helpers/usage-limits.js +85 -0
package/docs/ai-library.md
CHANGED
|
@@ -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
|
|---|---|
|
package/docs/email-system.md
CHANGED
|
@@ -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)` |
|
|
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
|
@@ -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
|
-
|
|
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
|
|