backend-manager 5.0.202 → 5.1.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 (68) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CLAUDE.md +43 -1501
  3. package/docs/admin-post-route.md +24 -0
  4. package/docs/ai-library.md +23 -0
  5. package/docs/architecture.md +31 -0
  6. package/docs/auth-hooks.md +74 -0
  7. package/docs/cli-firestore-auth.md +59 -0
  8. package/docs/cli-logs.md +67 -0
  9. package/docs/code-patterns.md +67 -0
  10. package/docs/common-operations.md +64 -0
  11. package/docs/directory-structure.md +119 -0
  12. package/docs/environment-detection.md +7 -0
  13. package/docs/file-naming.md +11 -0
  14. package/docs/marketing-campaigns.md +244 -0
  15. package/docs/marketing-fields.md +25 -0
  16. package/docs/mcp.md +95 -0
  17. package/docs/payment-system.md +325 -0
  18. package/docs/response-headers.md +7 -0
  19. package/docs/routes.md +126 -0
  20. package/docs/sanitization.md +61 -0
  21. package/docs/schemas.md +39 -0
  22. package/docs/stripe-webhook-forwarding.md +18 -0
  23. package/docs/testing.md +129 -0
  24. package/docs/usage-rate-limiting.md +67 -0
  25. package/package.json +8 -4
  26. package/src/defaults/CHANGELOG.md +15 -0
  27. package/src/defaults/CLAUDE.md +8 -4
  28. package/src/defaults/docs/README.md +17 -0
  29. package/src/defaults/test/README.md +33 -0
  30. package/src/manager/events/cron/daily/marketing-newsletter-generate.js +48 -8
  31. package/src/manager/functions/core/actions/api/admin/create-post.js +3 -27
  32. package/src/manager/helpers/settings.js +26 -7
  33. package/src/manager/helpers/utilities.js +21 -0
  34. package/src/manager/index.js +1 -1
  35. package/src/manager/libraries/ai/index.js +162 -0
  36. package/src/manager/libraries/ai/providers/anthropic.js +193 -0
  37. package/src/manager/libraries/ai/providers/claude-code.js +206 -0
  38. package/src/manager/libraries/ai/providers/openai.js +934 -0
  39. package/src/manager/libraries/disposable-domains.json +2 -0
  40. package/src/manager/libraries/email/generators/lib/filter.js +179 -0
  41. package/src/manager/libraries/email/generators/lib/image-host.js +231 -0
  42. package/src/manager/libraries/email/generators/lib/mjml-template.js +83 -0
  43. package/src/manager/libraries/email/generators/lib/structure.js +278 -0
  44. package/src/manager/libraries/email/generators/lib/svg-illustrator.js +184 -0
  45. package/src/manager/libraries/email/generators/lib/templates/classic-schema.js +63 -0
  46. package/src/manager/libraries/email/generators/lib/templates/clean.js +82 -0
  47. package/src/manager/libraries/email/generators/lib/templates/editorial-helpers.js +100 -0
  48. package/src/manager/libraries/email/generators/lib/templates/editorial.js +317 -0
  49. package/src/manager/libraries/email/generators/lib/templates/field-report-helpers.js +138 -0
  50. package/src/manager/libraries/email/generators/lib/templates/field-report.js +497 -0
  51. package/src/manager/libraries/email/generators/lib/templates/index.js +28 -0
  52. package/src/manager/libraries/email/generators/lib/templates/shared.js +534 -0
  53. package/src/manager/libraries/email/generators/newsletter.js +377 -95
  54. package/src/manager/libraries/email/marketing/index.js +5 -2
  55. package/src/manager/libraries/email/providers/beehiiv.js +7 -3
  56. package/src/manager/libraries/openai.js +13 -932
  57. package/src/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
  58. package/src/manager/routes/admin/post/post.js +10 -17
  59. package/templates/_.env +4 -0
  60. package/templates/_.gitignore +1 -0
  61. package/templates/backend-manager-config.json +48 -4
  62. package/test/helpers/slugify.js +394 -0
  63. package/test/marketing/fixtures/clean.json +31 -0
  64. package/test/marketing/fixtures/editorial.json +31 -0
  65. package/test/marketing/fixtures/field-report.json +54 -0
  66. package/test/marketing/newsletter-generate.js +731 -0
  67. package/test/marketing/newsletter-templates.js +512 -0
  68. package/test/routes/admin/deduplicate-image-alts.js +190 -0
@@ -0,0 +1,325 @@
1
+ # Payment System
2
+
3
+ This document covers the full payment system: pipeline architecture, subscription model + statuses, transition handlers, processor interface, product configuration, and the test processor.
4
+
5
+ ## Pipeline
6
+
7
+ The payment system follows a linear pipeline: **Intent → Webhook → On-Write → Transition**.
8
+
9
+ 1. **Intent** (`POST /payments/intent`): Client requests a payment session. BEM validates the product, generates an order ID (`XXXX-XXXX-XXXX`), and delegates to the processor module (e.g., Stripe creates a Checkout Session). Saves to `payments-intents/{orderId}`.
10
+
11
+ 2. **Webhook** (`POST /payments/webhook?processor=X&key=Y`): Processor sends event data. BEM parses and categorizes the event (`subscription` or `one-time`), extracts the UID, and saves to `payments-webhooks/{eventId}` with `status: 'pending'`.
12
+
13
+ 3. **On-Write** (Firestore trigger on `payments-webhooks/{eventId}`): Fetches the latest resource from the processor API (not stale webhook data), transforms it into a unified object, detects state transitions, dispatches handlers, tracks analytics, and writes to `users/{uid}.subscription` (subscriptions) and `payments-orders/{orderId}`.
14
+
15
+ 4. **Transitions** (fire-and-forget): Handler files run asynchronously after detection. Failures never block webhook processing. Skipped during tests unless `TEST_EXTENDED_MODE` is set.
16
+
17
+ ## 3-Layer Architecture
18
+
19
+ The payment system is cleanly separated into three independent layers:
20
+
21
+ | Layer | Purpose | Tests |
22
+ |-------|---------|-------|
23
+ | **Processor input** (Stripe, PayPal, Test) | Parse raw webhooks + transform to unified shape | Helper tests per processor (`payment/stripe/to-unified-subscription.js`, `payment/paypal/to-unified-one-time.js`, etc.) |
24
+ | **Unified pipeline** (processor-agnostic) | Transition detection, Firestore writes, analytics | Journey tests (`journey-payments-*.js`) |
25
+ | **Transition handlers** (fire-and-forget) | Emails, notifications, side effects | Skipped during tests unless `TEST_EXTENDED_MODE` |
26
+
27
+ Each processor transforms its raw data into the **same unified shape**. Once data enters the pipeline, the code doesn't know or care which processor it came from. This means:
28
+ - Adding a new processor = implement the processor interface (below). The pipeline handles the rest.
29
+ - Journey tests use the `test` processor but exercise the full unified pipeline end-to-end.
30
+ - Processor-specific tests only need to verify correct transformation to the unified shape.
31
+
32
+ ## Subscription Statuses
33
+
34
+ | Status | Meaning | User can delete account? |
35
+ |--------|---------|--------------------------|
36
+ | `active` | Subscription is current and valid (includes trialing) | No (unless `product.id === 'basic'`) |
37
+ | `suspended` | Payment failed (Stripe: `past_due`, `unpaid`) | No |
38
+ | `cancelled` | Subscription terminated (Stripe: `canceled`, `incomplete`, `incomplete_expired`) | Yes |
39
+
40
+ ### Stripe Status Mapping
41
+
42
+ | Stripe Status | `subscription.status` | Notes |
43
+ |---|---|---|
44
+ | `active` | `active` | Normal active subscription |
45
+ | `trialing` | `active` | `trial.claimed = true` |
46
+ | `past_due` | `suspended` | Payment failed, retrying |
47
+ | `unpaid` | `suspended` | Payment failed |
48
+ | `canceled` | `cancelled` | Subscription terminated |
49
+ | `incomplete` | `cancelled` | Never completed initial payment |
50
+ | `incomplete_expired` | `cancelled` | Expired before completion |
51
+ | `active` + `cancel_at_period_end` | `active` | `cancellation.pending = true` |
52
+
53
+ ## Unified Subscription Object (`users/{uid}.subscription`)
54
+
55
+ ```javascript
56
+ subscription: {
57
+ product: {
58
+ id: 'basic', // product ID from config ('basic', 'premium', etc.)
59
+ name: 'Basic', // display name from config
60
+ },
61
+ status: 'active', // 'active' | 'suspended' | 'cancelled'
62
+ expires: { timestamp, timestampUNIX },
63
+ trial: {
64
+ claimed: false, // has user EVER used a trial
65
+ expires: { timestamp, timestampUNIX },
66
+ },
67
+ cancellation: {
68
+ pending: false, // true = cancel at period end
69
+ date: { timestamp, timestampUNIX },
70
+ },
71
+ payment: {
72
+ processor: null, // 'stripe' | 'paypal' | etc.
73
+ orderId: null, // BEM order ID (e.g., '1234-5678-9012')
74
+ resourceId: null, // provider subscription ID (e.g., 'sub_xxx')
75
+ frequency: null, // 'monthly' | 'annually' | 'weekly' | 'daily'
76
+ price: 0, // resolved from config (number, e.g., 4.99)
77
+ startDate: { timestamp, timestampUNIX },
78
+ updatedBy: {
79
+ event: { name: null, id: null },
80
+ date: { timestamp, timestampUNIX },
81
+ },
82
+ },
83
+ }
84
+ ```
85
+
86
+ ## Access Check Patterns
87
+
88
+ ```javascript
89
+ // Is premium (paid)?
90
+ user.subscription.status === 'active' && user.subscription.product.id !== 'basic'
91
+
92
+ // Is on trial?
93
+ user.subscription.trial.claimed && user.subscription.status === 'active'
94
+
95
+ // Has pending cancellation?
96
+ user.subscription.cancellation.pending === true
97
+
98
+ // Payment failed?
99
+ user.subscription.status === 'suspended'
100
+ ```
101
+
102
+ ## resolveSubscription(account)
103
+
104
+ `User.resolveSubscription(account)` is a static method on the User helper that derives calculated subscription fields from raw account data. It returns only fields that require derivation logic — raw data (product.id, status, trial, cancellation) lives on the account object directly.
105
+
106
+ ```javascript
107
+ const User = require('backend-manager/src/manager/helpers/user');
108
+
109
+ const resolved = User.resolveSubscription(account);
110
+ // Returns: { plan, active, trialing, cancelling }
111
+ ```
112
+
113
+ | Field | Type | Description |
114
+ |-------|------|-------------|
115
+ | `plan` | `string` | Effective plan ID the user has access to RIGHT NOW (`'basic'` if cancelled/suspended) |
116
+ | `active` | `boolean` | User has active access (active, trialing, or cancelling) |
117
+ | `trialing` | `boolean` | In an active trial (status `'active'` + `trial.claimed` + unexpired `trial.expires`) |
118
+ | `cancelling` | `boolean` | Cancellation pending (status `'active'` + `cancellation.pending` + NOT trialing) |
119
+
120
+ Accepts either a raw Firestore account object or a resolved `User` instance (checks both `account.subscription` and `account.properties.subscription`).
121
+
122
+ **Unified with web-manager**: The same function exists as `auth.resolveSubscription(account)` in web-manager (`modules/auth.js`) with identical logic and return shape.
123
+
124
+ **Use this instead of manual access checks** — it centralizes all the derivation logic in one place:
125
+
126
+ ```javascript
127
+ // ✅ PREFERRED — use resolveSubscription
128
+ const resolved = User.resolveSubscription(user);
129
+ if (resolved.active) { /* has access */ }
130
+
131
+ // ❌ AVOID — manual checks that duplicate logic
132
+ if (user.subscription.status === 'active' && user.subscription.product.id !== 'basic') { /* ... */ }
133
+ ```
134
+
135
+ ## Transition Handlers
136
+
137
+ When a webhook changes a subscription or processes a one-time payment, BEM detects the state transition and dispatches to a handler file. Handlers are fire-and-forget (non-blocking) — they run after the transition is detected but before or during the Firestore writes. Handler failures never block webhook processing.
138
+
139
+ Handlers are skipped during tests unless `TEST_EXTENDED_MODE` is set.
140
+
141
+ ### Transition Detection
142
+
143
+ The `transitions/index.js` module compares the **before** state (current `users/{uid}.subscription`) with the **after** state (new unified subscription) to detect what changed.
144
+
145
+ ### Subscription Transitions
146
+
147
+ | Transition | Before → After | File |
148
+ |---|---|---|
149
+ | `new-subscription` | basic/null → active paid | `transitions/subscription/new-subscription.js` |
150
+ | `payment-failed` | active → suspended | `transitions/subscription/payment-failed.js` |
151
+ | `payment-recovered` | suspended → active | `transitions/subscription/payment-recovered.js` |
152
+ | `cancellation-requested` | pending=false → pending=true | `transitions/subscription/cancellation-requested.js` |
153
+ | `subscription-cancelled` | non-cancelled → cancelled | `transitions/subscription/subscription-cancelled.js` |
154
+ | `plan-changed` | active product A → active product B | `transitions/subscription/plan-changed.js` |
155
+
156
+ Note: Trials are NOT a separate transition. The `new-subscription` handler checks `after.trial.claimed` to determine if the subscription started with a trial.
157
+
158
+ ### One-Time Transitions
159
+
160
+ | Transition | Event Type | File |
161
+ |---|---|---|
162
+ | `purchase-completed` | `checkout.session.completed` | `transitions/one-time/purchase-completed.js` |
163
+ | `purchase-failed` | `invoice.payment_failed` | `transitions/one-time/purchase-failed.js` |
164
+
165
+ ### Handler Interface
166
+
167
+ All handlers are in `src/manager/events/firestore/payments-webhooks/transitions/` and export a single async function:
168
+
169
+ ```javascript
170
+ module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
171
+ // before: previous subscription state (null for new/one-time)
172
+ // after: new unified state (subscription or one-time)
173
+ // userDoc: full user document data
174
+ // eventType: original webhook event type (e.g., 'customer.subscription.updated')
175
+ // eventId: webhook event ID
176
+ };
177
+ ```
178
+
179
+ ### Creating a New Transition Handler
180
+
181
+ 1. Add detection logic in `transitions/index.js` (in priority order)
182
+ 2. Create handler file in `transitions/{category}/{name}.js`
183
+ 3. Handler receives full context — use `assistant.log()` for logging, `Manager.project.apiUrl` for API calls
184
+
185
+ ## Processor Interface
186
+
187
+ Each processor implements three modules:
188
+
189
+ **Intent processor** (`routes/payments/intent/processors/{processor}.js`):
190
+
191
+ ```javascript
192
+ module.exports = {
193
+ async createIntent({ uid, orderId, product, productId, frequency, trial, confirmationUrl, cancelUrl, Manager, assistant }) {
194
+ return { id, url, raw };
195
+ },
196
+ };
197
+ ```
198
+
199
+ **Webhook processor** (`routes/payments/webhook/processors/{processor}.js`):
200
+
201
+ ```javascript
202
+ module.exports = {
203
+ isSupported(eventType) { return boolean; },
204
+ parseWebhook(req) { return { eventId, eventType, category, resourceType, resourceId, raw, uid }; },
205
+ };
206
+ ```
207
+
208
+ **Cancel processor** (`routes/payments/cancel/processors/{processor}.js`):
209
+
210
+ ```javascript
211
+ module.exports = {
212
+ async cancelAtPeriodEnd({ resourceId, uid, subscription, assistant }) { /* cancel at end of period */ },
213
+ };
214
+ ```
215
+
216
+ **Refund processor** (`routes/payments/refund/processors/{processor}.js`):
217
+
218
+ ```javascript
219
+ module.exports = {
220
+ async processRefund({ resourceId, uid, subscription, assistant }) {
221
+ return { amount, currency, full };
222
+ },
223
+ };
224
+ ```
225
+
226
+ **Portal processor** (`routes/payments/portal/processors/{processor}.js`):
227
+
228
+ ```javascript
229
+ module.exports = {
230
+ async createPortalSession({ resourceId, uid, returnUrl, assistant }) {
231
+ return { url };
232
+ },
233
+ };
234
+ ```
235
+
236
+ **Shared library** (`libraries/payment/processors/{processor}.js`):
237
+
238
+ ```javascript
239
+ module.exports = {
240
+ init() { /* return SDK instance */ },
241
+ async fetchResource(resourceType, resourceId, rawFallback, context) { /* return resource */ },
242
+ getOrderId(resource) { /* return orderId string or null */ },
243
+ toUnifiedSubscription(rawSubscription, options) { /* return unified object */ },
244
+ toUnifiedOneTime(rawResource, options) { /* return unified object */ },
245
+ };
246
+ ```
247
+
248
+ ## Product Resolution
249
+
250
+ Products are resolved differently per processor, but always end up matching a product in `config.payment.products`:
251
+
252
+ | Processor | Resolution chain | Stable ID |
253
+ |-----------|-----------------|-----------|
254
+ | **Stripe** | `sub.items.data[0].price.product` or `raw.plan.product` → match `product.stripe.productId` or `legacyProductIds` | `prod_xxx` |
255
+ | **PayPal** | `sub → plan_id → plan → product_id` → match `product.paypal.productId` | PayPal catalog product ID |
256
+ | **Test** | Uses `product.stripe.productId` in Stripe-shaped data | Same as Stripe |
257
+
258
+ Falls back to `{ id: 'basic' }` if no match found.
259
+
260
+ ## Processor-Specific Details
261
+
262
+ **Stripe:** Uses `metadata.uid` and `metadata.orderId` on subscriptions for UID/order resolution.
263
+
264
+ **PayPal:** Uses `custom_id` field on subscriptions with format `uid:{uid},orderId:{orderId}`. Product resolution fetches the plan from the subscription, then gets `product_id` from the plan. Plans are scoped by `product_id` query param to avoid cross-brand matches on shared PayPal accounts.
265
+
266
+ ## Product Configuration
267
+
268
+ Products are defined in `backend-manager-config.json` under `payment.products`:
269
+
270
+ ```javascript
271
+ payment: {
272
+ processors: {
273
+ stripe: { publishableKey: 'pk_live_...' },
274
+ paypal: { clientId: 'ARvf...' },
275
+ },
276
+ products: [
277
+ {
278
+ id: 'basic', // Free tier (no prices, no processor keys)
279
+ name: 'Basic',
280
+ type: 'subscription',
281
+ limits: { requests: 100 },
282
+ },
283
+ {
284
+ id: 'premium', // Paid subscription
285
+ name: 'Premium',
286
+ type: 'subscription',
287
+ limits: { requests: 1000 },
288
+ trial: { days: 14 },
289
+ prices: { monthly: 4.99, annually: 49.99 }, // Flat numbers; also supports 'weekly' and 'daily'
290
+ stripe: { productId: 'prod_xxx', legacyProductIds: ['prod_OLD'] },
291
+ paypal: { productId: 'PROD-abc123' },
292
+ },
293
+ {
294
+ id: 'credits-100', // One-time purchase
295
+ name: '100 Credits',
296
+ type: 'one-time',
297
+ prices: { once: 9.99 },
298
+ stripe: { productId: 'prod_yyy' },
299
+ paypal: { productId: null },
300
+ },
301
+ ],
302
+ }
303
+ ```
304
+
305
+ Key rules:
306
+ - `prices` contains **flat numbers only** — no processor-specific IDs
307
+ - Processor IDs live at the product level: `stripe: { productId }`, `paypal: { productId }`
308
+ - `stripe.productId` is stable — never changes even when prices change
309
+ - `stripe.legacyProductIds` maps old pre-migration Stripe products to this product
310
+ - Price IDs (Stripe `price_xxx`, PayPal plan IDs) are **resolved at runtime** by matching amount + interval against active prices on the processor's product
311
+ - `basic` product has no `prices` and no processor keys — it's the free tier
312
+ - `archived: true` stops offering a product to new subscribers while keeping it resolvable for existing ones
313
+
314
+ ## Firestore Collections
315
+
316
+ | Collection | Key | Purpose |
317
+ |---|---|---|
318
+ | `payments-intents/{orderId}` | Order ID | Intent metadata (processor, product, status) |
319
+ | `payments-webhooks/{eventId}` | Processor event ID | Webhook processing state + transition result |
320
+ | `payments-orders/{orderId}` | Order ID | Unified order data (single source of truth for orders) |
321
+ | `users/{uid}.subscription` | User UID | Current subscription state (subscriptions only) |
322
+
323
+ ## Test Processor
324
+
325
+ The `test` processor generates Stripe-shaped data and auto-fires webhooks to the local server. Only available in non-production environments. Use `processor: 'test'` in intent requests during testing. The test webhook processor delegates to Stripe's parser since it generates Stripe-shaped payloads.
@@ -0,0 +1,7 @@
1
+ # Response Headers
2
+
3
+ BEM automatically sets `bm-properties` header with:
4
+ - `code`: HTTP status code
5
+ - `tag`: Function name and execution ID
6
+ - `usage`: Current usage stats
7
+ - `schema`: Resolved schema info
package/docs/routes.md ADDED
@@ -0,0 +1,126 @@
1
+ # Creating Routes, API Commands, Events, Cron Jobs
2
+
3
+ Recipes for building consumer-side routes plus BEM-side API commands, event handlers, and cron jobs. See also [docs/schemas.md](schemas.md) for schema definitions, [docs/auth-hooks.md](auth-hooks.md) for auth lifecycle hooks, and [docs/common-operations.md](common-operations.md) for inside-the-handler patterns.
4
+
5
+ ## New API Command
6
+
7
+ Create `src/manager/functions/core/actions/api/{category}/{action}.js`:
8
+
9
+ ```javascript
10
+ function Module() {}
11
+
12
+ Module.prototype.main = function () {
13
+ const self = this;
14
+ const Manager = self.Manager;
15
+ const Api = self.Api;
16
+ const assistant = self.assistant;
17
+ const payload = self.payload;
18
+
19
+ return new Promise(async function(resolve, reject) {
20
+ // Validate input
21
+ if (!payload.data.payload.requiredField) {
22
+ return reject(assistant.errorify('Missing required field', { code: 400 }));
23
+ }
24
+
25
+ // Business logic here
26
+ const result = { success: true };
27
+
28
+ // Log and return
29
+ assistant.log('Action completed', result);
30
+ return resolve({ data: result });
31
+ });
32
+ };
33
+
34
+ module.exports = Module;
35
+ ```
36
+
37
+ ## New Route (Consumer Project)
38
+
39
+ Create `routes/{name}/index.js`:
40
+
41
+ ```javascript
42
+ function Route() {}
43
+
44
+ Route.prototype.main = async function (assistant) {
45
+ const Manager = assistant.Manager;
46
+ const usage = assistant.usage;
47
+ const user = assistant.usage.user;
48
+ const analytics = assistant.analytics;
49
+ const settings = assistant.settings;
50
+
51
+ // Check authentication if needed
52
+ if (!user.authenticated) {
53
+ return assistant.respond('Authentication required', { code: 401 });
54
+ }
55
+
56
+ // Track usage
57
+ await usage.validate('requests');
58
+ usage.increment('requests');
59
+ await usage.update();
60
+
61
+ // Send response
62
+ assistant.respond({ success: true, data: settings });
63
+ };
64
+
65
+ module.exports = Route;
66
+ ```
67
+
68
+ ## New Event Handler
69
+
70
+ Create `src/manager/functions/core/events/{type}/{event}.js`:
71
+
72
+ ```javascript
73
+ function Module() {}
74
+
75
+ Module.prototype.init = function (Manager, payload) {
76
+ const self = this;
77
+ self.Manager = Manager;
78
+ self.assistant = Manager.Assistant();
79
+ self.libraries = Manager.libraries;
80
+ self.user = payload.user;
81
+ self.context = payload.context;
82
+ return self;
83
+ };
84
+
85
+ Module.prototype.main = function () {
86
+ const self = this;
87
+ const Manager = self.Manager;
88
+ const assistant = self.assistant;
89
+
90
+ return new Promise(async function(resolve, reject) {
91
+ const { admin } = self.libraries;
92
+
93
+ assistant.log('Event triggered', self.user);
94
+
95
+ // Event logic here
96
+
97
+ return resolve(self);
98
+ });
99
+ };
100
+
101
+ module.exports = Module;
102
+ ```
103
+
104
+ ## New Cron Job (Consumer Project)
105
+
106
+ Create `hooks/cron/daily/{job}.js`:
107
+
108
+ ```javascript
109
+ function Job() {}
110
+
111
+ Job.prototype.main = function () {
112
+ const self = this;
113
+ const Manager = self.Manager;
114
+ const assistant = self.assistant;
115
+
116
+ return new Promise(async function(resolve, reject) {
117
+ assistant.log('Running daily job...');
118
+
119
+ // Job logic here
120
+
121
+ return resolve();
122
+ });
123
+ };
124
+
125
+ module.exports = Job;
126
+ ```
@@ -0,0 +1,61 @@
1
+ # Sanitization (XSS Prevention)
2
+
3
+ BEM automatically sanitizes all incoming request data — stripping HTML tags and trimming whitespace from every string field. This happens in the middleware pipeline before route handlers execute, so **routes receive clean data by default**.
4
+
5
+ ## How It Works
6
+
7
+ 1. **Schema fields**: Sanitized per-field during the middleware pipeline. Fields can opt out with `sanitize: false` in the schema.
8
+ 2. **Non-schema fields** (when `setupSettings: false` or `includeNonSchemaSettings: true`): All strings are sanitized with no opt-out.
9
+ 3. The middleware uses `Manager.Utilities().sanitize()` under the hood.
10
+
11
+ ## Schema Opt-Out
12
+
13
+ For fields that legitimately need HTML (rich text, email templates, etc.), set `sanitize: false` in the schema:
14
+
15
+ ```javascript
16
+ // This field will NOT be sanitized — raw HTML is preserved
17
+ htmlContent: {
18
+ types: ['string'],
19
+ default: '',
20
+ sanitize: false,
21
+ },
22
+ // This field IS sanitized (default behavior, no flag needed)
23
+ name: {
24
+ types: ['string'],
25
+ default: '',
26
+ },
27
+ ```
28
+
29
+ ## Route-Level Opt-Out
30
+
31
+ Disable sanitization entirely for a route (rare — only for routes that handle raw HTML everywhere):
32
+
33
+ ```javascript
34
+ // In functions/index.js
35
+ Manager.Middleware(req, res).run('my-route', { sanitize: false });
36
+ ```
37
+
38
+ ## Manual Sanitization (Outside Middleware)
39
+
40
+ For cron jobs, event handlers, or anywhere outside the request pipeline, use `utilities.sanitize()` directly:
41
+
42
+ ```javascript
43
+ // Available in route context
44
+ const clean = utilities.sanitize(untrustedData);
45
+
46
+ // Or via Manager
47
+ const clean = Manager.Utilities().sanitize(untrustedData);
48
+ ```
49
+
50
+ Accepts any data type — strings, objects, arrays, primitives. Walks objects/arrays recursively, strips HTML from strings, passes everything else through unchanged.
51
+
52
+ ## Route Handler Context
53
+
54
+ The middleware injects these into every route handler:
55
+
56
+ ```javascript
57
+ module.exports = async ({ Manager, assistant, analytics, usage, user, settings, libraries, utilities }) => {
58
+ // settings — already sanitized by middleware
59
+ // utilities — Manager.Utilities() instance for manual sanitization
60
+ };
61
+ ```
@@ -0,0 +1,39 @@
1
+ # Schemas
2
+
3
+ Schemas define and validate the payload your routes accept. Schema names should match the route name (e.g. route `myEndpoint` ↔ schema `myEndpoint`).
4
+
5
+ ## New Schema (Consumer Project)
6
+
7
+ Create `schemas/{name}/index.js`:
8
+
9
+ ```javascript
10
+ module.exports = function (assistant, settings, options) {
11
+ const user = options.user;
12
+
13
+ return {
14
+ defaults: {
15
+ fieldName: {
16
+ types: ['string'],
17
+ default: 'default value',
18
+ required: false,
19
+ },
20
+ numericField: {
21
+ types: ['number'],
22
+ default: 10,
23
+ min: 1,
24
+ max: 100,
25
+ },
26
+ },
27
+ // Override for premium users
28
+ premium: {
29
+ numericField: {
30
+ max: 1000,
31
+ },
32
+ },
33
+ };
34
+ };
35
+ ```
36
+
37
+ ## Field Sanitization
38
+
39
+ By default, every string field in a schema is sanitized (HTML tags stripped, whitespace trimmed) during the middleware pipeline. To preserve raw HTML (rich text, email templates), set `sanitize: false` on the field. See [docs/sanitization.md](sanitization.md).
@@ -0,0 +1,18 @@
1
+ # Stripe Webhook Forwarding
2
+
3
+ BEM auto-starts Stripe CLI webhook forwarding when running `npx mgr serve` or `npx mgr emulator`. This forwards Stripe test webhooks to the local server so the full payment pipeline works end-to-end during development.
4
+
5
+ **Requirements:**
6
+ - `STRIPE_SECRET_KEY` set in `functions/.env`
7
+ - `BACKEND_MANAGER_KEY` set in `functions/.env`
8
+ - [Stripe CLI](https://stripe.com/docs/stripe-cli) installed
9
+
10
+ **Standalone usage:**
11
+
12
+ ```bash
13
+ npx mgr stripe
14
+ ```
15
+
16
+ If any prerequisite is missing, webhook forwarding is silently skipped with an info message.
17
+
18
+ The forwarding URL is: `http://localhost:{hostingPort}/backend-manager/payments/webhook?processor=stripe&key={BACKEND_MANAGER_KEY}`