backend-manager 5.0.182 → 5.0.184
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 +5 -0
- package/CLAUDE.md +94 -0
- package/package.json +1 -1
- package/src/manager/cron/frequent/abandoned-carts.js +1 -1
- package/src/manager/events/firestore/payments-disputes/on-write.js +1 -1
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +1 -1
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +1 -1
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +1 -1
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +1 -1
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +1 -1
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-refunded.js +1 -1
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +1 -1
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +1 -1
- package/src/manager/functions/core/actions/api/general/emails/general:download-app-link.js +1 -1
- package/src/manager/libraries/email/constants.js +22 -15
- package/src/manager/routes/general/email/templates/download-app-link.js +1 -1
- package/src/manager/routes/user/signup/post.js +1 -1
- package/src/mcp/handler.js +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,11 @@ 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.184] - 2026-03-31
|
|
18
|
+
### Changed
|
|
19
|
+
- Renamed email template shortcuts from `main/` to `core/` prefix across constants and all consumer files
|
|
20
|
+
- Added new templates: `core/plain` and `core/marketing/promotional`
|
|
21
|
+
|
|
17
22
|
# [5.0.177] - 2026-03-29
|
|
18
23
|
### Changed
|
|
19
24
|
- `payment-recovered` transition now sends email to internal team only — customer no longer receives a "Payment received" notification
|
package/CLAUDE.md
CHANGED
|
@@ -1278,6 +1278,100 @@ assistant.isProduction() // true when ENVIRONMENT === 'production'
|
|
|
1278
1278
|
assistant.isTesting() // true when running tests (via npx bm test)
|
|
1279
1279
|
```
|
|
1280
1280
|
|
|
1281
|
+
## Model Context Protocol (MCP)
|
|
1282
|
+
|
|
1283
|
+
BEM includes a built-in MCP server that exposes BEM routes as tools for Claude Chat, Claude Code, and other MCP clients.
|
|
1284
|
+
|
|
1285
|
+
### Architecture
|
|
1286
|
+
|
|
1287
|
+
Two transport modes:
|
|
1288
|
+
- **Stdio** (local): `npx bm mcp` — for Claude Code / Claude Desktop
|
|
1289
|
+
- **Streamable HTTP** (remote): `POST /backend-manager/mcp` — for Claude Chat (stateless, Firebase Functions compatible)
|
|
1290
|
+
|
|
1291
|
+
### Available Tools (19)
|
|
1292
|
+
|
|
1293
|
+
| Tool | Route | Description |
|
|
1294
|
+
|------|-------|-------------|
|
|
1295
|
+
| `firestore_read` | `GET /admin/firestore` | Read a Firestore document by path |
|
|
1296
|
+
| `firestore_write` | `POST /admin/firestore` | Write/merge a Firestore document |
|
|
1297
|
+
| `firestore_query` | `POST /admin/firestore/query` | Query a collection with where/orderBy/limit |
|
|
1298
|
+
| `send_email` | `POST /admin/email` | Send transactional email via SendGrid |
|
|
1299
|
+
| `send_notification` | `POST /admin/notification` | Send push notification via FCM |
|
|
1300
|
+
| `get_user` | `GET /user` | Get authenticated user info |
|
|
1301
|
+
| `get_subscription` | `GET /user/subscription` | Get subscription info for a user |
|
|
1302
|
+
| `sync_users` | `POST /admin/users/sync` | Sync user data across systems |
|
|
1303
|
+
| `list_campaigns` | `GET /marketing/campaign` | List marketing campaigns |
|
|
1304
|
+
| `create_campaign` | `POST /marketing/campaign` | Create a marketing campaign |
|
|
1305
|
+
| `get_stats` | `GET /admin/stats` | Get system statistics |
|
|
1306
|
+
| `cancel_subscription` | `POST /payments/cancel` | Cancel subscription at period end |
|
|
1307
|
+
| `refund_payment` | `POST /payments/refund` | Process a refund |
|
|
1308
|
+
| `run_cron` | `POST /admin/cron` | Trigger a cron job by ID |
|
|
1309
|
+
| `create_post` | `POST /admin/post` | Create a blog post |
|
|
1310
|
+
| `create_backup` | `POST /admin/backup` | Create a Firestore backup |
|
|
1311
|
+
| `run_hook` | `POST /admin/hook` | Execute a custom hook |
|
|
1312
|
+
| `generate_uuid` | `POST /general/uuid` | Generate a UUID |
|
|
1313
|
+
| `health_check` | `GET /test/health` | Check server health |
|
|
1314
|
+
|
|
1315
|
+
### Authentication
|
|
1316
|
+
|
|
1317
|
+
- **Stdio (local):** Reads `BACKEND_MANAGER_KEY` from `functions/.env` automatically
|
|
1318
|
+
- **HTTP (remote):** OAuth 2.1 Authorization Code flow with PKCE. Claude Chat handles the flow — user pastes BEM key once on the authorize page. If `OAuth Client ID` is set to the BEM key in the connector config, the authorize step auto-approves.
|
|
1319
|
+
|
|
1320
|
+
### Hosting Rewrites
|
|
1321
|
+
|
|
1322
|
+
The `npx bm setup` command automatically adds required Firebase Hosting rewrites for MCP OAuth:
|
|
1323
|
+
```json
|
|
1324
|
+
{
|
|
1325
|
+
"source": "{/backend-manager,/backend-manager/**,/.well-known/oauth-protected-resource,/.well-known/oauth-authorization-server,/authorize,/token}",
|
|
1326
|
+
"function": "bm_api"
|
|
1327
|
+
}
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
### CLI Usage
|
|
1331
|
+
|
|
1332
|
+
```bash
|
|
1333
|
+
npx bm mcp # Start stdio MCP server (for Claude Code)
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
### Claude Code Configuration
|
|
1337
|
+
|
|
1338
|
+
Add to `.claude/settings.json`:
|
|
1339
|
+
```json
|
|
1340
|
+
{
|
|
1341
|
+
"mcpServers": {
|
|
1342
|
+
"backend-manager": {
|
|
1343
|
+
"command": "npx",
|
|
1344
|
+
"args": ["bm", "mcp"],
|
|
1345
|
+
"cwd": "/path/to/consumer-project"
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
### Claude Chat Configuration
|
|
1352
|
+
|
|
1353
|
+
1. Go to Settings → Custom Connectors → Add
|
|
1354
|
+
2. **URL:** `https://api.yourdomain.com/backend-manager/mcp`
|
|
1355
|
+
3. **OAuth Client ID:** your `BACKEND_MANAGER_KEY` (enables auto-approve)
|
|
1356
|
+
4. **OAuth Client Secret:** your `BACKEND_MANAGER_KEY`
|
|
1357
|
+
|
|
1358
|
+
### Key Files
|
|
1359
|
+
|
|
1360
|
+
| Purpose | File |
|
|
1361
|
+
|---------|------|
|
|
1362
|
+
| Tool definitions | `src/mcp/tools.js` |
|
|
1363
|
+
| HTTP handler (stateless + OAuth) | `src/mcp/handler.js` |
|
|
1364
|
+
| Stdio server | `src/mcp/index.js` |
|
|
1365
|
+
| HTTP client | `src/mcp/client.js` |
|
|
1366
|
+
| CLI command | `src/cli/commands/mcp.js` |
|
|
1367
|
+
| MCP route interception | `src/manager/index.js` (`_handleMcp`, `resolveMcpRoutePath`) |
|
|
1368
|
+
| Hosting rewrites setup | `src/cli/commands/setup-tests/hosting-rewrites.js` |
|
|
1369
|
+
|
|
1370
|
+
### Adding New Tools
|
|
1371
|
+
|
|
1372
|
+
1. Add the tool definition to `src/mcp/tools.js` with `name`, `description`, `method`, `path`, and `inputSchema`
|
|
1373
|
+
2. The tool automatically maps to the corresponding BEM route via the HTTP client — no handler code needed
|
|
1374
|
+
|
|
1281
1375
|
## Response Headers
|
|
1282
1376
|
|
|
1283
1377
|
BEM automatically sets `bm-properties` header with:
|
package/package.json
CHANGED
|
@@ -71,7 +71,7 @@ module.exports = async ({ Manager, assistant, context, libraries }) => {
|
|
|
71
71
|
email.send({
|
|
72
72
|
sender: 'marketing',
|
|
73
73
|
to: userDoc,
|
|
74
|
-
template: '
|
|
74
|
+
template: 'core/order/abandoned-cart',
|
|
75
75
|
subject: `Complete your ${brandName} ${productName} checkout`,
|
|
76
76
|
categories: ['order/abandoned-cart', `order/abandoned-cart/reminder-${reminderIndex + 1}`],
|
|
77
77
|
copy: false,
|
|
@@ -225,7 +225,7 @@ function sendDisputeEmail({ alert, match, result, alertId, assistant }) {
|
|
|
225
225
|
sender: 'internal',
|
|
226
226
|
to: brandEmail,
|
|
227
227
|
subject: subject,
|
|
228
|
-
template: '
|
|
228
|
+
template: 'core/card',
|
|
229
229
|
categories: ['order/dispute-alert'],
|
|
230
230
|
copy: true,
|
|
231
231
|
data: {
|
package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js
CHANGED
|
@@ -16,7 +16,7 @@ module.exports = async function ({ before, after, order, uid, userDoc, assistant
|
|
|
16
16
|
assistant.log(`Transition [one-time/purchase-completed]: uid=${uid}, resourceId=${after.payment?.resourceId}, discount=${hasPromoDiscount ? discount.code : 'none'}`);
|
|
17
17
|
|
|
18
18
|
sendOrderEmail({
|
|
19
|
-
template: '
|
|
19
|
+
template: 'core/order/confirmation',
|
|
20
20
|
subject: `Your ${brandName} ${productName} order #${order?.id || ''}`,
|
|
21
21
|
categories: ['order/confirmation'],
|
|
22
22
|
userDoc,
|
|
@@ -8,7 +8,7 @@ module.exports = async function ({ before, after, order, uid, userDoc, assistant
|
|
|
8
8
|
assistant.log(`Transition [subscription/cancellation-requested]: uid=${uid}, product=${after.product?.id}, cancelDate=${after.cancellation?.date?.timestamp}`);
|
|
9
9
|
|
|
10
10
|
sendOrderEmail({
|
|
11
|
-
template: '
|
|
11
|
+
template: 'core/order/cancellation-requested',
|
|
12
12
|
subject: `Your cancellation is confirmed #${order?.id || ''}`,
|
|
13
13
|
categories: ['order/cancellation-requested'],
|
|
14
14
|
userDoc,
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js
CHANGED
|
@@ -18,7 +18,7 @@ module.exports = async function ({ before, after, order, uid, userDoc, assistant
|
|
|
18
18
|
assistant.log(`Transition [subscription/new-subscription]: uid=${uid}, product=${after.product?.id}, frequency=${after.payment?.frequency}, trial=${isTrial}, discount=${hasPromoDiscount ? discount.code : 'none'}`);
|
|
19
19
|
|
|
20
20
|
sendOrderEmail({
|
|
21
|
-
template: '
|
|
21
|
+
template: 'core/order/confirmation',
|
|
22
22
|
subject: `Your ${brandName} ${planName} order #${order?.id || ''}`,
|
|
23
23
|
categories: ['order/confirmation'],
|
|
24
24
|
userDoc,
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js
CHANGED
|
@@ -8,7 +8,7 @@ module.exports = async function ({ before, after, order, uid, userDoc, assistant
|
|
|
8
8
|
assistant.log(`Transition [subscription/payment-failed]: uid=${uid}, product=${after.product?.id}, previousStatus=${before?.status}`);
|
|
9
9
|
|
|
10
10
|
sendOrderEmail({
|
|
11
|
-
template: '
|
|
11
|
+
template: 'core/order/payment-failed',
|
|
12
12
|
subject: `Payment failed for order #${order?.id || ''}`,
|
|
13
13
|
categories: ['order/payment-failed'],
|
|
14
14
|
userDoc,
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js
CHANGED
|
@@ -8,7 +8,7 @@ module.exports = async function ({ before, after, order, uid, userDoc, assistant
|
|
|
8
8
|
assistant.log(`Transition [subscription/payment-recovered]: uid=${uid}, product=${after.product?.id}`);
|
|
9
9
|
|
|
10
10
|
sendOrderEmail({
|
|
11
|
-
template: '
|
|
11
|
+
template: 'core/order/payment-recovered',
|
|
12
12
|
subject: `Payment received for order #${order?.id || ''}`,
|
|
13
13
|
categories: ['order/payment-recovered'],
|
|
14
14
|
internalOnly: true,
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-refunded.js
CHANGED
|
@@ -14,7 +14,7 @@ module.exports = async function ({ before, after, order, uid, userDoc, assistant
|
|
|
14
14
|
assistant.log(`Transition [subscription/payment-refunded]: uid=${uid}, product=${after?.product?.id}, amount=${refundDetails?.amount} ${refundDetails?.currency}, reason=${refundDetails?.reason || 'none'}`);
|
|
15
15
|
|
|
16
16
|
sendOrderEmail({
|
|
17
|
-
template: '
|
|
17
|
+
template: 'core/order/refunded',
|
|
18
18
|
subject: `Your payment has been refunded #${order?.id || ''}`,
|
|
19
19
|
categories: ['order/refunded'],
|
|
20
20
|
userDoc,
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js
CHANGED
|
@@ -9,7 +9,7 @@ module.exports = async function ({ before, after, order, uid, userDoc, assistant
|
|
|
9
9
|
assistant.log(`Transition [subscription/plan-changed]: uid=${uid}, ${before.product?.id} → ${after.product?.id} (${direction})`);
|
|
10
10
|
|
|
11
11
|
sendOrderEmail({
|
|
12
|
-
template: '
|
|
12
|
+
template: 'core/order/plan-changed',
|
|
13
13
|
subject: `Your plan has been updated #${order?.id || ''}`,
|
|
14
14
|
categories: ['order/plan-changed'],
|
|
15
15
|
userDoc,
|
|
@@ -13,7 +13,7 @@ module.exports = async function ({ before, after, order, uid, userDoc, assistant
|
|
|
13
13
|
const hasFutureExpiry = !isTrial && after.expires?.timestamp && new Date(after.expires.timestamp) > new Date();
|
|
14
14
|
|
|
15
15
|
sendOrderEmail({
|
|
16
|
-
template: '
|
|
16
|
+
template: 'core/order/cancelled',
|
|
17
17
|
subject: `Your subscription has been cancelled #${order?.id || ''}`,
|
|
18
18
|
categories: ['order/cancelled'],
|
|
19
19
|
userDoc,
|
|
@@ -13,7 +13,7 @@ module.exports = function (payload, config) {
|
|
|
13
13
|
sender: 'marketing',
|
|
14
14
|
categories: ['download'],
|
|
15
15
|
subject: `Free ${config.brand.name} download link for ${payload.name || 'you'}!`,
|
|
16
|
-
template: '
|
|
16
|
+
template: 'core/misc/app-download-link',
|
|
17
17
|
copy: false,
|
|
18
18
|
data: {},
|
|
19
19
|
}
|
|
@@ -8,23 +8,30 @@
|
|
|
8
8
|
// Template shortcut map — callers use readable paths instead of SendGrid IDs
|
|
9
9
|
// Paths mirror the email website structure: {category}/{subcategory}/{name}
|
|
10
10
|
const TEMPLATES = {
|
|
11
|
-
//
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
11
|
+
// Default templates
|
|
12
|
+
'core/card': 'd-1cd2eee44b6340268c964cd7971d49b9',
|
|
13
|
+
'core/plain': 'd-1d99985c1f0e40ff99d130c94047b080',
|
|
14
|
+
|
|
15
|
+
// Global templates
|
|
16
|
+
'core/engagement/feedback': 'd-319ab5c9d5074b21926a93562d6f41f6',
|
|
17
|
+
'core/misc/app-download-link': 'd-fc8b4834d7e1472896fe7e46152029f4',
|
|
18
|
+
'core/marketing/promotional': 'd-5fbaf210b0aa498e9167dfd8ae8e08d0',
|
|
19
|
+
'core/order/confirmation': 'd-5371ac2b4e3b490bbce51bfc2922ece8',
|
|
20
|
+
'core/order/payment-failed': 'd-e56af0ac62364bfb9e50af02854e2cd3',
|
|
21
|
+
'core/order/payment-recovered': 'd-d6dbd17a260a4755b34a852ba09c2454',
|
|
22
|
+
'core/order/cancellation-requested': 'd-78074f3e8c844146bf263b86fc8d5ecf',
|
|
23
|
+
'core/order/cancelled': 'd-39041132e6b24e5ebf0e95bce2d94dba',
|
|
24
|
+
'core/order/plan-changed': 'd-399086311bbb48b4b77bc90b20fb9d0a',
|
|
25
|
+
'core/order/trial-ending': 'd-af8ab499cbfb4d56918b4118f44343b0',
|
|
26
|
+
'core/order/refunded': 'd-aa47fdbffa2b4ca9b73b6256e963e49f',
|
|
27
|
+
'core/order/abandoned-cart': 'd-d8b3fa67e2b44b398dc280d0576bf1b7',
|
|
28
|
+
|
|
29
|
+
// Brand-specific templates are NOT registered here.
|
|
30
|
+
// Each consuming project should hardcode its own brand-specific template IDs directly.
|
|
24
31
|
};
|
|
25
32
|
|
|
26
|
-
// "default" resolves to the
|
|
27
|
-
TEMPLATES['default'] = TEMPLATES['
|
|
33
|
+
// "default" resolves to the default card template
|
|
34
|
+
TEMPLATES['default'] = TEMPLATES['core/card'];
|
|
28
35
|
|
|
29
36
|
// Group shortcut map — SendGrid ASM group IDs
|
|
30
37
|
// Rename these in SendGrid dashboard to match the comments
|
|
@@ -17,7 +17,7 @@ module.exports = function (payload, config) {
|
|
|
17
17
|
sender: 'marketing',
|
|
18
18
|
categories: ['download'],
|
|
19
19
|
subject: `Free ${config.brand.name} download link for ${payload.name || 'you'}!`,
|
|
20
|
-
template: '
|
|
20
|
+
template: 'core/misc/app-download-link',
|
|
21
21
|
copy: false,
|
|
22
22
|
data: {},
|
|
23
23
|
}
|
|
@@ -378,7 +378,7 @@ function sendFeedbackEmail(assistant, uid) {
|
|
|
378
378
|
sender: 'hello',
|
|
379
379
|
categories: ['engagement/feedback'],
|
|
380
380
|
subject: `Want to share your feedback about ${Manager.config.brand.name}?`,
|
|
381
|
-
template: '
|
|
381
|
+
template: 'core/engagement/feedback',
|
|
382
382
|
copy: false,
|
|
383
383
|
sendAt: moment().add(10, 'days').unix(),
|
|
384
384
|
})
|
package/src/mcp/handler.js
CHANGED
|
@@ -93,7 +93,7 @@ function handleAuthorize(req, res, options) {
|
|
|
93
93
|
const Manager = options.Manager;
|
|
94
94
|
|
|
95
95
|
// Auto-approve if client_id matches the BEM key
|
|
96
|
-
if (isValidKey(client_id
|
|
96
|
+
if (isValidKey(client_id) && redirect_uri) {
|
|
97
97
|
const url = new URL(redirect_uri);
|
|
98
98
|
url.searchParams.set('code', client_id);
|
|
99
99
|
if (state) {
|
|
@@ -151,7 +151,7 @@ function handleAuthorize(req, res, options) {
|
|
|
151
151
|
const redirectUri = body.redirect_uri || '';
|
|
152
152
|
const postState = body.state || '';
|
|
153
153
|
|
|
154
|
-
if (!isValidKey(key
|
|
154
|
+
if (!isValidKey(key)) {
|
|
155
155
|
res.writeHead(403, { 'Content-Type': 'text/html' });
|
|
156
156
|
res.end('<html><body style="background:#111;color:#e55;font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh"><h2>Invalid key. Go back and try again.</h2></body></html>');
|
|
157
157
|
return;
|
|
@@ -188,7 +188,7 @@ function handleToken(req, res, options) {
|
|
|
188
188
|
const Manager = options.Manager;
|
|
189
189
|
|
|
190
190
|
// The code, client_secret, or client_id IS the backendManagerKey — validate any
|
|
191
|
-
if (!isValidKey(code
|
|
191
|
+
if (!isValidKey(code)) {
|
|
192
192
|
return sendJson(res, 401, {
|
|
193
193
|
error: 'invalid_grant',
|
|
194
194
|
error_description: 'Invalid authorization code.',
|
|
@@ -213,7 +213,7 @@ async function handleMcpProtocol(req, res, options) {
|
|
|
213
213
|
const authHeader = req.headers.authorization || '';
|
|
214
214
|
const key = authHeader.replace(/^Bearer\s+/i, '');
|
|
215
215
|
|
|
216
|
-
if (!isValidKey(key
|
|
216
|
+
if (!isValidKey(key)) {
|
|
217
217
|
// Return 401 with OAuth discovery hint
|
|
218
218
|
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'https';
|
|
219
219
|
const host = req.headers['x-forwarded-host'] || req.headers.host || '';
|
|
@@ -321,8 +321,8 @@ async function handleMcpProtocol(req, res, options) {
|
|
|
321
321
|
* Validate a key against the configured backendManagerKey.
|
|
322
322
|
* Returns false if either the key or the config key is empty/missing.
|
|
323
323
|
*/
|
|
324
|
-
function isValidKey(key
|
|
325
|
-
const configKey =
|
|
324
|
+
function isValidKey(key) {
|
|
325
|
+
const configKey = process.env.BACKEND_MANAGER_KEY || '';
|
|
326
326
|
return !!key && !!configKey && key === configKey;
|
|
327
327
|
}
|
|
328
328
|
|