backend-manager 5.9.5 → 5.9.7

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 (246) hide show
  1. package/package.json +8 -1
  2. package/src/manager/events/cron/runner.js +0 -1
  3. package/src/manager/libraries/email/data/disposable-domains.json +21 -0
  4. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/metadata.json +14 -0
  5. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.html +486 -0
  6. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.md +41 -0
  7. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.mjml +97 -0
  8. package/{test/email/fixtures/clean.json → src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/structure.json} +16 -4
  9. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/summary.md +1 -0
  10. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/metadata.json +14 -0
  11. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.html +486 -0
  12. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.md +41 -0
  13. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.mjml +97 -0
  14. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/structure.json +42 -0
  15. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/summary.md +1 -0
  16. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/metadata.json +14 -0
  17. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.html +486 -0
  18. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.md +41 -0
  19. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.mjml +97 -0
  20. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/structure.json +42 -0
  21. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/summary.md +1 -0
  22. package/src/test/fixtures/firebase-project/.temp/test-mode.json +6 -0
  23. package/src/test/fixtures/firebase-project/database-debug.log +12 -0
  24. package/src/test/fixtures/firebase-project/firestore-debug.log +132 -0
  25. package/src/test/fixtures/firebase-project/pubsub-debug.log +17 -0
  26. package/.codeclimate.yml +0 -32
  27. package/.nvmrc +0 -1
  28. package/CHANGELOG.md +0 -1440
  29. package/PROGRESS.md +0 -93
  30. package/TODO-2.md +0 -121
  31. package/TODO-CHARGEBLAST.md +0 -32
  32. package/TODO-WEBHOOK-KEY-LEGACY-REMOVAL.md +0 -15
  33. package/TODO-WEBHOOK-KEY-UPGRADE.md +0 -138
  34. package/TODO-email-auth.md +0 -14
  35. package/TODO.md +0 -187
  36. package/plans/mcp2.md +0 -247
  37. package/plans/suspended-subscription-dead-zone.md +0 -97
  38. package/scripts/test-helper-providers.js +0 -162
  39. package/scripts/update-disposable-domains.js +0 -44
  40. package/templates/_.env +0 -42
  41. package/templates/_.gitignore +0 -60
  42. package/templates/backend-manager-config.json +0 -249
  43. package/templates/database.rules.json +0 -83
  44. package/templates/firebase.json +0 -66
  45. package/templates/firestore.indexes.json +0 -4
  46. package/templates/firestore.rules +0 -112
  47. package/templates/public/404.html +0 -26
  48. package/templates/public/index.html +0 -24
  49. package/templates/remoteconfig.template.json +0 -1
  50. package/templates/storage-lifecycle-config-1-day.json +0 -9
  51. package/templates/storage-lifecycle-config-30-days.json +0 -9
  52. package/templates/storage.rules +0 -8
  53. package/test/_init/accounts-validation.js +0 -58
  54. package/test/_legacy/ai/index.js +0 -231
  55. package/test/_legacy/ai/test.jpg +0 -0
  56. package/test/_legacy/ai/test.pdf +0 -0
  57. package/test/_legacy/index.js +0 -31
  58. package/test/_legacy/payment-resolver/chargebee/orders/refunded.json +0 -0
  59. package/test/_legacy/payment-resolver/chargebee/orders/unpaid.json +0 -98
  60. package/test/_legacy/payment-resolver/chargebee/subscriptions/active.json +0 -578
  61. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-in-trial.json +0 -38
  62. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-skipped-to-active.json +0 -135
  63. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active-to-cancelled.json +0 -42
  64. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active.json +0 -44
  65. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-cancelled.json +0 -39
  66. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-refund.json +0 -143
  67. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-suspended.json +0 -48
  68. package/test/_legacy/payment-resolver/coinbase/orders/regular.json +0 -204
  69. package/test/_legacy/payment-resolver/coinbase/subscriptions/cancelled.json +0 -204
  70. package/test/_legacy/payment-resolver/coinbase/subscriptions/paid-2.json +0 -125
  71. package/test/_legacy/payment-resolver/index.js +0 -1558
  72. package/test/_legacy/payment-resolver/paypal/orders/refunded.json +0 -0
  73. package/test/_legacy/payment-resolver/paypal/orders/regular.json +0 -120
  74. package/test/_legacy/payment-resolver/paypal/subscriptions/active-refund-previous-stmnt.json +0 -323
  75. package/test/_legacy/payment-resolver/paypal/subscriptions/active.json +0 -192
  76. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-in-trial.json +0 -125
  77. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-not-complete.json +0 -76
  78. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue-2.json +0 -114
  79. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue.json +0 -114
  80. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active-to-cancelled.json +0 -111
  81. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active.json +0 -132
  82. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-cancelled.json +0 -107
  83. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-expired.json +0 -91
  84. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-refund.json +0 -147
  85. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-suspended.json +0 -120
  86. package/test/_legacy/payment-resolver/stripe/orders/refunded.json +0 -0
  87. package/test/_legacy/payment-resolver/stripe/orders/regular.json +0 -91
  88. package/test/_legacy/payment-resolver/stripe/subscriptions/active.json +0 -421
  89. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-in-trial.json +0 -349
  90. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active-failed-authorization.json +0 -430
  91. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active.json +0 -319
  92. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-cancelled.json +0 -319
  93. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-refund.json +0 -414
  94. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-suspended.json +0 -319
  95. package/test/_legacy/payment-resolver/stripe/subscriptions/unsure.json +0 -339
  96. package/test/_legacy/test-new +0 -1178
  97. package/test/_legacy/test.md +0 -89
  98. package/test/_legacy/usage.js +0 -175
  99. package/test/_legacy/user.js +0 -31
  100. package/test/ai/tools-live.js +0 -170
  101. package/test/boot/emulator-boots.js +0 -37
  102. package/test/content/blog-generate.js +0 -160
  103. package/test/email/campaign-send.js +0 -45
  104. package/test/email/consent-lifecycle.js +0 -257
  105. package/test/email/feedback-and-plain-send.js +0 -52
  106. package/test/email/fixtures/editorial.json +0 -30
  107. package/test/email/fixtures/field-report.json +0 -53
  108. package/test/email/marketing-lifecycle.js +0 -132
  109. package/test/email/newsletter-generate.js +0 -819
  110. package/test/email/newsletter-templates.js +0 -491
  111. package/test/email/templates.js +0 -207
  112. package/test/email/transactional-send.js +0 -34
  113. package/test/email/transactional.js +0 -560
  114. package/test/email/validation.js +0 -710
  115. package/test/events/auth-delete-race.js +0 -201
  116. package/test/events/payments/journey-payments-cancel-endpoint.js +0 -100
  117. package/test/events/payments/journey-payments-cancel.js +0 -182
  118. package/test/events/payments/journey-payments-discount.js +0 -89
  119. package/test/events/payments/journey-payments-failure.js +0 -146
  120. package/test/events/payments/journey-payments-legacy-product.js +0 -149
  121. package/test/events/payments/journey-payments-one-time-failure.js +0 -111
  122. package/test/events/payments/journey-payments-one-time.js +0 -136
  123. package/test/events/payments/journey-payments-plan-change.js +0 -144
  124. package/test/events/payments/journey-payments-refund-webhook.js +0 -180
  125. package/test/events/payments/journey-payments-suspend.js +0 -181
  126. package/test/events/payments/journey-payments-trial-cancel.js +0 -130
  127. package/test/events/payments/journey-payments-trial.js +0 -171
  128. package/test/events/payments/journey-payments-uid-resolution.js +0 -132
  129. package/test/events/payments/journey-payments-upgrade.js +0 -135
  130. package/test/fixtures/chargebee/invoice-one-time.json +0 -27
  131. package/test/fixtures/chargebee/subscription-active.json +0 -44
  132. package/test/fixtures/chargebee/subscription-cancelled.json +0 -42
  133. package/test/fixtures/chargebee/subscription-in-trial.json +0 -41
  134. package/test/fixtures/chargebee/subscription-legacy-plan.json +0 -41
  135. package/test/fixtures/chargebee/subscription-non-renewing.json +0 -41
  136. package/test/fixtures/chargebee/subscription-paused.json +0 -42
  137. package/test/fixtures/chargebee/webhook-payment-failed.json +0 -51
  138. package/test/fixtures/chargebee/webhook-subscription-created.json +0 -47
  139. package/test/fixtures/paypal/order-approved.json +0 -62
  140. package/test/fixtures/paypal/order-completed.json +0 -110
  141. package/test/fixtures/paypal/subscription-active.json +0 -76
  142. package/test/fixtures/paypal/subscription-cancelled.json +0 -50
  143. package/test/fixtures/paypal/subscription-suspended.json +0 -65
  144. package/test/fixtures/stripe/checkout-session-completed.json +0 -130
  145. package/test/fixtures/stripe/invoice-payment-failed.json +0 -148
  146. package/test/fixtures/stripe/invoice-subscription-payment-failed.json +0 -28
  147. package/test/fixtures/stripe/subscription-active.json +0 -161
  148. package/test/fixtures/stripe/subscription-canceled.json +0 -161
  149. package/test/fixtures/stripe/subscription-trialing.json +0 -161
  150. package/test/functions/admin/database-read.js +0 -134
  151. package/test/functions/admin/database-write.js +0 -159
  152. package/test/functions/admin/edit-post.js +0 -366
  153. package/test/functions/admin/firestore-query.js +0 -207
  154. package/test/functions/admin/firestore-read.js +0 -129
  155. package/test/functions/admin/firestore-write.js +0 -105
  156. package/test/functions/admin/get-stats.js +0 -115
  157. package/test/functions/admin/send-email.js +0 -119
  158. package/test/functions/admin/send-notification.js +0 -208
  159. package/test/functions/admin/write-repo-content.js +0 -223
  160. package/test/functions/general/add-marketing-contact.js +0 -335
  161. package/test/functions/general/fetch-post.js +0 -54
  162. package/test/functions/general/generate-uuid.js +0 -114
  163. package/test/functions/test/authenticate.js +0 -64
  164. package/test/functions/user/create-custom-token.js +0 -73
  165. package/test/functions/user/delete.js +0 -135
  166. package/test/functions/user/get-active-sessions.js +0 -111
  167. package/test/functions/user/get-subscription-info.js +0 -99
  168. package/test/functions/user/regenerate-api-keys.js +0 -156
  169. package/test/functions/user/resolve.js +0 -52
  170. package/test/functions/user/sign-out-all-sessions.js +0 -94
  171. package/test/functions/user/sign-up.js +0 -252
  172. package/test/functions/user/submit-feedback.js +0 -104
  173. package/test/functions/user/validate-settings.js +0 -82
  174. package/test/helpers/ai-request-payload.js +0 -300
  175. package/test/helpers/ai-test-provider.js +0 -202
  176. package/test/helpers/ai-tools-format.js +0 -383
  177. package/test/helpers/content/blog-auto-publisher.js +0 -484
  178. package/test/helpers/content/feed-parser.js +0 -528
  179. package/test/helpers/content/ghostii-blocks.js +0 -134
  180. package/test/helpers/content/ghostii-feed-integration.js +0 -404
  181. package/test/helpers/content/ghostii-write-article.js +0 -243
  182. package/test/helpers/environment.js +0 -230
  183. package/test/helpers/infer-contact.js +0 -113
  184. package/test/helpers/merge-line-files.js +0 -271
  185. package/test/helpers/payment/chargebee/parse-webhook.js +0 -413
  186. package/test/helpers/payment/chargebee/to-unified-one-time.js +0 -147
  187. package/test/helpers/payment/chargebee/to-unified-subscription.js +0 -648
  188. package/test/helpers/payment/paypal/parse-webhook.js +0 -539
  189. package/test/helpers/payment/paypal/to-unified-one-time.js +0 -382
  190. package/test/helpers/payment/paypal/to-unified-subscription.js +0 -820
  191. package/test/helpers/payment/stripe/parse-webhook.js +0 -447
  192. package/test/helpers/payment/stripe/to-unified-one-time.js +0 -306
  193. package/test/helpers/payment/stripe/to-unified-subscription.js +0 -708
  194. package/test/helpers/sanitize.js +0 -222
  195. package/test/helpers/slugify.js +0 -394
  196. package/test/helpers/storage.js +0 -194
  197. package/test/helpers/user.js +0 -755
  198. package/test/helpers/webhook-forward.js +0 -418
  199. package/test/mcp/discovery.js +0 -53
  200. package/test/mcp/oauth.js +0 -161
  201. package/test/mcp/protocol.js +0 -268
  202. package/test/mcp/roles.js +0 -188
  203. package/test/mcp/utils.js +0 -245
  204. package/test/routes/admin/create-post.js +0 -364
  205. package/test/routes/admin/database.js +0 -131
  206. package/test/routes/admin/deduplicate-image-alts.js +0 -190
  207. package/test/routes/admin/email.js +0 -114
  208. package/test/routes/admin/firestore-query.js +0 -204
  209. package/test/routes/admin/firestore.js +0 -127
  210. package/test/routes/admin/infer-contact.js +0 -218
  211. package/test/routes/admin/notification.js +0 -198
  212. package/test/routes/admin/post-resize-image.js +0 -184
  213. package/test/routes/admin/post.js +0 -368
  214. package/test/routes/admin/repo-content.js +0 -223
  215. package/test/routes/admin/stats.js +0 -112
  216. package/test/routes/content/post.js +0 -54
  217. package/test/routes/general/uuid.js +0 -115
  218. package/test/routes/marketing/campaign.js +0 -125
  219. package/test/routes/marketing/contact.js +0 -370
  220. package/test/routes/marketing/email-preferences.js +0 -292
  221. package/test/routes/marketing/push-send.js +0 -32
  222. package/test/routes/marketing/webhook-forward.js +0 -61
  223. package/test/routes/marketing/webhook.js +0 -639
  224. package/test/routes/payments/cancel.js +0 -163
  225. package/test/routes/payments/discount.js +0 -80
  226. package/test/routes/payments/dispute-alert.js +0 -320
  227. package/test/routes/payments/intent.js +0 -336
  228. package/test/routes/payments/portal.js +0 -92
  229. package/test/routes/payments/refund.js +0 -179
  230. package/test/routes/payments/trial-eligibility.js +0 -71
  231. package/test/routes/payments/webhook.js +0 -107
  232. package/test/routes/test/authenticate.js +0 -59
  233. package/test/routes/test/schema.js +0 -554
  234. package/test/routes/test/usage.js +0 -342
  235. package/test/routes/user/api-keys.js +0 -156
  236. package/test/routes/user/delete.js +0 -136
  237. package/test/routes/user/feedback.js +0 -104
  238. package/test/routes/user/sessions.js +0 -199
  239. package/test/routes/user/settings-validate.js +0 -82
  240. package/test/routes/user/signup.js +0 -542
  241. package/test/routes/user/subscription.js +0 -99
  242. package/test/routes/user/token.js +0 -73
  243. package/test/routes/user/user.js +0 -157
  244. package/test/rules/notifications.js +0 -410
  245. package/test/rules/payments-carts.js +0 -371
  246. package/test/rules/user.js +0 -396
package/plans/mcp2.md DELETED
@@ -1,247 +0,0 @@
1
- # MCP Role-Based Tool Scoping + Consumer Extensibility
2
-
3
- ## Context
4
-
5
- The BEM MCP server currently works only for admins — a single `BACKEND_MANAGER_KEY` grants access to all 19 tools. Regular users can't connect at all, and consumer BEM projects have no way to add custom MCP tools. This plan adds:
6
-
7
- 1. **Role-based tool scoping** — tools tagged `admin`, `user`, or `public`; connections only see tools matching their role
8
- 2. **User authentication** — seamless OAuth sign-in flow + API key as long-lived credential
9
- 3. **Consumer MCP tools** — a single `functions/mcp.js` file in consumer projects
10
-
11
- ## Architecture
12
-
13
- ### Role Model
14
-
15
- Every tool gets a `role` field:
16
-
17
- | Role | Who sees it | Examples |
18
- |------|-------------|---------|
19
- | `admin` | Admin key connections only | firestore_read, send_email, run_cron |
20
- | `user` | Authenticated users + admins | get_user, get_subscription, cancel_subscription |
21
- | `public` | Everyone (including unauthenticated) | generate_uuid, health_check |
22
-
23
- Admin sees ALL tools. User sees `user` + `public`. Unauthenticated sees only `public`. Defense-in-depth — even if someone calls an admin tool by name, the route still rejects with 403.
24
-
25
- ### Auth: How Users Connect (OAuth + Consumer's Website)
26
-
27
- **The key insight:** users already have an API key (`api.privateKey`) on their Firestore account doc. It's long-lived, already works with `assistant.authenticate()`, and already encodes identity + access level. This is the credential.
28
-
29
- **The UX:** Claude Code/Desktop opens a browser → user signs in on their familiar site → redirected back to Claude. Done. No copy-pasting tokens.
30
-
31
- **The OAuth flow (step by step):**
32
-
33
- ```
34
- 1. Claude discovers endpoints:
35
- GET /.well-known/oauth-authorization-server
36
- → { authorization_endpoint, token_endpoint, ... }
37
-
38
- 2. Claude opens browser to BEM authorize endpoint:
39
- GET /backend-manager/mcp/authorize?redirect_uri=CLAUDE_CALLBACK&state=STATE
40
-
41
- 3. BEM authorize checks the request:
42
- - If client_id === BACKEND_MANAGER_KEY → auto-redirect (admin, unchanged)
43
- - Otherwise → redirect to consumer's /token page:
44
- https://app.example.com/token?redirect_uri=CLAUDE_CALLBACK&state=STATE
45
-
46
- 4. User lands on their familiar website sign-in page.
47
- Signs in with Google / email+password / whatever.
48
- After sign-in, page gets Firebase ID token via webManager.auth().getIdToken()
49
-
50
- 5. Consumer's /token page redirects to CLAUDE_CALLBACK:
51
- CLAUDE_CALLBACK?code=FIREBASE_ID_TOKEN&state=STATE
52
-
53
- 6. Claude exchanges code for access token:
54
- POST /backend-manager/mcp/token
55
- { code: FIREBASE_ID_TOKEN }
56
-
57
- 7. BEM token endpoint:
58
- - If code === BACKEND_MANAGER_KEY → return it as access_token (admin, unchanged)
59
- - Otherwise → verify Firebase ID token with admin.auth().verifyIdToken()
60
- → look up user doc → return user's api.privateKey as access_token
61
-
62
- 8. Claude uses the API key for all future MCP requests:
63
- Authorization: Bearer {api.privateKey}
64
- ```
65
-
66
- **Why API key, not JWT:**
67
- - Firebase ID tokens expire in 1 hour — MCP connection would break constantly
68
- - The API key never expires (until regenerated)
69
- - `assistant.authenticate()` already handles API key → user lookup
70
- - User can revoke by regenerating from account settings
71
-
72
- **Why redirect to consumer's website instead of embedding Firebase Auth:**
73
- - User sees their familiar sign-in page (branded, trusted)
74
- - No need to embed Firebase Auth SDK in BEM's authorize HTML
75
- - UJM already has a `/token` page — just needs a small update to support `redirect_uri` param
76
- - Works with any auth provider the consumer has configured
77
-
78
- ### Auth Classification (Fast, No DB Call)
79
-
80
- `resolveAuthInfo(token)` classifies the Bearer token for tool filtering:
81
-
82
- - Token matches `BACKEND_MANAGER_KEY` → `role: 'admin'`
83
- - Any other non-empty token → `role: 'user'` (API key from the OAuth flow)
84
- - No token → `role: 'public'`
85
-
86
- No DB call needed — actual validation happens at the route level when a tool is called.
87
-
88
- ### Consumer MCP Tools
89
-
90
- Consumer tools live in a **single file**: `functions/mcp.js`. Keeps things simple since most consumer MCP tools are just route delegations pointing at routes already defined in `functions/routes/`.
91
-
92
- ```js
93
- // functions/mcp.js
94
- module.exports = [
95
- // Route delegation — points at an existing route (works on stdio + HTTP)
96
- {
97
- name: 'get_sponsorship',
98
- description: 'Get sponsorship details by ID',
99
- role: 'user',
100
- method: 'GET',
101
- path: 'sponsorship',
102
- inputSchema: {
103
- type: 'object',
104
- properties: {
105
- id: { type: 'string', description: 'Sponsorship ID' },
106
- },
107
- required: ['id'],
108
- },
109
- },
110
-
111
- // Handler mode — runs code directly (HTTP transport only, has full Manager context)
112
- {
113
- name: 'newsletter_stats',
114
- description: 'Get newsletter stats for the past N days',
115
- role: 'admin',
116
- inputSchema: {
117
- type: 'object',
118
- properties: {
119
- days: { type: 'number', description: 'Days to look back', default: 30 },
120
- },
121
- },
122
- handler: async ({ Manager, assistant, user, params, libraries }) => {
123
- const cutoff = Date.now() - (params.days || 30) * 86400000;
124
- const snapshot = await libraries.admin.firestore()
125
- .collection('newsletters')
126
- .where('metadata.created.timestampUNIX', '>=', Math.floor(cutoff / 1000))
127
- .get();
128
- return { total: snapshot.docs.length };
129
- },
130
- },
131
- ];
132
- ```
133
-
134
- Consumer tools with the same name as a built-in tool override it (same precedence as consumer routes).
135
-
136
- ## File Changes
137
-
138
- ### 1. `src/mcp/tools.js` — Add `role` to every tool
139
-
140
- | Tool | Role |
141
- |------|------|
142
- | `firestore_read/write/query` | `admin` |
143
- | `send_email`, `send_notification` | `admin` |
144
- | `sync_users`, `list_campaigns`, `create_campaign` | `admin` |
145
- | `get_stats`, `run_cron`, `create_post`, `create_backup`, `run_hook` | `admin` |
146
- | `get_user`, `get_subscription` | `user` |
147
- | `cancel_subscription`, `refund_payment` | `user` |
148
- | `generate_uuid`, `health_check` | `public` |
149
-
150
- ### 2. NEW: `src/mcp/utils.js` — Shared utilities
151
-
152
- - **`resolveAuthInfo(token)`** — classifies token → `{ role, authType, token }`. Admin key check is instant; everything else = user.
153
- - **`filterToolsByRole(tools, role)`** — admin→all, user→user+public, public→public only.
154
- - **`loadConsumerTools(cwd)`** — checks for `${cwd}/mcp.js`, `require()`s it if it exists, validates shape, returns array.
155
- - **`buildToolMap(builtinTools, consumerTools)`** — merges into a Map; consumer tools override same-name built-ins.
156
-
157
- ### 3. `src/mcp/client.js` — Support user auth tokens
158
-
159
- Extend constructor to accept `{ baseUrl, backendManagerKey, userToken }`.
160
-
161
- In `call()`:
162
- - Has `backendManagerKey` → current behavior (key in query/body)
163
- - Has `userToken` (no backendManagerKey) → put token in `Authorization: Bearer` header + `authenticationToken` query param for GET
164
- - Neither → unauthenticated request
165
-
166
- ### 4. `src/mcp/index.js` — Stdio server with role filtering
167
-
168
- - Import utils, call `resolveAuthInfo()` to determine role
169
- - Call `loadConsumerTools(options.cwd)` if `cwd` is provided
170
- - Merge + filter tools by role in `ListToolsRequestSchema` handler
171
- - In `CallToolRequestSchema`: `path`-based tools via BEMClient. Handler-only tools return error explaining they require HTTP transport.
172
- - Accept new `options.token` for user connections
173
-
174
- ### 5. `src/mcp/handler.js` — HTTP handler with OAuth user flow
175
-
176
- **`handleAuthorize()`** — the big change:
177
- - If `client_id` matches admin key → auto-redirect (current behavior, unchanged)
178
- - Otherwise → **redirect to consumer's website** for sign-in:
179
- - Build consumer auth URL from `Manager.config.brand.url` (or a configurable `mcp.authUrl` in backend-manager-config.json)
180
- - Redirect to: `{consumerUrl}/token?redirect_uri={originalRedirectUri}&state={state}`
181
- - The consumer's `/token` page handles sign-in, gets Firebase ID token, redirects back to Claude's callback
182
-
183
- **`handleToken()`** — exchanges Firebase ID token for API key:
184
- - If code matches admin key → return as `access_token` (unchanged)
185
- - Otherwise → treat code as Firebase ID token:
186
- 1. `admin.auth().verifyIdToken(code)` to get UID
187
- 2. `admin.firestore().doc('users/{uid}').get()` to get user doc
188
- 3. Return `user.api.privateKey` as the `access_token`
189
- 4. If user has no API key, generate one and save it
190
-
191
- **`handleMcpProtocol()`** — role-based tool filtering:
192
- - Extract Bearer token, call `resolveAuthInfo()`
193
- - Allow public connections (no 401 for empty tokens — just show public tools)
194
- - Discover + merge consumer tools (cached at module scope)
195
- - Filter tools by role in `ListToolsRequestSchema`
196
- - In `CallToolRequestSchema`:
197
- - `path`-based tools: BEMClient HTTP call (current behavior)
198
- - `handler`-based tools: execute directly with `{ Manager, assistant, user, params, libraries }`
199
-
200
- **Rename `isValidKey()` → `isAdminKey()`** for clarity.
201
-
202
- ### 6. `src/cli/commands/mcp.js` — New CLI flags
203
-
204
- - `--token` (`-t`): user's API key (for user-level connections)
205
- - Pass `cwd: functionsDir` to `startServer()` for consumer tool discovery
206
- - When `--token` is provided, create BEMClient with userToken instead of backendManagerKey
207
-
208
- ### 7. Consumer's UJM `/token` page — Small update (separate repo)
209
-
210
- The UJM `/token` page needs to support the MCP OAuth redirect flow:
211
- - Detect `redirect_uri` and `state` query params
212
- - After sign-in, get Firebase ID token via `webManager.auth().getIdToken()`
213
- - Redirect to `redirect_uri?code={idToken}&state={state}`
214
-
215
- This is a small addition to the existing `/token` page layout. Files:
216
- - `ultimate-jekyll-manager/src/defaults/dist/_layouts/blueprint/auth/token.html`
217
- - `ultimate-jekyll-manager/src/defaults/dist/_layouts/themes/classy/frontend/pages/auth/token.html`
218
-
219
- ### 8. `docs/mcp.md` — Documentation
220
-
221
- - Add "Roles" section explaining admin/user/public scoping
222
- - Add "User Authentication" section with the OAuth flow diagram
223
- - Add "Consumer MCP Tools" section with `functions/mcp.js` format + examples
224
- - Add CLI examples for user connections
225
- - Add guide for configuring the consumer auth URL
226
-
227
- ## Implementation Order
228
-
229
- 1. `src/mcp/utils.js` (new) — foundation utilities
230
- 2. `src/mcp/tools.js` — add `role` to all 19 tools
231
- 3. `src/mcp/client.js` — user token support
232
- 4. `src/mcp/index.js` — stdio role filtering + consumer tools
233
- 5. `src/cli/commands/mcp.js` — new CLI flags
234
- 6. `src/mcp/handler.js` — HTTP role filtering + OAuth user flow + consumer tool handlers
235
- 7. UJM `/token` page update (separate repo)
236
- 8. `docs/mcp.md` — documentation
237
-
238
- ## Verification
239
-
240
- 1. **Existing admin flow**: `npx mgr mcp` with `BACKEND_MANAGER_KEY` → all 19 tools listed, all callable
241
- 2. **User flow (stdio)**: `npx mgr mcp --token <api-key>` → only user+public tools listed
242
- 3. **Public flow**: `npx mgr mcp` (no key, no token) → only public tools listed
243
- 4. **Consumer tools**: Create `functions/mcp.js` in a consumer project → tools appear in listing
244
- 5. **HTTP OAuth flow**: Connect from Claude Code/Desktop → redirected to consumer site → sign in → redirected back → user tools available
245
- 6. **Token exchange**: POST to `/mcp/token` with Firebase ID token → returns API key as access_token
246
- 7. **Role enforcement**: User calling an admin tool by name → unknown tool error (tool not in filtered list)
247
- 8. **Run `npx mgr test`** to verify no regressions
@@ -1,97 +0,0 @@
1
- # Suspended Subscription Dead Zone
2
-
3
- ## Issue
4
-
5
- A user with a `suspended` subscription was trapped in a loop:
6
- - **Trying to subscribe** → "You already have a subscription. Please cancel your existing subscription before purchasing a new one." (400)
7
- - **Trying to cancel** → "No active paid subscription found" (400)
8
-
9
- No way out.
10
-
11
- ## Affected User
12
-
13
- - **UID:** `t9AeAe7QUhNXAUYRV1vUbOU0QVV2`
14
- - **Brand:** Somiibo
15
-
16
- The user had two stale subscriptions from different processors:
17
-
18
- | Field | Processor | Status | Resource ID | Last Webhook |
19
- |---|---|---|---|---|
20
- | `subscription` | PayPal | `suspended` | `I-Y41DMAGNWGP1` | `BILLING.SUBSCRIPTION.SUSPENDED` (2026-04-19) |
21
- | `plan` (legacy) | Chargebee | `suspended` | `169xhEU1pYXAt3dsj` | `subscription-profile-fixer` (2024-02-28) |
22
-
23
- PayPal suspended the subscription (likely failed payment) but never sent a `BILLING.SUBSCRIPTION.CANCELLED` webhook. PayPal doesn't auto-cancel suspended subscriptions — it leaves them in limbo until the user resolves payment or manually cancels in PayPal. The local status stayed `suspended` forever.
24
-
25
- ## Root Cause
26
-
27
- Asymmetric status gates between the intent and cancel endpoints:
28
-
29
- ### Intent gate (`POST /payments/intent`)
30
- ```js
31
- // Blocked if product is NOT basic AND status is NOT cancelled
32
- if (subProductId !== 'basic' && subStatus !== 'cancelled') { ... }
33
- ```
34
- - `active` → blocked (correct)
35
- - `suspended` → blocked (correct — user should cancel first)
36
- - `cancelled` → allowed (correct)
37
-
38
- ### Cancel gate (`POST /payments/cancel`)
39
- ```js
40
- // Blocked if status is NOT active
41
- if (subscription.status !== 'active') { ... }
42
- ```
43
- - `active` → allowed (correct)
44
- - `suspended` → **blocked** (the bug)
45
- - `cancelled` → blocked (correct — nothing to cancel)
46
-
47
- The intent gate treats `suspended` as "has a subscription" (must cancel first), but the cancel gate treats `suspended` as "no subscription to cancel." Dead zone.
48
-
49
- ## The Fix
50
-
51
- ### 1. Cancel gate now accepts `suspended`
52
-
53
- ```js
54
- // Before
55
- if (!subscription || subscription.status !== 'active' || ...)
56
-
57
- // After
58
- if (!subscription || (subscription.status !== 'active' && subscription.status !== 'suspended') || ...)
59
- ```
60
-
61
- ### 2. Fallback for dead processor subscriptions
62
-
63
- When cancelling a `suspended` subscription, the processor API call might fail — the subscription could be expired/deleted on the processor's side while our local state is stale. The cancel endpoint now catches processor errors for suspended subscriptions and directly resets the user doc:
64
-
65
- ```js
66
- if (subscription.status === 'suspended') {
67
- // Processor rejected — subscription is already dead on their end
68
- // Directly reset the user doc to cancelled
69
- await admin.firestore().doc(`users/${uid}`).set({
70
- subscription: {
71
- status: 'cancelled',
72
- product: { id: 'basic', name: 'Basic' },
73
- cancellation: { pending: false, date: { timestamp, timestampUNIX } },
74
- },
75
- }, { merge: true });
76
- }
77
- ```
78
-
79
- ### 3. New test coverage
80
-
81
- - Test account `cancel-suspended` with `status: 'suspended'` and test processor payment details
82
- - Test `allows-suspended-subscription` verifying the cancel endpoint accepts and processes suspended subscriptions
83
-
84
- ## Status Matrix (after fix)
85
-
86
- | Status | Can subscribe? | Can cancel? | Can delete account? |
87
- |---|---|---|---|
88
- | `active` (basic) | Yes | No (nothing to cancel) | Yes |
89
- | `active` (paid) | No (cancel first) | Yes | No |
90
- | `suspended` | No (cancel first) | **Yes** (fixed) | No |
91
- | `cancelled` | Yes | No (already cancelled) | Yes |
92
-
93
- ## Files Changed
94
-
95
- - `src/manager/routes/payments/cancel/post.js` — gate + fallback
96
- - `src/test/test-accounts.js` — `cancel-suspended` account
97
- - `test/routes/payments/cancel.js` — `allows-suspended-subscription` test
@@ -1,162 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Test Helper — direct provider lookups + removals via API
5
- *
6
- * Lets the live-test checklist verify SendGrid + Beehiiv state without
7
- * clicking around in dashboards. Reuses the same provider helpers that
8
- * BEM uses in production (findContact / removeContact).
9
- *
10
- * Usage (run from a consumer project's functions/ dir):
11
- *
12
- * node ../../../backend-manager/scripts/test-helper-providers.js find user@example.com
13
- * node ../../../backend-manager/scripts/test-helper-providers.js purge user@example.com
14
- *
15
- * Or symlink: `ln -s ../../../backend-manager/scripts/test-helper-providers.js ./prov`
16
- * then: `node ./prov find user@example.com`
17
- *
18
- * - find: prints whether the contact exists in SendGrid + Beehiiv (and the data shape).
19
- * - purge: removes the contact from BOTH providers (idempotent; safe if absent).
20
- *
21
- * The script picks up:
22
- * - SENDGRID_API_KEY, BEEHIIV_API_KEY from <cwd>/.env (functions/.env)
23
- * - marketing.campaigns.listId, marketing.newsletter.publicationId from <cwd>/backend-manager-config.json
24
- * - service-account from <cwd>/service-account.json
25
- *
26
- * Exit codes: 0 ok, 1 usage error, 2 provider error.
27
- */
28
-
29
- const path = require('path');
30
- const fs = require('fs');
31
-
32
- const cwd = process.cwd();
33
- const envPath = path.join(cwd, '.env');
34
- const configPath = path.join(cwd, 'backend-manager-config.json');
35
- const serviceAccountPath = path.join(cwd, 'service-account.json');
36
-
37
- // --- sanity checks ---
38
- if (!fs.existsSync(envPath)) {
39
- console.error(`✗ Missing ${envPath} — run from the consumer's functions/ dir`);
40
- process.exit(1);
41
- }
42
- if (!fs.existsSync(configPath)) {
43
- console.error(`✗ Missing ${configPath} — run from the consumer's functions/ dir`);
44
- process.exit(1);
45
- }
46
- if (!fs.existsSync(serviceAccountPath)) {
47
- console.error(`✗ Missing ${serviceAccountPath} — run from the consumer's functions/ dir`);
48
- process.exit(1);
49
- }
50
-
51
- // --- parse args ---
52
- const [, , cmd, email] = process.argv;
53
- const VALID_CMDS = ['find', 'purge'];
54
-
55
- if (!cmd || !VALID_CMDS.includes(cmd) || !email) {
56
- console.error('Usage:');
57
- console.error(' node test-helper-providers.js find <email>');
58
- console.error(' node test-helper-providers.js purge <email>');
59
- process.exit(1);
60
- }
61
-
62
- // --- bootstrap env (dotenv style, minimal) ---
63
- require('dotenv').config({ path: envPath });
64
-
65
- if (!process.env.SENDGRID_API_KEY) {
66
- console.error('✗ SENDGRID_API_KEY not set in .env');
67
- process.exit(1);
68
- }
69
- if (!process.env.BEEHIIV_API_KEY) {
70
- console.error('✗ BEEHIIV_API_KEY not set in .env');
71
- process.exit(1);
72
- }
73
-
74
- // --- bootstrap Manager so providers can read Manager.config.marketing.* ---
75
- // The providers require '../../../index.js' which is the Manager singleton.
76
- // We need to load BEM from the consumer's node_modules (not the BEM repo's own src)
77
- // so it picks up the consumer's config + service account.
78
- let Manager;
79
- try {
80
- const BackendManager = require(path.join(cwd, 'node_modules', 'backend-manager'));
81
- Manager = (new BackendManager()).init({}, { setupFunctionsLegacy: false, log: false });
82
- } catch (e) {
83
- console.error('✗ Failed to bootstrap Manager from consumer node_modules:', e.message);
84
- process.exit(2);
85
- }
86
-
87
- // --- load providers via the BEM Manager.libraries surface (preferred) or direct path ---
88
- const sendgridProviderPath = path.join(cwd, 'node_modules', 'backend-manager', 'src', 'manager', 'libraries', 'email', 'providers', 'sendgrid.js');
89
- const beehiivProviderPath = path.join(cwd, 'node_modules', 'backend-manager', 'src', 'manager', 'libraries', 'email', 'providers', 'beehiiv.js');
90
-
91
- const sendgrid = require(sendgridProviderPath);
92
- const beehiiv = require(beehiivProviderPath);
93
-
94
- // --- commands ---
95
- async function find(email) {
96
- console.log(`Looking up ${email} on both providers...`);
97
-
98
- const [sgContact, bhContact] = await Promise.all([
99
- sendgrid.findContact(email).catch(e => ({ _error: e.message })),
100
- beehiiv.findContact(email).catch(e => ({ _error: e.message })),
101
- ]);
102
-
103
- console.log('\n── SendGrid ──');
104
- if (sgContact?._error) {
105
- console.log(` ✗ error: ${sgContact._error}`);
106
- } else if (!sgContact) {
107
- console.log(' ⊘ not found');
108
- } else {
109
- console.log(` ✓ found — id=${sgContact.id || '?'}, email=${sgContact.email || '?'}`);
110
- }
111
-
112
- console.log('\n── Beehiiv ──');
113
- if (bhContact?._error) {
114
- console.log(` ✗ error: ${bhContact._error}`);
115
- } else if (!bhContact) {
116
- console.log(' ⊘ not found');
117
- } else {
118
- console.log(` ✓ found — id=${bhContact.id || '?'}, email=${bhContact.email || '?'}, status=${bhContact.status || '?'}`);
119
- }
120
-
121
- return { sgContact, bhContact };
122
- }
123
-
124
- async function purge(email) {
125
- console.log(`Removing ${email} from both providers...`);
126
-
127
- const [sgResult, bhResult] = await Promise.all([
128
- sendgrid.removeContact(email).catch(e => ({ _error: e.message })),
129
- beehiiv.removeContact(email).catch(e => ({ _error: e.message })),
130
- ]);
131
-
132
- console.log('\n── SendGrid ──');
133
- if (sgResult?._error) {
134
- console.log(` ✗ error: ${sgResult._error}`);
135
- } else {
136
- console.log(` ✓ ${JSON.stringify(sgResult)}`);
137
- }
138
-
139
- console.log('\n── Beehiiv ──');
140
- if (bhResult?._error) {
141
- console.log(` ✗ error: ${bhResult._error}`);
142
- } else {
143
- console.log(` ✓ ${JSON.stringify(bhResult)}`);
144
- }
145
-
146
- return { sgResult, bhResult };
147
- }
148
-
149
- // --- main ---
150
- (async () => {
151
- try {
152
- if (cmd === 'find') {
153
- await find(email);
154
- } else if (cmd === 'purge') {
155
- await purge(email);
156
- }
157
- process.exit(0);
158
- } catch (e) {
159
- console.error('✗ Unexpected error:', e);
160
- process.exit(2);
161
- }
162
- })();
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Fetches the latest disposable email domain list from GitHub and saves it as JSON.
5
- *
6
- * Source: https://github.com/disposable-email-domains/disposable-email-domains
7
- * (curated, ~5k domains, low false-positive rate)
8
- *
9
- * Run manually: node scripts/update-disposable-domains.js
10
- * Runs automatically on: npm prepublishOnly
11
- */
12
- const fs = require('fs');
13
- const path = require('path');
14
-
15
- const SOURCE_URL = 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf';
16
- const OUTPUT_PATH = path.join(__dirname, '..', 'src', 'manager', 'libraries', 'email', 'data', 'disposable-domains.json');
17
-
18
- async function main() {
19
- console.log('Fetching disposable domain list...');
20
-
21
- const response = await fetch(SOURCE_URL);
22
-
23
- if (!response.ok) {
24
- throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
25
- }
26
-
27
- const text = await response.text();
28
- const domains = text
29
- .trim()
30
- .split('\n')
31
- .map(d => d.trim().toLowerCase())
32
- .filter(Boolean);
33
-
34
- const unique = [...new Set(domains)].sort();
35
-
36
- fs.writeFileSync(OUTPUT_PATH, JSON.stringify(unique, null, 2) + '\n');
37
-
38
- console.log(`Updated disposable-domains.json: ${unique.length} domains`);
39
- }
40
-
41
- main().catch((e) => {
42
- console.warn('Warning: Failed to update disposable domains:', e.message);
43
- console.warn('Using existing list. Run manually later: node scripts/update-disposable-domains.js');
44
- });
package/templates/_.env DELETED
@@ -1,42 +0,0 @@
1
- # ========== Default Values ==========
2
- # GitHub
3
- GH_TOKEN=""
4
-
5
- # Backend Manager
6
- BACKEND_MANAGER_KEY=""
7
- BACKEND_MANAGER_WEBHOOK_KEY=""
8
- BACKEND_MANAGER_NAMESPACE=""
9
- BACKEND_MANAGER_OPENAI_API_KEY=""
10
- BACKEND_MANAGER_ANTHROPIC_API_KEY=""
11
-
12
- # AI
13
- OPENAI_API_KEY=""
14
- ANTHROPIC_API_KEY=""
15
- CLAUDE_CODE_OAUTH_TOKEN=""
16
-
17
- # Payment Processors
18
- PAYPAL_CLIENT_SECRET=""
19
- STRIPE_SECRET_KEY=""
20
- CHARGEBEE_API_KEY=""
21
- COINBASE_API_KEY=""
22
-
23
- # Analytics
24
- GOOGLE_ANALYTICS_SECRET=""
25
- META_ACCESS_TOKEN=""
26
- TIKTOK_ACCESS_TOKEN=""
27
-
28
- # Security
29
- CLOUDFLARE_TOKEN=""
30
- RECAPTCHA_SECRET_KEY=""
31
-
32
- # Email & Newsletter
33
- SENDGRID_API_KEY=""
34
- BEEHIIV_API_KEY=""
35
- NEVERBOUNCE_API_KEY=""
36
- ZEROBOUNCE_API_KEY=""
37
- UNSUBSCRIBE_HMAC_KEY=""
38
- APOLLO_API_KEY=""
39
-
40
- # ========== Custom Values ==========
41
- # Add your custom environment variables below this line
42
- # ...
@@ -1,60 +0,0 @@
1
- # ========== Default Values ==========
2
- # macOS
3
- .DS_Store
4
-
5
- # Logs
6
- logs
7
- *.log
8
- npm-debug.log*
9
- yarn-debug.log*
10
- yarn-error.log*
11
- firebase-debug.log*
12
-
13
- # Firebase cache
14
- .firebase/
15
-
16
- # Firebase config
17
- .firebaserc
18
-
19
- # Runtime data
20
- pids
21
- *.pid
22
- *.seed
23
- *.pid.lock
24
-
25
- # Coverage directory
26
- coverage
27
- .nyc_output
28
-
29
- # Dependency directories
30
- node_modules/
31
-
32
- # Optional npm cache directory
33
- .npm
34
-
35
- # Optional eslint cache
36
- .eslintcache
37
-
38
- # Output of 'npm pack'
39
- *.tgz
40
-
41
- # Yarn Integrity file
42
- .yarn-integrity
43
-
44
- # dotenv environment variables file
45
- .env*
46
-
47
- # BEM specific
48
- .runtimeconfig.json
49
- service-account*.json
50
- bem-reload-trigger.js
51
- .temp/
52
- _legacy
53
-
54
- # Root proxy (generated by setup)
55
- /package.json
56
- /package-lock.json
57
-
58
- # ========== Custom Values ==========
59
- # Add your custom ignore patterns below this line
60
- # ...