backend-manager 5.9.5 → 5.9.6

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 (244) hide show
  1. package/package.json +8 -1
  2. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/metadata.json +14 -0
  3. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.html +486 -0
  4. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.md +41 -0
  5. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/newsletter.mjml +97 -0
  6. package/{test/email/fixtures/clean.json → src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/structure.json} +16 -4
  7. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-11T03-28-07/summary.md +1 -0
  8. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/metadata.json +14 -0
  9. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.html +486 -0
  10. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.md +41 -0
  11. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/newsletter.mjml +97 -0
  12. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/structure.json +42 -0
  13. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-00-28/summary.md +1 -0
  14. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/metadata.json +14 -0
  15. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.html +486 -0
  16. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.md +41 -0
  17. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/newsletter.mjml +97 -0
  18. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/structure.json +42 -0
  19. package/src/test/fixtures/firebase-project/.temp/newsletter/run-2026-06-18T01-01-34/summary.md +1 -0
  20. package/src/test/fixtures/firebase-project/.temp/test-mode.json +6 -0
  21. package/src/test/fixtures/firebase-project/database-debug.log +12 -0
  22. package/src/test/fixtures/firebase-project/firestore-debug.log +136 -0
  23. package/src/test/fixtures/firebase-project/pubsub-debug.log +17 -0
  24. package/.codeclimate.yml +0 -32
  25. package/.nvmrc +0 -1
  26. package/CHANGELOG.md +0 -1440
  27. package/PROGRESS.md +0 -93
  28. package/TODO-2.md +0 -121
  29. package/TODO-CHARGEBLAST.md +0 -32
  30. package/TODO-WEBHOOK-KEY-LEGACY-REMOVAL.md +0 -15
  31. package/TODO-WEBHOOK-KEY-UPGRADE.md +0 -138
  32. package/TODO-email-auth.md +0 -14
  33. package/TODO.md +0 -187
  34. package/plans/mcp2.md +0 -247
  35. package/plans/suspended-subscription-dead-zone.md +0 -97
  36. package/scripts/test-helper-providers.js +0 -162
  37. package/scripts/update-disposable-domains.js +0 -44
  38. package/templates/_.env +0 -42
  39. package/templates/_.gitignore +0 -60
  40. package/templates/backend-manager-config.json +0 -249
  41. package/templates/database.rules.json +0 -83
  42. package/templates/firebase.json +0 -66
  43. package/templates/firestore.indexes.json +0 -4
  44. package/templates/firestore.rules +0 -112
  45. package/templates/public/404.html +0 -26
  46. package/templates/public/index.html +0 -24
  47. package/templates/remoteconfig.template.json +0 -1
  48. package/templates/storage-lifecycle-config-1-day.json +0 -9
  49. package/templates/storage-lifecycle-config-30-days.json +0 -9
  50. package/templates/storage.rules +0 -8
  51. package/test/_init/accounts-validation.js +0 -58
  52. package/test/_legacy/ai/index.js +0 -231
  53. package/test/_legacy/ai/test.jpg +0 -0
  54. package/test/_legacy/ai/test.pdf +0 -0
  55. package/test/_legacy/index.js +0 -31
  56. package/test/_legacy/payment-resolver/chargebee/orders/refunded.json +0 -0
  57. package/test/_legacy/payment-resolver/chargebee/orders/unpaid.json +0 -98
  58. package/test/_legacy/payment-resolver/chargebee/subscriptions/active.json +0 -578
  59. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-in-trial.json +0 -38
  60. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-skipped-to-active.json +0 -135
  61. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active-to-cancelled.json +0 -42
  62. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-active.json +0 -44
  63. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-cancelled.json +0 -39
  64. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-refund.json +0 -143
  65. package/test/_legacy/payment-resolver/chargebee/subscriptions/trial-to-suspended.json +0 -48
  66. package/test/_legacy/payment-resolver/coinbase/orders/regular.json +0 -204
  67. package/test/_legacy/payment-resolver/coinbase/subscriptions/cancelled.json +0 -204
  68. package/test/_legacy/payment-resolver/coinbase/subscriptions/paid-2.json +0 -125
  69. package/test/_legacy/payment-resolver/index.js +0 -1558
  70. package/test/_legacy/payment-resolver/paypal/orders/refunded.json +0 -0
  71. package/test/_legacy/payment-resolver/paypal/orders/regular.json +0 -120
  72. package/test/_legacy/payment-resolver/paypal/subscriptions/active-refund-previous-stmnt.json +0 -323
  73. package/test/_legacy/payment-resolver/paypal/subscriptions/active.json +0 -192
  74. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-in-trial.json +0 -125
  75. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-not-complete.json +0 -76
  76. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue-2.json +0 -114
  77. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-payment-overdue.json +0 -114
  78. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active-to-cancelled.json +0 -111
  79. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-active.json +0 -132
  80. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-cancelled.json +0 -107
  81. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-expired.json +0 -91
  82. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-refund.json +0 -147
  83. package/test/_legacy/payment-resolver/paypal/subscriptions/trial-to-suspended.json +0 -120
  84. package/test/_legacy/payment-resolver/stripe/orders/refunded.json +0 -0
  85. package/test/_legacy/payment-resolver/stripe/orders/regular.json +0 -91
  86. package/test/_legacy/payment-resolver/stripe/subscriptions/active.json +0 -421
  87. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-in-trial.json +0 -349
  88. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active-failed-authorization.json +0 -430
  89. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-active.json +0 -319
  90. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-cancelled.json +0 -319
  91. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-refund.json +0 -414
  92. package/test/_legacy/payment-resolver/stripe/subscriptions/trial-to-suspended.json +0 -319
  93. package/test/_legacy/payment-resolver/stripe/subscriptions/unsure.json +0 -339
  94. package/test/_legacy/test-new +0 -1178
  95. package/test/_legacy/test.md +0 -89
  96. package/test/_legacy/usage.js +0 -175
  97. package/test/_legacy/user.js +0 -31
  98. package/test/ai/tools-live.js +0 -170
  99. package/test/boot/emulator-boots.js +0 -37
  100. package/test/content/blog-generate.js +0 -160
  101. package/test/email/campaign-send.js +0 -45
  102. package/test/email/consent-lifecycle.js +0 -257
  103. package/test/email/feedback-and-plain-send.js +0 -52
  104. package/test/email/fixtures/editorial.json +0 -30
  105. package/test/email/fixtures/field-report.json +0 -53
  106. package/test/email/marketing-lifecycle.js +0 -132
  107. package/test/email/newsletter-generate.js +0 -819
  108. package/test/email/newsletter-templates.js +0 -491
  109. package/test/email/templates.js +0 -207
  110. package/test/email/transactional-send.js +0 -34
  111. package/test/email/transactional.js +0 -560
  112. package/test/email/validation.js +0 -710
  113. package/test/events/auth-delete-race.js +0 -201
  114. package/test/events/payments/journey-payments-cancel-endpoint.js +0 -100
  115. package/test/events/payments/journey-payments-cancel.js +0 -182
  116. package/test/events/payments/journey-payments-discount.js +0 -89
  117. package/test/events/payments/journey-payments-failure.js +0 -146
  118. package/test/events/payments/journey-payments-legacy-product.js +0 -149
  119. package/test/events/payments/journey-payments-one-time-failure.js +0 -111
  120. package/test/events/payments/journey-payments-one-time.js +0 -136
  121. package/test/events/payments/journey-payments-plan-change.js +0 -144
  122. package/test/events/payments/journey-payments-refund-webhook.js +0 -180
  123. package/test/events/payments/journey-payments-suspend.js +0 -181
  124. package/test/events/payments/journey-payments-trial-cancel.js +0 -130
  125. package/test/events/payments/journey-payments-trial.js +0 -171
  126. package/test/events/payments/journey-payments-uid-resolution.js +0 -132
  127. package/test/events/payments/journey-payments-upgrade.js +0 -135
  128. package/test/fixtures/chargebee/invoice-one-time.json +0 -27
  129. package/test/fixtures/chargebee/subscription-active.json +0 -44
  130. package/test/fixtures/chargebee/subscription-cancelled.json +0 -42
  131. package/test/fixtures/chargebee/subscription-in-trial.json +0 -41
  132. package/test/fixtures/chargebee/subscription-legacy-plan.json +0 -41
  133. package/test/fixtures/chargebee/subscription-non-renewing.json +0 -41
  134. package/test/fixtures/chargebee/subscription-paused.json +0 -42
  135. package/test/fixtures/chargebee/webhook-payment-failed.json +0 -51
  136. package/test/fixtures/chargebee/webhook-subscription-created.json +0 -47
  137. package/test/fixtures/paypal/order-approved.json +0 -62
  138. package/test/fixtures/paypal/order-completed.json +0 -110
  139. package/test/fixtures/paypal/subscription-active.json +0 -76
  140. package/test/fixtures/paypal/subscription-cancelled.json +0 -50
  141. package/test/fixtures/paypal/subscription-suspended.json +0 -65
  142. package/test/fixtures/stripe/checkout-session-completed.json +0 -130
  143. package/test/fixtures/stripe/invoice-payment-failed.json +0 -148
  144. package/test/fixtures/stripe/invoice-subscription-payment-failed.json +0 -28
  145. package/test/fixtures/stripe/subscription-active.json +0 -161
  146. package/test/fixtures/stripe/subscription-canceled.json +0 -161
  147. package/test/fixtures/stripe/subscription-trialing.json +0 -161
  148. package/test/functions/admin/database-read.js +0 -134
  149. package/test/functions/admin/database-write.js +0 -159
  150. package/test/functions/admin/edit-post.js +0 -366
  151. package/test/functions/admin/firestore-query.js +0 -207
  152. package/test/functions/admin/firestore-read.js +0 -129
  153. package/test/functions/admin/firestore-write.js +0 -105
  154. package/test/functions/admin/get-stats.js +0 -115
  155. package/test/functions/admin/send-email.js +0 -119
  156. package/test/functions/admin/send-notification.js +0 -208
  157. package/test/functions/admin/write-repo-content.js +0 -223
  158. package/test/functions/general/add-marketing-contact.js +0 -335
  159. package/test/functions/general/fetch-post.js +0 -54
  160. package/test/functions/general/generate-uuid.js +0 -114
  161. package/test/functions/test/authenticate.js +0 -64
  162. package/test/functions/user/create-custom-token.js +0 -73
  163. package/test/functions/user/delete.js +0 -135
  164. package/test/functions/user/get-active-sessions.js +0 -111
  165. package/test/functions/user/get-subscription-info.js +0 -99
  166. package/test/functions/user/regenerate-api-keys.js +0 -156
  167. package/test/functions/user/resolve.js +0 -52
  168. package/test/functions/user/sign-out-all-sessions.js +0 -94
  169. package/test/functions/user/sign-up.js +0 -252
  170. package/test/functions/user/submit-feedback.js +0 -104
  171. package/test/functions/user/validate-settings.js +0 -82
  172. package/test/helpers/ai-request-payload.js +0 -300
  173. package/test/helpers/ai-test-provider.js +0 -202
  174. package/test/helpers/ai-tools-format.js +0 -383
  175. package/test/helpers/content/blog-auto-publisher.js +0 -484
  176. package/test/helpers/content/feed-parser.js +0 -528
  177. package/test/helpers/content/ghostii-blocks.js +0 -134
  178. package/test/helpers/content/ghostii-feed-integration.js +0 -404
  179. package/test/helpers/content/ghostii-write-article.js +0 -243
  180. package/test/helpers/environment.js +0 -230
  181. package/test/helpers/infer-contact.js +0 -113
  182. package/test/helpers/merge-line-files.js +0 -271
  183. package/test/helpers/payment/chargebee/parse-webhook.js +0 -413
  184. package/test/helpers/payment/chargebee/to-unified-one-time.js +0 -147
  185. package/test/helpers/payment/chargebee/to-unified-subscription.js +0 -648
  186. package/test/helpers/payment/paypal/parse-webhook.js +0 -539
  187. package/test/helpers/payment/paypal/to-unified-one-time.js +0 -382
  188. package/test/helpers/payment/paypal/to-unified-subscription.js +0 -820
  189. package/test/helpers/payment/stripe/parse-webhook.js +0 -447
  190. package/test/helpers/payment/stripe/to-unified-one-time.js +0 -306
  191. package/test/helpers/payment/stripe/to-unified-subscription.js +0 -708
  192. package/test/helpers/sanitize.js +0 -222
  193. package/test/helpers/slugify.js +0 -394
  194. package/test/helpers/storage.js +0 -194
  195. package/test/helpers/user.js +0 -755
  196. package/test/helpers/webhook-forward.js +0 -418
  197. package/test/mcp/discovery.js +0 -53
  198. package/test/mcp/oauth.js +0 -161
  199. package/test/mcp/protocol.js +0 -268
  200. package/test/mcp/roles.js +0 -188
  201. package/test/mcp/utils.js +0 -245
  202. package/test/routes/admin/create-post.js +0 -364
  203. package/test/routes/admin/database.js +0 -131
  204. package/test/routes/admin/deduplicate-image-alts.js +0 -190
  205. package/test/routes/admin/email.js +0 -114
  206. package/test/routes/admin/firestore-query.js +0 -204
  207. package/test/routes/admin/firestore.js +0 -127
  208. package/test/routes/admin/infer-contact.js +0 -218
  209. package/test/routes/admin/notification.js +0 -198
  210. package/test/routes/admin/post-resize-image.js +0 -184
  211. package/test/routes/admin/post.js +0 -368
  212. package/test/routes/admin/repo-content.js +0 -223
  213. package/test/routes/admin/stats.js +0 -112
  214. package/test/routes/content/post.js +0 -54
  215. package/test/routes/general/uuid.js +0 -115
  216. package/test/routes/marketing/campaign.js +0 -125
  217. package/test/routes/marketing/contact.js +0 -370
  218. package/test/routes/marketing/email-preferences.js +0 -292
  219. package/test/routes/marketing/push-send.js +0 -32
  220. package/test/routes/marketing/webhook-forward.js +0 -61
  221. package/test/routes/marketing/webhook.js +0 -639
  222. package/test/routes/payments/cancel.js +0 -163
  223. package/test/routes/payments/discount.js +0 -80
  224. package/test/routes/payments/dispute-alert.js +0 -320
  225. package/test/routes/payments/intent.js +0 -336
  226. package/test/routes/payments/portal.js +0 -92
  227. package/test/routes/payments/refund.js +0 -179
  228. package/test/routes/payments/trial-eligibility.js +0 -71
  229. package/test/routes/payments/webhook.js +0 -107
  230. package/test/routes/test/authenticate.js +0 -59
  231. package/test/routes/test/schema.js +0 -554
  232. package/test/routes/test/usage.js +0 -342
  233. package/test/routes/user/api-keys.js +0 -156
  234. package/test/routes/user/delete.js +0 -136
  235. package/test/routes/user/feedback.js +0 -104
  236. package/test/routes/user/sessions.js +0 -199
  237. package/test/routes/user/settings-validate.js +0 -82
  238. package/test/routes/user/signup.js +0 -542
  239. package/test/routes/user/subscription.js +0 -99
  240. package/test/routes/user/token.js +0 -73
  241. package/test/routes/user/user.js +0 -157
  242. package/test/rules/notifications.js +0 -410
  243. package/test/rules/payments-carts.js +0 -371
  244. package/test/rules/user.js +0 -396
package/CHANGELOG.md DELETED
@@ -1,1440 +0,0 @@
1
- # CHANGELOG
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
-
7
- ## Changelog Categories
8
-
9
- - `BREAKING` for breaking changes.
10
- - `Added` for new features.
11
- - `Changed` for changes in existing functionality.
12
- - `Deprecated` for soon-to-be removed features.
13
- - `Removed` for now removed features.
14
- - `Fixed` for any bug fixes.
15
- - `Security` in case of vulnerabilities.
16
-
17
- # [5.9.5] - 2026-06-21
18
-
19
- ### Fixed
20
- - **Suspended subscription cancel dead zone.** Users with `subscription.status: 'suspended'` were trapped — the subscribe endpoint required them to cancel first, but the cancel endpoint only accepted `active` subscriptions. Cancel now accepts both `active` and `suspended`. When the processor rejects a suspended cancel (subscription already dead on their end), falls back to directly resetting the user doc to `cancelled`.
21
-
22
- ### Security
23
- - **Supply-chain audited installs.** All `npm install` calls in CLI commands (`install`, setup-tests) now route through `safeInstall()` which prefixes Socket Firewall (`sfw`) when available, blocking confirmed malware before packages reach disk.
24
-
25
- # [5.9.4] - 2026-06-21
26
-
27
- ### Fixed
28
- - **Download-app-link email had blank body.** Template passed `data: {}` so the card rendered logo + signoff + footer but no content. Now populates title, message, and "Download Now" button linking to `{brand.url}/download`.
29
-
30
- # [5.9.3] - 2026-06-21
31
-
32
- ### Changed
33
- - **`admin/hook` route now triggers BEM internal crons.** Path resolution searches BEM's events directory (crons), consumer project root, and consumer hooks/ directory. Supports both function exports (`module.exports = async (opts) => {}`) and class-based hooks. Example: `POST /admin/hook { path: "cron/daily/blog-auto-publisher" }`.
34
- - **MCP `run_hook` tool updated** with expanded description and example paths for all built-in cron jobs.
35
-
36
- ### Available hook paths (built-in BEM crons)
37
- - `cron/daily/blog-auto-publisher` — generate + publish blog articles
38
- - `cron/daily/marketing-newsletter-generate` — pre-generate newsletter content
39
- - `cron/daily/marketing-prune` — prune inactive marketing contacts
40
- - `cron/daily/reset-usage` — reset monthly usage counters
41
- - `cron/daily/data-requests` — process GDPR data export requests
42
- - `cron/daily/expire-paypal-cancellations` — expire pending PayPal cancellations
43
- - `cron/frequent/marketing-campaigns` — send pending marketing campaigns
44
-
45
- # [5.9.2] - 2026-06-20
46
-
47
- ### Added
48
- - Blog generation extended test (`test/content/blog-generate.js`). Run `npx mgr test mgr:content/blog-generate --extended` to exercise the full blog auto-publisher pipeline (source resolution → Ghostii → publish). Supports `BLOG_SOURCE`, `BLOG_NO_PUBLISH`, `BLOG_OPEN` env vars.
49
-
50
- # [5.9.1] - 2026-06-20
51
-
52
- ### Fixed
53
- - Missing `require('node-powertools')` in newsletter generator — broke `powertools.arrayify()` for array content support added in v5.9.0
54
-
55
- # [5.9.0] - 2026-06-19
56
-
57
- ### BREAKING
58
- - **Config key `ghostii` renamed to `blog`.** Consumer projects must migrate their `backend-manager-config.json`: `ghostii[]` → `blog.content[]` with `blog.enabled` and `blog.platform: 'ghostii'`.
59
- - **Field renames in blog content entries.** `articles` → `quantity`, `prompt` → `instructions`, `$app` → `$brand`.
60
- - **Newsletter `claimSources()` removed.** Parent server now serves sources read-only (no claiming). Children track usage locally in `content-sources`.
61
-
62
- ### Added
63
- - **Provider-based blog architecture.** `blog.platform` selects the article-generation provider (dispatched via `require(../content/${platform}.js)`). Only `ghostii` for now.
64
- - **`$parent` source type.** Blog entries can fetch sources from the parent server's pool, filtered by categories, with local dedup tracking.
65
- - **`$brand` source type.** Renamed from `$app` — generic brand-topic generation.
66
- - **New content entry fields.** `tone`, `categories`, `keywords` added to both blog and newsletter content entries. Injected into AI prompts for better content targeting.
67
- - **`links` and `keywords` support in newsletter.** Newsletter AI prompt now weaves in brand links and SEO keywords from content config.
68
- - **`sources` field on newsletter content.** Newsletter entries now explicitly declare source types (default: `['$parent']`), matching blog config shape.
69
- - **Newsletter `content` supports array format.** Matches blog config shape for structural parity.
70
- - **Unified `content-sources` Firestore collection.** Both blog and newsletter track used sources in the same collection with `usedBy` field (`'blog'` or `'newsletter'`).
71
-
72
- ### Changed
73
- - **Cron renamed.** `ghostii-auto-publisher.js` → `blog-auto-publisher.js`.
74
- - **`postPath` defaults to platform name.** Instead of hardcoded `'blog'`, defaults to the platform value (e.g. `'ghostii'`).
75
- - **Consistent field ordering.** `sources → categories → links → instructions → tone → keywords` across blog and newsletter config templates.
76
-
77
- ### Removed
78
- - **`ghostii-auto-publisher.js`** — replaced by `blog-auto-publisher.js`.
79
- - **`claimSources()` from newsletter generator** — parent no longer tracks claim state.
80
-
81
- # [5.8.3] - 2026-06-19
82
-
83
- ### Fixed
84
- - **Ghostii feed tracking timestamps.** `trackFeedItem` now uses BEM's standard `metadata.{created,updated}` with `timestamp` (ISO) and `timestampUNIX` (seconds) instead of `FieldValue.serverTimestamp()`, which failed in the emulator context.
85
-
86
- ### Changed
87
- - **Firestore collection rename.** `ghostii-feed-items` → `ghostii-sources` for clarity and consistency with the OMEGA naming convention.
88
- - **Extended feed test coverage.** Added 5 marketing/social feeds (Social Media Examiner, Hootsuite, Sprout Social, Buffer, Digiday) to the real-feed integration test. Threshold bumped from 2/3 to 5/7.
89
-
90
- # [5.8.2] - 2026-06-19
91
-
92
- ### Fixed
93
- - **Setup crash on fresh projects.** `npx mgr setup` crashed with `Cannot read properties of undefined (reading 'default')` when `.firebaserc`, `firebase.json`, or `backend-manager-config.json` didn't exist. Setup now scaffolds all three from templates before reading from them.
94
- - **`engines.node` hard crash.** Missing `engines.node` in `package.json` threw an unrecoverable error. Now auto-fixes it using the current Node.js major version.
95
- - **`dependencies['backend-manager']` crash.** The local-BEM detection crashed when `backend-manager` was listed in `devDependencies` instead of `dependencies`.
96
-
97
- ### Changed
98
- - **Unified scaffold pass.** Consolidated config scaffolding, `engines.node` fix, and doc defaults into one `[DEFAULTS]` section with a single file reload afterwards. Eliminates scattered scaffolding steps and conditional re-loads.
99
-
100
- ### Added
101
- - **`templates/firebase.json`** — standard Firebase config template (hosting, functions, firestore, database, storage, emulators) scaffolded into new projects by `npx mgr setup`.
102
-
103
- # [5.8.1] - 2026-06-19
104
-
105
- ### Fixed
106
- - **Ghostii 502 timeout fix.** Switched `writeArticle()` from the Firebase Hosting URL (`api.ghostii.ai`) to the raw Cloud Functions URL. Firebase Hosting rewrites have a hard 60-second proxy timeout that caused 502 errors when the AI article pipeline exceeded that limit. The raw URL uses the function's own 5-minute timeout.
107
-
108
- ### Added
109
- - **Ghostii call duration logging.** `writeArticle()` now logs the round-trip time of each Ghostii API call (`[ghostii] writeArticle() completed in Xms`).
110
-
111
- # [5.8.0] - 2026-06-19
112
-
113
- ### Added
114
- - **RSS/Atom feed sources for Ghostii.** New `$feed:<url>` source type in `config.ghostii[]` entries. Parses RSS 2.0, Atom 1.0, and JSON Feed formats, selects unprocessed articles, extracts content, and passes it to the Ghostii API as `sourceContent` for AI-assisted original article generation. Feed items tracked in Firestore (`ghostii-feed-items`) to prevent re-processing. Falls back to `$app` when feeds are unreachable or exhausted.
115
- - **Per-entry Ghostii API overrides.** `config.ghostii[].overrides` object lets each entry customize keywords, length, research, insertImages, headerImageUrl, maxLinks, sectionQuantity, and feedUrl — previously hardcoded in `writeArticle()`.
116
- - **Configurable postPath.** `config.ghostii[].postPath` controls the `_posts/{year}/` sub-folder for published articles (default: `'ghostii'`).
117
- - **Feed parser library.** New `src/manager/libraries/content/feed-parser.js` — `parseFeed()` and `extractArticleContent()` via Cheerio for parsing syndication feeds and extracting readable article text from URLs.
118
- - **New dependencies:** `fast-xml-parser` for RSS/Atom parsing, `cheerio` for article content extraction.
119
-
120
- ### Changed
121
- - **`writeArticle()` accepts overrides and sourceContent.** Signature changed from `({ brand, description, links })` to `({ brand, description, links, sourceContent, overrides })`. Existing callers (newsletter generator) pass no new args and get identical behavior.
122
- - **Newsletter fact paraphrasing.** Newsletter writer prompt now instructs the AI to paraphrase all facts, figures, and statistics from sources — never copy exact numbers or phrasing.
123
-
124
- # [5.7.6] - 2026-06-18
125
-
126
- ### Fixed
127
- - **MCP admin role promotion.** Users with `roles.admin=true` on their Firestore doc now see all 25 admin tools when connecting via OAuth. Previously only the `BACKEND_MANAGER_KEY` granted admin MCP access; user-role admins were limited to 3 tools.
128
-
129
- # [5.7.4] - 2026-06-18
130
-
131
- ### Added
132
- - **Gitignore root proxy artifacts.** Default `.gitignore` template now excludes `/package.json` and `/package-lock.json` at the repo root, so the setup-generated root proxy and any lockfile are not tracked.
133
-
134
- # [5.7.3] - 2026-06-18
135
-
136
- ### Changed
137
- - **Root proxy preinstall forwards to functions/.** `npm install` at the project root now runs `cd functions && npm install` (forwarding deps to where they live) instead of just rejecting. Still exits 1 afterward to prevent npm from creating root-level artifacts.
138
-
139
- # [5.7.2] - 2026-06-18
140
-
141
- ### Fixed
142
- - **Newsletter generation crash.** v5.5.0 refactored `marketing.beehiiv` → `marketing.newsletter` but missed renaming `beehiivConfig` at 3 sites in `newsletter.js` (lines 331, 336, 340). The `ReferenceError` crashed generation after all AI work completed, preventing the campaign doc from being written. Newsletter generation has been silently failing since June 6.
143
- - **HTTPS proxy silent failure.** `serve.js` `_startHttpsProxy` now returns a boolean. The caller uses `httpsReady` (not `httpsEnabled`) for port and env decisions, so when cert generation fails, the server correctly falls back to plain HTTP instead of setting `BEM_HTTPS_PORT` with no proxy listening.
144
- - **AI system prompt injection for array content blocks.** `normalizeOptions` now handles system messages with array content (content blocks) — prepends rules as a `{ type: 'text' }` block. Previously, the if/else-if chain fell through and rules were silently dropped.
145
- - **Setup retry loop treats warns as failures.** Added `warnCount` tracking; the `allPassed` check now includes warns (`testCount + warnCount === testTotal`). Warns no longer trigger unnecessary retries with `--retry N`.
146
- - **Copy-paste: `sender: 'electron-manager'` in setup IPC.** Changed to `'backend-manager'`.
147
- - **Test account creation race condition.** `deleteTestUsers` now uses the emulator's bulk-clear REST API instead of individual `deleteUser()` calls. Individual deletes triggered async on-delete handlers that could clobber freshly-created accounts (80-100% repro rate). Bulk clear eliminates the race entirely.
148
- - **Consent rules test value collision.** Changed test value from `'granted'` to `'forged'` so the write always differs from prior test state.
149
- - **`cancel-too-young` timestampUNIX convention.** `Date.now()` (milliseconds) → `Math.floor(Date.now() / 1000)` (seconds).
150
-
151
- ### Added
152
- - **AI array-content test.** `normalize-options-structured-system-content-as-array-injects-rules` — covers the missing branch.
153
- - **Auth on-delete race condition test.** `test/events/auth-delete-race.js` — proves the emulator race (clobber without mitigation) and verifies both mitigation strategies (wait-for-gone, force-delete).
154
- - **Root package.json setup check.** Validates the project root `package.json` during `npx mgr setup`.
155
-
156
- # [5.7.1] - 2026-06-17
157
-
158
- ### Added
159
- - **MCP shorthand URL.** MCP endpoint now accessible at `/mcp` in addition to `/backend-manager/mcp`. Hosting rewrites updated; consumer projects pick up on next `npx mgr setup`.
160
- - **`/register` hosting rewrite.** Dynamic client registration endpoint now included in the default hosting rewrite pattern.
161
-
162
- # [5.7.0] - 2026-06-17
163
-
164
- ### Added
165
- - **MCP role-based tool scoping.** 25 tools (was 19) with admin/user/public roles. Admin sees all, user sees `get_user` + `get_subscription` + `health_check`, unauthenticated gets 401 triggering OAuth. Defense-in-depth: route-level auth still validates.
166
- - **MCP OAuth user authentication.** OAuth 2.1 with PKCE + dynamic client registration (RFC 7591). User sign-in via consumer website's `/token` page → Firebase ID token → exchanged for `api.privateKey`. Verified end-to-end in Claude Desktop.
167
- - **MCP consumer tools.** Consumer projects define custom MCP tools in `functions/mcp.js` — route delegation (works on stdio + HTTP) or handler mode (HTTP only). Consumer tools override same-name built-ins.
168
- - **MCP tool annotations.** `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` on all tools. Claude Desktop shows read/write categorization and human-readable titles.
169
- - **6 new MCP tools:** `update_post`, `update_campaign`, `delete_campaign`, `create_contact`, `delete_contact`, `get_payment_portal`.
170
- - **HTTPS local dev.** `npx mgr serve` starts an HTTPS proxy on port 5002 (firebase serve on 5443 internally) with auto-generated mkcert certificates. Claude Desktop requires HTTPS for MCP connectors. Disable with `--no-https`.
171
- - **MCP CLI `--token` flag.** `npx mgr mcp --token <api-key>` for user-level stdio connections.
172
-
173
- ### Changed
174
- - **MCP discovery endpoints** use root-level issuer per RFC 8414 (was path-scoped, broke Claude Desktop's discovery chain).
175
- - **`getApiUrl()`** returns `https://localhost:<port>` when `BEM_HTTPS_PORT` is set (serve command sets this automatically).
176
- - **`cancel_subscription`, `refund_payment`, `generate_uuid`** moved from user/public to admin role (destructive operations and dev utilities shouldn't be in user-facing MCP).
177
- - **`resolveConsumerAuthUrl()`** uses `Manager.getWebsiteUrl()` (auto-resolves localhost in dev) instead of `brand.url` (always production).
178
-
179
- # [5.6.6] - 2026-06-15
180
-
181
- ### Added
182
- - **`'warn'` return type for setup checks.** `run()` can now return the string `'warn'` for non-blocking failures — the check prints as `⚠` with detail lines from `getWarning()`, is counted in the summary (`36 passed, 1 warned, 0 failed`), but does **not** halt setup. `BaseTest` provides a default `getWarning()` returning `[]`. `Summary` gains a `warn(name, details)` method alongside `pass()` and `fail()`.
183
- - **Java setup check** (`setup-tests/java-installed.js`). Checks whether Java is installed (required by the Firebase Firestore emulator for testing). Uses the `'warn'` return type — setup continues without Java, but the summary reports it.
184
- - **Java pre-check in test runner.** `npx mgr test` now checks for Java before starting the emulator and fails fast with a clear message (`Java is required to run tests`) instead of the raw emulator crash.
185
-
186
- ### Changed
187
- - **Firebase CLI and auth setup checks no longer halt setup.** Both checks now use the `'warn'` return type instead of throwing from `fix()`. Missing Firebase CLI or unauthenticated state is reported in the summary but does not block the remaining checks.
188
-
189
- # [5.6.5] - 2026-06-14
190
-
191
- ### Added
192
- - **Cross-provider native tool calling in the AI library (agentic loops).** `ai.request()` now supports a unified tools interface on every text provider: `tools.list` accepts normalized function tools (`{ name, description, parameters }` — JSON Schema), `tools.choice` accepts `'auto' | 'required' | 'none' | { name }`, and the response adds `toolCalls: [{ id, name, arguments }]` (arguments parsed) plus `stopReason: 'tool_use' | 'end' | 'max_tokens'`. The Anthropic and claude-code providers gain native `tool_use` via a shared pure formatter (`providers/anthropic-format.js` — tool defs → `input_schema`, choice mapping incl. `required`→`any`, response extraction); the OpenAI provider normalizes function tools to the Responses API envelope (hosted tools like `{ type: 'web_search' }` still pass verbatim, and throw a clear error on Anthropic) and extracts `function_call` items. Multi-turn loop continuation is first-class through `options.messages`: `{ role: 'assistant', toolCalls }` and `{ role: 'tool', toolCallId, content }` turns map to each provider's wire format (consecutive tool results merge into one Anthropic user turn; OpenAI gets `function_call`/`function_call_output` items), raw Anthropic block arrays replay verbatim, and `normalizeOptions()` no longer string-flattens structured conversations (system-prompt injections still apply). OpenAI additionally gains a direct-messages mode: passing `messages[]` now sends ALL turns (previously middle turns were silently dropped in favor of prompt/message/history). JSON parsing (`response: 'json'`) is skipped on tool-call turns, where empty text is the normal intermediate state. Return shapes stay backward-compatible (`{ content, output, tokens, raw }` unchanged; OpenAI now also returns `raw`). Covered by `test/helpers/ai-tools-format.js` (pure, 22 cases) and `test/ai/tools-live.js` (extended-mode real 2-step tool loops on both providers; OpenAI live-validated).
193
- - **Deterministic `test` AI provider** (`providers/test.js`, registered as `provider: 'test'`) — the AI analog of the `test` payment processor: a first-class provider that consumer suites drive with directives embedded in the last user message (`[[tool:name {json}]]`, `[[tools:[...]]]` for parallel calls, `[[reply:{json}]]`, `[[delay:ms]]`, `[[error:msg]]`), consumed sequentially across the turns of a tool loop. Refuses to run outside development/testing (Manager environment detection; falls back to `BEM_TESTING`/`FUNCTIONS_EMULATOR` signals when constructed without a Manager). Lets consumer chat/agent routes test the full loop — Firestore writes, usage, locks, tool executors — against the real emulator with zero paid API calls. Covered by `test/helpers/ai-test-provider.js`.
194
- - **OpenAI provider constructor hardened** — `assistant.Manager` access now uses optional chaining (matching the Anthropic provider), so the provider can be constructed with a minimal assistant context (direct construction in tests/tools).
195
- - **`docs/cdp-debugging.md` — launching a controllable browser (mirrored across UJM/BEM/BXM/EM).** The canonical Chrome launch for agents and humans: CDP port + REQUIRED dedicated `--user-data-dir` (Chrome 136+ silently ignores the debug port on the default profile — verified on 149), the persistent agent profile (`~/Library/Application Support/chrome-profiles/agent` — log in once, state survives relaunches, verified), the shared-instance model (CDP is multi-client — agents share the one logged-in Chrome on one port, one tab each; a second profile/port only for a second identity), safe quit by profile match, and driving via the `chrome-devtools` MCP (`CHROME_CDP_PORT` set before the session) or any CDP client. BEM flavor: aimed at verifying the frontend against your routes — watch network payloads and drive auth'd flows through the real UI. Indexed in CLAUDE.md.
196
-
197
- # [5.6.4] - 2026-06-11
198
-
199
- ### Changed
200
- - **Consent side effects moved off shared test accounts (journey-account isolation).** The marketing webhook suite's revoke-event tests (`test/routes/marketing/webhook.js`) repeatedly write `consent.marketing.status = 'revoked'` to their target account — persistent side-effect data that previously landed on the shared `basic` account, leaving it revoked for the remainder of every run (and, since the v5.6.3 library consent gate, changing `sync()`/`add()` behavior for every later suite touching it). They now target a dedicated `journey-webhook-revoke` account. The extended-mode lifecycle suite (`test/email/marketing-lifecycle.js`) likewise now syncs a dedicated `journey-marketing-sync` account (`_test.allow_*` prefix) instead of the shared `consent-granted` sentinel (which the signup + consent-lifecycle suites rely on). Its cleanup step also now deletes the contact the suite actually created — previously it deleted the ADMIN account's contact, which (post-v5.6.3) revoked admin's doc consent mid-run AND left the synced contact behind in SendGrid/Beehiiv after every extended run. `docs/test-framework.md`'s journey-account rule now lists `consent.marketing` writes as a trigger. Validated: marketing route suites pass (46 passing / 10 env-gated skips / 0 failures).
201
-
202
- ### Fixed
203
- - **Anonymous HMAC unsubscribe tests now actually run.** The self-test boot (`src/cli/commands/test.js`) injects a test-only `UNSUBSCRIBE_HMAC_KEY` into the process env (the emulated functions inherit it — same mechanism as the fixture webhook key), closing the fixture gap that left all 8 anon-HMAC tests in `test/routes/marketing/email-preferences.js` failing as "known env gap". Those tests are the route-level coverage for the v5.6.3 HMAC changes (signature validation, rate limiting, consent mirroring); marketing suites went from 38 passing + 8 failing to 46 passing + 0 failing.
204
-
205
- # [5.6.3] - 2026-06-11
206
-
207
- ### Fixed
208
- - **NeverBounce single-check result parsing — every signup mailbox check has failed since v5.5.1.** `validation-provider-neverbounce.js` compared `data.result` against numeric codes (`data.result === 0 || 3 || 4`), but the NeverBounce v4 single/check API returns string textcodes (`"valid"`, `"invalid"`, ...) — so every completed check returned `{ valid: false, status: 'unknown' }`, including deliverable mailboxes, and the signup route silently skipped marketing sync for ALL signups. Production-confirmed via GCF logs: somiibo June 7 = 30 synced / 0 failed; June 9–11 = 0 synced / 93 failed; first failure 25 minutes after the v5.5.1 deploy (which switched signup to `ALL_CHECKS` and exposed the latent v5.5.0 bug). New `parseResult()` handles textcodes (canonical), tolerates numeric codes, and normalizes `catch-all` → `catchall`; allowed results stay valid/catchall/unknown. 11 regression parse cases added to `validation.test.js` (suite now 69 cases); fix live-validated against the real API. ZeroBounce provider audited — already string-based, no change.
209
- - **Library-level marketing consent gate.** `Marketing.sync()`/`Marketing.add()` (`src/manager/libraries/email/marketing/index.js`) now skip users whose `consent.marketing.status` is the literal string `'revoked'` and return `{ blocked: 'consent', email }` (mirroring the `{ blocked: 'validation', ... }` shape) — BEFORE validation and provider calls. Previously the gate existed only at SOME call sites, so the payments on-write sync (`events/firestore/payments-webhooks/on-write.js`) and the admin PUT re-sync re-added users who had unsubscribed. `sync()` gates after the user doc is resolved (doc or uid); `add()` looks up the user by email first (same `auth.email` query as the webhook processors) — no user doc or lookup failure proceeds (fail open). ONLY `'revoked'` blocks: missing/null consent proceeds, so legacy users without a `consent` field keep syncing. `remove()` stays ungated. Covers all callers including the legacy API-command twins (`add-marketing-contact.js`). Gate logic unit-tested in `src/manager/libraries/email/marketing/consent-gate.test.js` (28 plain-node cases, no emulator).
210
- - **Anonymous HMAC unsubscribe now removes the contact from Beehiiv too** (`routes/marketing/email-preferences/post.js`). The email-footer one-click unsubscribe only suppressed the SendGrid ASM group, leaving the contact live on Beehiiv — a compliance bug. The route now also calls `mailer.remove(email)` (best-effort, after the ASM call succeeds).
211
- - **Anonymous HMAC re-subscribe now actually re-adds the contact.** Previously it only lifted the ASM suppression and wrote consent `granted` — the contact was never re-added to providers. `mirrorAnonymousToUserDoc` now returns the matched uid (or null); the route calls `mailer.sync(uid)` for matched users (the mirror writes `granted` first, so the library consent gate passes — no bypass flags) or `mailer.add({ email, source: 'resubscribe' })` for pure newsletter contacts. Best-effort, same testing guard as the ASM call.
212
- - **Admin `DELETE /marketing/contact` now sticks** (`routes/marketing/contact/delete.js`). The route removed the contact from providers but left `consent.marketing.status = 'granted'`, so any later sync re-added the contact. After the provider removal it now mirrors `consent.marketing.status = 'revoked'` (`revokedAt` with `source: 'admin'`, same write shape as the webhook processors) to the matching user doc — best-effort, silent when the email maps to no user.
213
- - **Stale docs/comments corrected in the same change set:** `docs/consent.md` now describes the shipped library gate, cross-provider HMAC unsubscribe, re-subscribe re-add, and admin-DELETE revoke mirror (new capture point 5); `validation.js` header no longer claims signup uses "disposable check only" (it runs `ALL_CHECKS` before marketing sync); `marketing/index.js` header no longer attributes the signup sync to the "Auth on-create handler" (it's the `/user/signup` route).
214
-
215
- # [5.6.2] - 2026-06-11
216
-
217
- ### Fixed
218
- - **4 stale framework tests aligned with shipped v5.5.4–v5.5.6 validation behavior.** The default `npx mgr test` run failed 4 tests whose expectations predated intentional source changes: `test/email/validation.js` still expected all-numeric (`123456@`) and short letter+number (`a123@`) local parts to be blocked (both patterns were removed in v5.5.6 after NeverBounce confirmed real users — QQ emails, real Gmail accounts — were being blocked; tests renamed `localpart-all-numeric-allowed` / `localpart-letter-plus-numbers-allowed`) and expected the pre-v5.5.5 `DEFAULT_CHECKS`/`ALL_CHECKS` lists (now include `typo`, and `dns` in `ALL_CHECKS`); `test/routes/marketing/webhook.js`'s bounce test sent no `bounce_classification`, which v5.5.4 deliberately skips (renamed `sendgrid-hard-bounce-event-handled`, now sends `'Invalid Address'`).
219
-
220
- ### Added
221
- - **Suite coverage for the v5.5.5 `typo` + `dns` email validation checks** (previously only covered by the standalone `validation.test.js` script): typo-domain blocking (`gamil.com`, `gmail.con`) + correct-domain pass-through, dns-not-in-default-checks, an offline-safe dns positive (network errors skip, never block), and an extended-gated (`TEST_EXTENDED_MODE`) dns negative for nonexistent domains.
222
- - **Suite coverage for the v5.5.4 bounce-classification filter**: `dropped` + `'Invalid Address'` revokes, technical bounce (`'Technical Failure'`) skipped, and unclassified bounce skipped — locking in that sender-side bounces never revoke recipient consent.
223
-
224
- # [5.6.1] - 2026-06-11
225
-
226
- ### Added
227
- - **`docs/audit.md` — full-audit check catalog (`/omega:bem audit`).** ID'd, severity-graded checks with scope auto-detect (consumer vs framework via `functions/package.json`): mirrored universal checks (U-01..U-14 — tests at every surface, sanitization, secrets incl. `service-account.json`, config canon, doc parity, dead/legacy patterns, dep health, …), BEM-specific checks (BEM-01..BEM-09 — name-matched context-object schemas, the required-vs-default footgun, ownership checks + `assistant.respond()`, index.js/rewrites wiring, Firestore canon, usage helper + rate limits, composite indexes, auth gates, rules coverage), and framework-repo checks (F-01..F-04). Findings persist to `functions/.temp/audit/claude-audit.md`; fixes run as a severity-ordered TodoWrite loop ending with a green `npx mgr test`. Wired to the `omega:bem` router's Audit process; `docs/audit.md` is mirrored across UJM/BXM/EM. Indexed in CLAUDE.md.
228
-
229
- ### Changed
230
- - **package.json `keywords` corrected** — replaced the thin generic set (`cli`, `backend manager`, `firebase`) with accurate, discovery-oriented ones (`firebase`, `firebase-functions`, `cloud-functions`, `firestore`, `backend`, `serverless`, `api`, `express`, `cli`). npm-listing metadata only; no behavior change. Mirrored across UJM/BXM/EM.
231
-
232
- # [5.6.0] - 2026-06-11
233
-
234
- ### Added
235
- - **`docs/migration.md` (action-skill consolidation).** The standalone `BEM:migrate` skill was deleted and folded into `omega:bem` as a process checklist; its playbooks landed in the repo as `docs/migration.md` — env-var migration (runtime config → top-level `functions/.env` keys with the full mapping table), legacy code conversion (`Manager.config.*` → `process.env.*`), and route/schema migration to the current context-object/flat formats. Indexed in CLAUDE.md.
236
- - **Dev-process guidance relaxed: only `npx mgr serve` / `npx mgr emulator` are off-limits.** The "NEVER run" rule in CLAUDE.md now prohibits only the long-running dev processes (instruct the user to start them if they aren't running; read `functions/*.log`, never tail) — `npx mgr test` is fine to run (it auto-starts its own emulator).
237
- - **Skills-as-routers migration — `docs/firestore.md` (new) + `routes.md`/`schemas.md` rewrites + `test-framework.md`/`usage-rate-limiting.md` extensions.** Framework facts migrated from the `omega:bem` skill into the repo so they version-match the installed package, with stale content corrected against source: `routes.md`'s consumer-route recipe now shows the CURRENT context-object handler (`module.exports = async ({ Manager, assistant, user, settings, ... })` — middleware calls `routeHandler(context)`; the old `Route.prototype.main` constructor example was stale) plus the CRUD method-file table, ownership checks, plural naming, firebase.json rewrite syntax + first-match ordering, and the `functions/index.js` entry pattern; `schemas.md` rewritten to the current contract (context object `{ assistant, user, data, method, headers, geolocation, client }` → FLAT schema with in-function plan branching — the old positional-args + `defaults:/premium:` tier examples were stale) plus field properties (`value`/`clean`/function `required`), the required-vs-default footgun, and the ID-generation/path-extraction patterns; new `firestore.md` carries the Firestore conventions (`.doc('col/id')` style, NO subcollections, ~500-doc cursor-paginated batch reads, `metadata.{created,updated}` timestamps, mirror-the-doc response format + delete-don't-redact); `test-framework.md` gains the `rules` client reference (`asAccount`/`expectSuccess`/`expectFailure` + seed-as-admin pattern), the journey-account isolation rule (CRITICAL — never use shared accounts with the `test` processor), naming conventions, and common patterns (auth rejection, concurrent-duplicate rejection, suite cleanup); `usage-rate-limiting.md` gains the core API table (`validate`/`increment`/`update`/`getLimit`/`getProduct`/`getUsage`) and the never-write-usage-manually rules. All indexed in CLAUDE.md; the skill is now a thin router (pointers + hard rules + process checklists).
238
- - **`--extended` CLI flag for cross-framework parity.** `npx mgr test --extended` is now the CLI shorthand for the shared, unprefixed `TEST_EXTENDED_MODE` env var (standardized across BEM/BXM/UJM/EM) — equivalent to `TEST_EXTENDED_MODE=true npx mgr test`. The flag is read pre-flight in `src/cli/commands/test.js` (before the `captureSyncedEnv`/`writeTestMode` pre-flight), so it propagates to BOTH the test-runner subprocess (`{ ...process.env }`) AND the live emulator (via `.temp/test-mode.json`) exactly like the env var. The env-var path keeps working — env var OR flag opts into REAL external services (default: skipped).
239
- - **Framework self-test from the repo (bundled fixture + `BEM_TEST_BOOT_PROJECT`).** `npx mgr test` run from the backend-manager repo now boots a bundled fixture Firebase project ([`src/test/fixtures/firebase-project/`](src/test/fixtures/firebase-project)) and runs a `test/boot/` smoke suite (emulator boots → fixture `Manager.init()` wires `bm_api` → health returns 200), instead of failing with "no firebase.json". Brings BEM into parity with BXM's `BXM_TEST_BOOT_PROJECT` / UJM's `UJ_TEST_BOOT_PROJECT`. The runner symlinks the local `backend-manager` (+ `firebase-admin`/`firebase-functions`) into the fixture, injects the fixture admin keys, and generates a throwaway emulator-only service account at runtime (all gitignored). `BEM_TEST_BOOT_PROJECT=<path>` overrides the fixture to self-test against a real consumer. The `boot/` smoke is excluded from consumer runs; the full `routes`/`events`/`rules` suites still run against a real consumer as before.
240
- - **Docs parity — new `docs/build-system.md` + `docs/logging.md`.** `build-system.md` documents BEM's deliberate outlier status (no consumer build pipeline; framework prepare-package; deploy flow); `logging.md` is now the SSOT for the `functions/*.log` file table (extracted from test-framework.md, which keeps a pointer — mirrors EM/BXM/UJM). Both indexed in CLAUDE.md → Documentation.
241
- - **Test coverage convention (docs).** New mirrored "Test coverage" sections in `CLAUDE.md`, `docs/test-framework.md`, `src/defaults/CLAUDE.md`, and `src/defaults/test/README.md` — every feature ships with tests at every surface it exposes (logic via handler suites, wiring via `http.as(...)` round-trips, rules suites when Firestore rules change); a surface is skipped only when the feature genuinely doesn't have one. UI coverage explicitly lives in the consuming frontends. Mirrored across EM/BXM/UJM.
242
- - **Universal `mgr:` test source prefix.** `npx mgr test` now accepts the universal cross-framework `mgr:` prefix (alias for `bem:`) to run framework-only tests, complementing the existing `bem:` and `project:` prefixes. `npx mgr test mgr:` runs all framework tests, `npx mgr test mgr:<path>` runs framework tests matching a path, and multiple space-separated targets compose (e.g. `npx mgr test bem:rules project:routes`).
243
-
244
- ### Changed
245
- - **Router skill renamed `BEM:patterns` → `omega:bem`** — all framework skills now live under the `omega:` namespace (`omega:em`/`omega:bxm`/`omega:ujm`/`omega:bem` + the `omega:main` hub). CLAUDE.md's Recommended skills section updated.
246
- - **`docs/testing.md` renamed `docs/test-framework.md`** (H1 `# Testing` → `# Test Framework`) for cross-framework doc-file parity — EM/BXM/UJM all name their test reference `docs/test-framework.md`, and the mirrored docs must match down to the file name. All references updated (`CLAUDE.md`, `README.md`, `docs/*.md` cross-links, `src/defaults/CLAUDE.md`, `src/defaults/test/_init.js`, historical CHANGELOG links).
247
- - **Log files renamed for cross-framework parity.** `functions/serve.log` → `functions/dev.log` (the `npx mgr serve` dev-server output) and `functions/logs.log` → `functions/production.log` (the `npx mgr logs` Cloud Logging output). The `dev`/`test` names now match EM/BXM/UJM; `emulator.log` and `test.log` are unchanged. BEM logs still live in `functions/` (not `logs/`) — that directory is a deliberate exception so they sit beside firebase-tools' own `*-debug.log` files. The watcher reset sentinel `serve.log.reset` is correspondingly `dev.log.reset` (internal, in `.temp/`).
248
-
249
- ### Fixed
250
- - **`npm publish` vs the fixture's runtime symlinks.** New `prepublishOnly` script removes `src/test/fixtures/firebase-project/functions/node_modules` before packing — the self-test's `backend-manager` symlink points back at the repo root, and prepare-package's publish-time cleanup walk (`jetpack.find` with a top-level-only `!node_modules/**` exclusion) followed the cycle until `ENAMETOOLONG`. The symlinks are throwaway runtime artifacts; the next self-test run regenerates them via `linkFixtureDeps()`.
251
- - **Fixture `.firebaserc` re-included over the global `.gitignore` rule** (`!src/test/fixtures/firebase-project/.firebaserc`) — the emulator boots with no `--project` flag and resolves the demo project from `.firebaserc`, so a fresh clone's self-test would have failed without it.
252
-
253
- # [5.5.4] - 2026-06-09
254
-
255
- ### Fixed
256
- - **SendGrid bounce/dropped webhook filtering.** Bounce and dropped events now only revoke marketing consent when `bounce_classification` is `'Invalid Address'`. Technical bounces (DMARC, TLS, DNS failures) are sender-side issues and no longer revoke the recipient's consent.
257
-
258
- ### Changed
259
- - **`isSupported()` receives full parsed event.** All webhook processors (`sendgrid`, `beehiiv`) now receive the full parsed event object instead of just the event type string, enabling classification-aware filtering.
260
- - **Config template `parent` field.** Added `parent` field to `backend-manager-config.json` template for newsletter-sources provider configuration.
261
-
262
- # [5.5.3] - 2026-06-08
263
-
264
- ### Fixed
265
- - **Per-provider error isolation in Marketing.sync/add/remove.** Added `.catch()` to each provider promise so one provider failing (e.g. SendGrid timeout during `buildFields`) no longer kills the other. Each provider now resolves independently with `{ success: false, error }` on failure, and the function always returns a per-provider status object instead of throwing.
266
-
267
- # [5.5.2] - 2026-06-08
268
-
269
- ### Fixed
270
- - **Await signup async operations.** `syncMarketingContact()`, `mailer.sync()`, and `sendWelcomeEmails()` were fire-and-forget — the Cloud Function returned the HTTP response while NeverBounce validation and provider syncs were still in flight, causing premature shutdown and request timeouts. All are now properly awaited.
271
-
272
- ### Changed
273
- - **Welcome emails send in parallel.** `sendWelcomeEmails()` now uses `Promise.all()` for concurrent sends while still awaiting completion.
274
- - **Log inferred contact name.** Signup route now logs the `inferUserContact` result for observability.
275
-
276
- # [5.5.1] - 2026-06-07
277
-
278
- ### Changed
279
- - **Test HTTP client sends plain requests.** `http.post('route')` now goes to `/${route}` instead of `/backend-manager/${route}`. All BEM test files updated to explicitly include `backend-manager/` prefix. Consumer projects can now test their own routes directly (e.g. `http.post('projects', {...})`).
280
- - **`_processMiddleware` checks consumer routes.** Falls back to BEM's own routes directory only if the consumer doesn't have a matching route — enables consumer routes to work through `bm_api`.
281
- - **Signup uses full email validation.** `syncMarketingContact` now runs `ALL_CHECKS` (including NeverBounce mailbox verification) before syncing to marketing lists.
282
-
283
- # [5.5.0] - 2026-06-06
284
-
285
- ### BREAKING
286
- - **Marketing config keys renamed from platform-specific to role-based.** `marketing.sendgrid` → `marketing.campaigns` (with `platform: 'sendgrid'`), `marketing.beehiiv` → `marketing.newsletter` (with `platform: 'beehiiv'`). Response object keys (`providers.sendgrid`/`providers.beehiiv`) renamed to `providers.campaigns`/`providers.newsletter`. Seed campaign provider arrays updated to match. All consumer `backend-manager-config.json` files must be updated.
287
-
288
- ### Added
289
- - **NeverBounce mailbox verification.** Added as the preferred mailbox verification provider (`NEVERBOUNCE_API_KEY`), with ZeroBounce kept as fallback. Provider logic extracted into pluggable `validation-provider-neverbounce.js` and `validation-provider-zerobounce.js` modules.
290
- - **`platform` property** on marketing config roles — specifies which provider backs each role (e.g. `platform: 'sendgrid'` under `campaigns`).
291
-
292
- ### Changed
293
- - **Mailbox verification is now provider-agnostic.** `validation.js` dispatches to whichever provider has an API key set, instead of hardcoding ZeroBounce.
294
-
295
- # [5.4.1] - 2026-06-06
296
-
297
- ### Added
298
- - **Push notification campaigns.** Marketing campaign POST route and cron now handle `type: 'push'` with brand-aware icon/clickAction defaults and test-mode owner filtering.
299
- - **`filters` field** in campaign schema for push notification targeting.
300
- - **Test filtering docs** in CLAUDE.md and docs/test-framework.md (`npx mgr test <path>`, `bem:`, `project:` prefixes).
301
-
302
- ### Fixed
303
- - **Email migration shims (temporary).** Old template names (`default` → `card`, `core/engagement/feedback` → `feedback`) and old data format (`data.body` → `data.content`) mapped for queued emails saved before the MJML migration.
304
- - **Double-escaped URLs in email templates.** Button hrefs, signoff links, and CTA URLs were HTML-escaped via `escape()`, breaking URLs with `&` characters. Removed `escape()` from href attributes across all templates while keeping it on display text.
305
- - **Spacer elements.** Replaced `&nbsp;` text hacks with proper `mj-spacer` elements in base template.
306
- - **Notification library defaults.** Icon and click URL now use brand config instead of hardcoded ITW values.
307
-
308
- # [5.4.0] - 2026-06-05
309
-
310
- ### Added
311
- - **Unified MJML email system.** All emails (transactional, marketing, newsletter) rendered server-side via composable MJML templates. No more SendGrid dynamic templates (`d-xxx` IDs).
312
- - **Composable template blocks** in `base.js`: `skeleton()`, `logo()`, `cardWrapper()`, `signoff()`, `button()`, `footer()` — templates compose what they need.
313
- - **4 email templates:** `card` (default), `plain` (full-width personal), `order` (9 payment event types), `feedback` (rating faces with gift card incentive).
314
- - **Shared preparation layer** (`prepare.js`): brand resolution, sender resolution, markdown rendering, signoff defaults, categories, HMAC unsubscribe URLs, template data building.
315
- - **Marketing campaign CTA buttons** and discount codes in seed campaigns. Each audience segment gets tailored copy + audience-matched discount code.
316
- - **`id="email-content"` wrapper** on all compiled email HTML (matches legacy SendGrid template convention).
317
- - **`docs/email-system.md`** deep reference for the entire email pipeline.
318
- - **New test accounts:** `intent-discount-validation`, names on all journey accounts for email personalization.
319
- - **New test:** `journey-payments-discount` for subscription with promo code.
320
- - **Email tests reorganized** under `test/email/` mirroring `src/manager/libraries/email/`.
321
-
322
- ### Changed
323
- - **`data.body` → `data.content`** across all email callers and templates. Template-specific payload lives in `data.content`; system metadata in `data.brand`/`data.email`/`data.personalization`.
324
- - **`data.order` → `data.content`** for the order template. All payment transition handlers updated.
325
- - **Marketing campaigns consolidated:** top-level `content`/`discountCode` → `data.content.message`/`data.content.discountCode`. Frontend calendar updated.
326
- - **UTM auto-tagging tags ALL HTTP links**, not just brand-domain. Campaign name derived from caller's first category. Signup signoff URLs no longer hardcode UTM.
327
- - **Marketing unsubscribe** uses `<%asm_group_unsubscribe_raw_url%>` for per-recipient links in Single Sends.
328
- - **Schema defaults** `'default'` → `'card'` in admin/email, marketing/campaign, MCP tools.
329
- - **Seed campaign enforced templates** `'core/card'` → `'card'`.
330
- - **Seed campaign discount codes** matched to audience-restricted codes from `discount-codes.js`.
331
-
332
- ### Fixed
333
- - **UTM tags missing on template-generated links.** `renderEmail()` now calls `tagLinks()` on compiled HTML (CTA buttons, footer, signoff links).
334
- - **Newsletter fallback alert email blank.** Used old `data.body` key — updated to `data.content`.
335
- - **`IMAGE_MAX_DIMENSION` test expected 4096** but source was changed to 2048.
336
- - **Intent discount test used hardcoded `frequency: 'monthly'`** — product only has `daily`. Now reads from product config.
337
- - **Marketing lifecycle sync test** used `_test.admin` email blocked by validation. Now syncs the lifecycle test contact.
338
- - **AI inference test assertions** too strict — softened to tolerate AI flakiness.
339
- - **Test account isolation:** `intent-discount-validation` gets its own account to avoid pollution from journey tests.
340
-
341
- ### Removed
342
- - **SendGrid template ID references** from schemas and seed campaign enforced fields.
343
- - **`core/` template aliases** — callers use direct names (`card`, `order`, `feedback`, `plain`).
344
- - **Top-level `content` and `discountCode`** fields from marketing campaign schema.
345
-
346
- # [5.3.5] - 2026-06-04
347
-
348
- ### Added
349
- - **Campaign integration test.** `test/routes/marketing/campaign.js` covers send-to-test_admin (extended mode), future-campaign-pending, and auth gate checks.
350
- - **Provider observability logs.** SendGrid: `resolveFieldIds`, `addContact` (email + list + field count), `buildFields` (warns on unmapped fields). Beehiiv: `addSubscriber` (email + publication + field count). These were completely silent before.
351
-
352
- ### Fixed
353
- - **Beehiiv fallback alert used `brand.url` for email domain.** Subdomain projects (e.g. Ultimate Jekyll) built `alerts@ultimate-jekyll.itwcreativeworks.com` instead of the correct `alerts@itwcreativeworks.com`. Now derives domain from `brand.contact.email`.
354
-
355
- # [5.3.4] - 2026-06-04
356
-
357
- ### Added
358
- - **`monthly-weekday` recurrence pattern.** Campaigns can now recur on the Nth weekday of each month (e.g., 2nd Wednesday). New `getNextOccurrence` case + `nextNthWeekday()` helper.
359
- - **Minute precision for campaign scheduling.** All scheduling helpers (`nextWeekday`, `nextNthWeekday`, `nextMonthDay`, `getNextOccurrence`) now accept an optional `minute` parameter (previously hour-only).
360
- - **Emulator project mismatch detection.** Health check returns `projectId`; the test runner compares it (via Emulator Hub + health endpoint) and aborts immediately if the running emulator belongs to a different project.
361
- - **CDN image pre-scaling.** `admin/post` adds `?w=&q=` params to Unsplash URLs before downloading so the CDN delivers a pre-scaled image (~314KB vs 3.8MB) and sharp never decodes a massive original.
362
- - **Emulator orphan cleanup.** `npx mgr emulator` sweeps all emulator ports after shutdown and kills orphaned Java processes (Firestore, Database, PubSub) that survived SIGTERM.
363
- - **SendGrid sender identity resolution.** `resolveSenderIds()` fetches verified senders and maps `from_email → sender_id` for Single Send campaigns.
364
-
365
- ### Changed
366
- - **Campaign scheduling helpers consolidated into `constants.js` (SSOT).** `getNextOccurrence`, `nextWeekday`, `nextNthWeekday`, and `nextMonthDay` were duplicated across `seed-campaigns.js` and both cron jobs — now imported from a single source.
367
- - **Default campaign send times changed to Wednesday 10:30 AM PT (17:30 UTC).** Sale campaigns use the 2nd Wednesday of each month (`monthly-weekday`); newsletter sends every Wednesday (`weekly`). Previously: 15th of month at 14:00 UTC / Monday at 10:00 UTC.
368
- - **`IMAGE_MAX_DIMENSION` reduced from 4096 to 2048.** Blog header images capped at 2048px on the long edge. Reduces peak memory in `admin/post` from ~71MB to ~11MB decoded. Matches UJM's `imagemin.js`.
369
- - **`sharp.cache(false)` in `resizeImage()`.** Disables sharp's pixel cache so decoded buffers are freed immediately between images, preventing OOM on 256MB Cloud Functions.
370
- - **Emulator SIGINT handling.** Ctrl+C handler is now synchronous (prevents Node from exiting before shutdown completes). Second Ctrl+C force-kills. Port sweep runs after exit regardless of spam.
371
- - **SendGrid `createSingleSend` fixes.** Resolves `sender_id` from Sender Identities, builds `html_content` from preheader + dynamic content, uses `resolvedSettings` for segment mapping.
372
-
373
- ### Fixed
374
- - **Marketing segment resolution used wrong settings object.** `sendCampaign` passed `settings.segments` instead of `resolvedSettings.segments`, ignoring campaign-level overrides.
375
-
376
- # [5.3.3] - 2026-06-03
377
-
378
- ### BREAKING
379
- - **Payment webhook routes no longer accept `BACKEND_MANAGER_KEY` as a fallback.** `/payments/webhook` and `/payments/dispute-alert` now only accept `BACKEND_MANAGER_WEBHOOK_KEY`. All consumer brands must have `BACKEND_MANAGER_WEBHOOK_KEY` set in their `.env` and registered with their payment providers (Stripe, PayPal, Chargebee, Chargeblast) before upgrading to this version.
380
-
381
- # [5.3.2] - 2026-06-03
382
-
383
- ### Added
384
- - **Newsletter HTML preview via GitHub Pages.** Enabled GitHub Pages on the `newsletter-assets` repo so newsletter HTML renders natively in the browser. Added `PAGES_BASE` constant and `previewUrl` to `image-host.js` uploads, threaded through to the Firestore campaign `assets` doc and the Beehiiv fallback alert email.
385
-
386
- ### Changed
387
- - **Cleaner Beehiiv fallback email.** Restructured the asset links to use clean anchor text with a nested list (preview / raw HTML) under "Full HTML" instead of displaying raw GitHub URLs.
388
- - **Download-app-link email subject.** Friendlier subject line format.
389
-
390
- # [5.3.1] - 2026-06-02
391
-
392
- ### Fixed
393
- - **Leaking `wonderful-fetch` mock in the webhook-forward unit test.** `test/helpers/webhook-forward.js` installs a stub into `require.cache['wonderful-fetch']` at module load (the sanctioned cross-project fan-out exception — there's no second BEM emulator to receive the real fan-out POSTs), but never restored it. Because every test file is `require()`d into the same process, the stub leaked process-wide: every later test whose route did `require('wonderful-fetch')` got `{ received: true }` in 0ms instead of a real HTTP round-trip (observed breaking consumer sponsorship + inbound-email route tests). The helper now saves the original cache entry and restores it in a suite-level `cleanup()`, confining the mock to its own file.
394
-
395
- # [5.3.0] - 2026-06-02
396
-
397
- ### Added
398
- - **`Manager.AI(assistant).image({ prompt, ... })`** — image generation via OpenAI's `gpt-image-2`. Separate method from `request()` (return type is bytes, not text; bypasses moderation/token-accounting/schema/prompt-normalization, none of which apply to image gen). Returns `{ buffer, b64, mime, revisedPrompt, model, size, quality, raw }` for a single image, or an array when `n > 1`. Options: `prompt` (required), `model` (default `gpt-image-2`), `size` (`1024x1024` default), `quality` (`medium` default), `background`, `n`, `timeout` (default 5min — image gen is slow). Only `openai` implements it. Lives on `OpenAI.prototype.image()` + dispatched via `AI.prototype.image()`. See [docs/ai-library.md](docs/ai-library.md).
399
- - **AI tools passthrough (nested) — `ai.request({ tools: { list, choice } })`.** `tools.list` is an array of tool definitions passed to the OpenAI Responses API verbatim — built-in hosted tools (e.g. `{ type: 'web_search' }`) OR custom function tools (`{ type: 'function', name, parameters }`); `tools.choice` *(optional)* maps to `tool_choice`. Opt-in — omitted/empty means no tools and identical behavior to a plain request. Primary use is OpenAI's built-in **web search** so the model finds and cites real, currently-live URLs instead of hallucinating them. When tools are active the response `output` may carry tool-call items (e.g. `web_search_call`) + `url_citation` annotations; the message-text extractor ignores non-message items so `r.content` is unaffected. See [docs/ai-library.md](docs/ai-library.md).
400
- - **`image-illustrator.js` — newsletter section illustrations now generated as flat-vector PNGs via `gpt-image-2` by default**, replacing the SVG-author-then-rasterize approach as the default. Clean flat 2D vector style (Stripe / Linear / undraw.co aesthetic) built from the brand palette (`content.theme.{primary,secondary,accent}Color`), white background, no text. The legacy `svg-illustrator.js` method is still available per-brand via `marketing.beehiiv.content.method.image = 'svg'`. Both return the same `{ png: Buffer, fallback, meta }` contract, so `image-host.js` / `uploadAssets` are unchanged. Validated end-to-end against live `gpt-image-2` with Somiibo's real config. See [docs/marketing-campaigns.md](docs/marketing-campaigns.md).
401
- - **Full emulator-Firestore flush before every test run.** `deleteTestUsers()` now calls `flushEmulatorFirestore()` — `listCollections()` + `recursiveDelete()` on the entire emulator DB — before recreating test accounts. The emulator DB is 100% test data, so a full flush is the simplest correct clean slate; there are no per-collection allowlists to maintain. Guarded to run ONLY when `FIRESTORE_EMULATOR_HOST` is set, so it can never touch a real project.
402
- - **`test/_init.js` pre-test lifecycle hook.** The test runner loads an optional `test/_init.js` from BOTH test roots (BEM core + consumer project) and runs it before any test (it is not run as a test itself). The module **must export a function** — `module.exports = (ctx) => ({ ... })` — called with `{ config, Manager }` and returning the hook object. It may declare `accounts` (array of extra test accounts `{ id, uid, email, properties }`, created/fetched/deleted on the same path as the built-ins so a project has a user per lifecycle) and `async setup({ admin, config, accounts, Manager, assistant })` (reseed fixtures into the freshly-flushed DB, after account creation). There is **no `cleanup` hook** — the whole DB is flushed each run and each test cleans up after itself. A default boilerplate `test/_init.js` now ships via `src/defaults/` (copied into consumers on first `npx mgr setup`, never overwriting an existing one). Mirrored across all four OMEGA frameworks. See `docs/test-framework.md`.
403
-
404
- ### Changed
405
- - **Environment detection consolidated onto the Manager as SSOT.** `getEnvironment()` returns exactly one of `development | testing | production` (mutually exclusive, testing wins), read **live** from `process.env` on every call (no caching). `assistant.isDevelopment/isProduction/isTesting/getEnvironment` forward to the Manager. Fixes a bug where a cached environment made `getApiUrl()` resolve to the production URL inside the test runner.
406
- - **Install-command parity.** `npx mgr install` accepts the unified alias set across all four frameworks — `dev|d|development|local|l` for the local source install and `live|prod|p|production` for the published version; docs advertise the canonical `dev` + `live`.
407
- - **`copyDefaults` no longer skips `_`-prefixed filenames.** The archive-skip rule now only applies to `_`-prefixed *directory* segments (e.g. `_legacy/`); a `_`-prefixed *filename* like `test/_init.js` ships verbatim. The `_.env` / `_.gitignore` dotfile rename is unchanged.
408
-
409
- ### Fixed
410
- - **Test-account creation race.** A late `auth:on-delete` could clobber the fresh `auth:on-create` doc, leaving accounts without `api.clientId`/`privateKey` and cascading into dozens of auth/payment test failures. `createAccount()` now verifies the API keys landed and repairs (recreate) if a stale delete clobbered the doc, making the suite deterministic.
411
- - **`meta/stats` seed ordering.** `ensureMetaStats()` ran before the emulator flush (so it was wiped) and skipped when the doc already existed (so the notification on-write trigger could leave it without a `users` field). It now merges the `users` baseline AFTER the flush, fixing flaky admin-stats tests.
412
- - **Flaky payment/dispute tests.** The trial journey now skips cleanly when no paid product has `trial.days > 0` (mirrors trial-cancel) instead of timing out; the dispute-alert dedup test seeds a deterministic in-flight doc instead of racing a live webhook to `failed`.
413
-
414
- # [5.2.19] - 2026-05-29
415
-
416
- ### Added
417
-
418
- - **Shared CLI styling module (`src/cli/utils/ui.js`)** — the SSOT for BEM console output, matching the OMEGA Manager look: a `🚀` banner, 70-char `━` dividers, indented tree output, dimmed `Label:` fields, timestamps, a consistent set of status symbols (`→ ✓ ✗ ⊘ ⚠ ✅ + ↻`), and a `Summary` block (green `✅` / yellow `⚠`). Exposed on every command as `this.ui` (wired in `base-command.js`) and adoptable incrementally by `serve`/`deploy`/`test`/`emulator`. See `docs/cli-output.md`.
419
- - **Regression test for the `.env`/`.gitignore` merge** (`test/helpers/merge-line-files.js`). Covers key-based alignment when key order drifts (the original scramble), Custom→Default promotion when the template adopts a key, Default→Custom migration of unknown keys, value preservation, quote normalization, idempotency, and that the setup-test helper is the same function as the canonical impl (SSOT guard).
420
-
421
- ### Changed
422
-
423
- - **`npx mgr setup` now renders in the OMEGA style.** The old `---- RUNNING SETUP ----` / `[1] name: passed` / bare missing-keys dump is replaced with a banner, a divider-wrapped project header (brand name + Firebase console URL + `Project`/`API` fields), and `[DEFAULTS]` / `[CHECKS]` / `[STATS]` sections. Each check prints `[N] ✓ name`, with `⚠ … — fixing…` → `✓ fixed` on auto-fix and `✗ Could not fix: …` on hard failure. The run ends with a `Summary` block (checks count, duration, passed/failed, and any failing checks with detail lines). The `bem-config` check lists the missing `backend-manager-config.json` keys as a bulleted list and surfaces a compact version in the summary.
424
-
425
- ### Fixed
426
-
427
- - **No more `UnhandledPromiseRejection` crash on setup failure.** Previously an unfixable check (e.g. missing config keys) rejected a promise with no reason, which bubbled out of the un-`catch`'d `bin/backend-manager` IIFE as Node's raw `UnhandledPromiseRejection: undefined` dump (exit non-zero, lost message). Setup now exits cleanly via `haltSetup()` → `process.exit(1)` after printing the styled summary, and `bin/backend-manager` wraps the run in a `try/catch` that prints a one-line `✗ <message>` backstop. The early-exit guards (missing `functions/package.json`, wrong directory) now print styled errors and exit `1` instead of `0`.
428
- - **`.env` merge no longer scrambles keys under the wrong headers.** The `has correct .env file` / `has correct .gitignore` setup checks (`env-file.js`, `gitignore.js`) imported a SECOND, **positional** merge implementation in `src/cli/commands/setup-tests/helpers/merge-line-files.js` that zipped comment lines and value lines by index — so any drift between the consumer's key order and the template shifted every value down a slot (the last key of each group landed under the next group's header), dropped newly-added template keys, and duplicated keys. That duplicate is **deleted**; the helper is now a thin SSOT shim re-exporting the canonical key-based merge in `src/utils/merge-line-files.js`. The canonical merge also now **promotes** a key from the user's Custom section up into Default (with its value) when the framework template adopts that key, instead of emitting an empty Default line and leaving the value stranded in Custom.
429
-
430
- # [5.2.17] - 2026-05-29
431
-
432
- ### Added
433
-
434
- - **Newsletter-driven blog articles (`marketing.beehiiv.content.article`).** When enabled, the newsletter generator expands its **lead section** (`structure.sections[0]`) into a full blog article via the Ghostii engine, publishes it to the website repo through the `admin/post` route, and injects a "Read the full article" CTA (`section.cta = { label, url }`) onto that section so the newsletter links to the long-form post. The article build runs **concurrently** with SVG image generation (both are slow AI calls) and is **failure-isolated** — if it throws, no CTA is injected and the newsletter ships normally. The published URL (`{brand.url}/blog/{slug}`) is surfaced on the generator return as `assets.articleUrl` and `meta.article`. New config block: `content.article = { enabled, author }` (`enabled` default `false`). See `docs/marketing-campaigns.md`.
435
- - **`section.cta` rendering.** Both the MJML template (`sectionCard`, already supported) and the markdown renderer (`lib/markdown-renderer.js`, new) now render `section.cta = { label, url }` when present. `getBodySections` passes `cta` through for both classic and field-report shapes. CTAs are still never authored by the AI — they're injected by code post-publish with a real, verified URL.
436
- - **Generate/publish split on the linked article (`opts.publishArticle`).** When `config.article.enabled` is on, the article is always **generated** (Ghostii writes it + the public URL is computed from the title slug + the CTA is injected), but it's only **committed** to the website repo via `admin/post` when `opts.publishArticle` is true. The production cron passes `publishArticle: true`. The newsletter iteration test (`test/marketing/newsletter-generate.js`) leaves it false by default — so a full `TEST_EXTENDED_MODE=1` run exercises the Ghostii write + CTA path without committing a real post — and opts in with `NEWSLETTER_CREATE_ARTICLE=1`. The CTA URL is valid either way (derived from the same slugify `admin/post` uses). `meta.article.published` records whether the post was actually committed.
437
-
438
- ### Changed
439
-
440
- - **Extracted the Ghostii article engine into `src/manager/libraries/content/ghostii.js`** (`writeArticle` + `publishArticle`) as the SSOT for the Ghostii API request shape and the `admin/post` publish payload. Both the standalone `ghostii-auto-publisher.js` daily cron and the new newsletter linked-article flow import it (DRY). No behavior change to the standalone cron.
441
- - **Standalone Ghostii is now disabled by default** (`ghostii[0].articles: 0` in `templates/backend-manager-config.json`). The daily `ghostii-auto-publisher.js` cron remains fully functional — set `articles >= 1` to opt back into independent article publishing. For newsletter-linked articles, use `content.article.enabled` instead.
442
-
443
- # [5.2.16] - 2026-05-28
444
-
445
- ### Removed
446
-
447
- - **Dropped the legacy top-level `affiliateCode` field from `/user/signup`.** UJM and all current consumers send the referral code as `attribution.affiliate.code`; the top-level `affiliateCode` (and the normalize-to-`attribution` shim + the `processAffiliate` fallback that read it) was a dead legacy path. Removed from `src/manager/schemas/user/signup/post.js`, the `buildUserRecord` normalize block, and the `processAffiliate` lookup in `src/manager/routes/user/signup/post.js`. The route now reads referral codes exclusively from `attribution.affiliate.code`. (The legacy `bm_api` sign-up action `functions/core/actions/api/user/sign-up.js` is unchanged.)
448
-
449
- ### Fixed
450
-
451
- - **Consent: never downgrade an existing granted consent on `/user/signup`** (`src/manager/routes/user/signup/post.js`). A legacy account — signed up before the `flags.signupProcessed` completion flow existed, so the flag was never set — re-fires `/user/signup` on every page load until the flag flips. Its consent payload arrives empty (the original is long gone from `localStorage`), which previously computed `'revoked'` and, on the `{ merge: true }` write, wiped the consent the user actually granted months ago. `buildConsentRecord` now reads the existing doc's consent and preserves any already-`granted` status when the incoming payload doesn't explicitly re-grant it. A genuine new grant still applies; an at-signup decline with no prior grant still records the decline. Added `consent-empty-payload-preserves-existing-grant` and `consent-explicit-decline-does-not-downgrade-existing-grant` tests (+ a dedicated `consent-preserve` test account). See `docs/consent.md`.
452
-
453
- ### Changed
454
-
455
- - **`user/signup` schema: shaped the `consent` field.** `src/manager/schemas/user/signup/post.js` now declares the nested `consent.{legal,marketing}.{granted,text}` shape instead of a bare passthrough object, documenting the input contract at the schema layer (the SSOT for request shape). Each sub-object is optional — omitting it leaves existing consent untouched (see the downgrade guard above).
456
-
457
- # [5.2.15] - 2026-05-28
458
-
459
- ### Changed
460
-
461
- - **Standardized the GitHub token env var `GITHUB_TOKEN` → `GH_TOKEN`** across the entire repo, to match the convention used in all other ITW repos. Updated every GitHub-backed route and action (`admin/post`, `content/post`, `admin/repo/content`, `general/fetch-post`, `admin/write-repo-content`, `admin/edit-post`, `admin/create-post`, legacy `create-post`), the email image-host library (`libraries/email/generators/lib/image-host.js`), the CLI deprecated-env notice, the `templates/_.env` scaffold, the `docs/marketing-campaigns.md` reference, and all related test files. This is a hard rename with no fallback — any environment (CI, prod, local `.env`) that still sets `GITHUB_TOKEN` must switch to `GH_TOKEN` for the GitHub-backed routes to work.
462
-
463
- # [5.2.14] - 2026-05-28
464
-
465
- ### Removed
466
-
467
- - **Dropped the `marketing-webhooks` Firestore idempotency ledger.** The marketing-webhook dispatcher (`src/manager/routes/marketing/webhook/post.js`) no longer reads/writes `marketing-webhooks/{eventId}` docs for dedup. Both handler side effects — writing `consent.marketing.status = 'revoked'` and the cross-provider `mailer.remove()` — are naturally idempotent, so a provider retry or duplicate parent fan-out re-runs to the same end state with no extra side effects. (This is the key difference from `payments-webhooks`, where dedup is load-bearing because payment side effects are NOT idempotent.) Events with no `eventId` are now processed instead of skipped, since dedup is no longer required. Removed `marketing-webhooks` from the test runner's pre-test cleanup list (`src/test/test-accounts.js`). Consumers with an existing `marketing-webhooks` collection can safely delete it — nothing reads or writes it anymore.
468
-
469
- ### Fixed
470
-
471
- - **`libraries/infer-contact.js`: guard the optional `assistant` arg.** All log/error calls now use `assistant?.` so `inferContact(email)` works without an assistant. Previously threw a `TypeError` when `BACKEND_MANAGER_OPENAI_API_KEY` was unset and no assistant was passed.
472
- - **`libraries/email/marketing/index.js`: gate `Marketing.remove()` behind test mode.** `remove()` now short-circuits with `if (assistant.isTesting() && !process.env.TEST_EXTENDED_MODE) return {}`, matching `add()` and `sync()`. Closes a gap where auth `onDelete` (fired ~44× at test startup during user cleanup) could hit the live SendGrid/Beehiiv remove APIs during a normal test run. The gate lives at the library SSOT so every caller (onDelete, webhook processors, contact-delete route) inherits it.
473
-
474
- ### Changed
475
-
476
- - **Signup timestamps stamped from Firebase Auth `creationTime`.** Both write paths — the `onCreate` auth event (`src/manager/events/auth/on-create.js`) and the `user/signup` fallback route (`src/manager/routes/user/signup/post.js`) — now set `metadata.created` and `consent.{legal,marketing}.grantedAt`/`revokedAt` from `user.metadata.creationTime` instead of "now"/request-time. The OMEGA user migration treats Auth's `creationTime` as the SSOT and reconciles every doc against it; the prior few-seconds drift meant every new signup got re-fixed on the next migration run. Stamping from `creationTime` makes new docs match the migration's expected value exactly, ending the recurring churn.
477
- - **`user/signup`: `flags.signupProcessed` is the sole idempotency gate.** Removed the 5-minute account-age reject (`MAX_ACCOUNT_AGE_MS`). A genuinely-unprocessed account can now complete signup whenever it retries — fixes the failure where a slow/missing `onCreate` plus a retry after 5 minutes caused a legitimate signup to be rejected. The frontend (UJM) gate is being moved to the same doc flag in a parallel change.
478
- - **CLI: port-conflict prompt auto-confirms after 5s.** The "Kill these processes to free the ports?" prompt in `src/cli/commands/base-command.js` now auto-confirms `Y` after 5 seconds of no input (via `AbortSignal.timeout` → `AbortPromptError` → default `true`), so unattended test/dev loops (`emulator`, `serve`, `test`) no longer hang waiting for input. Manual `y`/`n` and Ctrl+C still work.
479
- - **`AI` `claude-code` provider rewritten for serverside use** (`src/manager/libraries/ai/providers/claude-code.js`). Was a local-only wrapper around `@anthropic-ai/claude-agent-sdk` that spawned the `claude` binary (`forceLoginMethod: 'claudeai'`, keychain OAuth) — would not run in Cloud Functions. Now calls the Claude Messages API over plain HTTPS via `@anthropic-ai/sdk` using the OAuth token as `Authorization: Bearer` + `anthropic-beta: oauth-2025-04-20`, so it bills the Claude Pro/Max subscription (not API credits) and runs anywhere Node runs. Token resolution: `options.apiKey` → `config.claude_code.oauth_token` → `process.env.CLAUDE_CODE_OAUTH_TOKEN`. Renewal is a manual yearly `claude setup-token` (no auto-refresh). Verified live (text + JSON-schema paths). See `docs/ai-library.md`.
480
- - **Dependency bumps**: `@anthropic-ai/claude-agent-sdk` ^0.3.152 → ^0.3.153, `stripe` ^22.1.1 → ^22.2.0.
481
- - **`templates/_.env`**: consolidate OpenAI/Anthropic keys under an "AI" section and add `CLAUDE_CODE_OAUTH_TOKEN`.
482
- - **Marketing webhook tests** (`test/routes/marketing/webhook.js`): renamed `*-duplicate-event-skipped` → `*-reprocessed-idempotently` (assert re-delivery reprocesses and the user stays revoked); `sendgrid-event-without-eventId-*` now asserts the event is processed; beehiiv idempotency variant skips when no publication is configured. Updated `docs/consent.md` and `docs/test-framework.md` to describe the no-ledger design.
483
-
484
- # [5.2.12] - 2026-05-27
485
-
486
- ### Changed
487
-
488
- - **`before-signin`: move `activity.language` from `geolocation` to `client`.** In `src/manager/events/auth/before-signin.js`, the `language` field (sourced from `context.locale`) now lives under `activity.client` alongside `userAgent`, instead of under `activity.geolocation` alongside `ip`. Language is a client-side property (browser/locale) rather than an IP-derived geolocation property, so this aligns the before-signin write shape with the rest of the activity schema.
489
-
490
- # [5.2.11] - 2026-05-27
491
-
492
- ### Added
493
-
494
- - **CLI: bare `logs` is now an alias for `logs:read`.** `npx mgr logs [...flags]` now dispatches to the same handler as `npx mgr logs:read`, so callers (humans and AI assistants alike) no longer error out when they omit the `:read` suffix. Routing in `src/cli/index.js` accepts the bare `logs` option, and `src/cli/commands/logs.js` maps the bare subcommand to the `read` action. `logs:tail` and `logs:stream` are unchanged.
495
-
496
- # [5.2.10] - 2026-05-26
497
-
498
- ### Added
499
-
500
- - **`POST /admin/post`: resize images at ingest.** Downloaded post images are now checked against `IMAGE_MAX_DIMENSION` (4096px on the long edge) in `src/manager/routes/admin/post/post.js` and re-encoded as progressive JPEG at `IMAGE_JPEG_QUALITY` (80) when oversized. Prevents downstream Jekyll/imagemin pipelines from stalling on huge sources (a real 16384×10576 source decoded to ~520MB raw and silently broke 4 StudyMonkey posts on production). `resizeImage`, `IMAGE_MAX_DIMENSION`, and `IMAGE_JPEG_QUALITY` are exported for tests. Adds `sharp` as a dependency.
501
- - **`test/routes/admin/post-resize-image.js`** — 7 unit tests covering the resize contract (pass-through under the limit, boundary at exact limit, landscape/portrait/square scaling, the 16384×10576 case, on-disk overwrite). No network, no auth, no GitHub.
502
- - **`test/routes/admin/create-post.js`**: extended `create-post-rewrites-body-images` to submit a 5000×3000 header image, plus new `verify-oversized-header-image-was-resized` step that fetches the committed image back from GitHub and asserts long edge ≤ 4096px.
503
-
504
- ### Changed
505
-
506
- - **Dependency bumps**: `sharp` ^0.34.4 → ^0.34.5, `sanitize-html` ^2.17.3 → ^2.17.4 (auto-bumped at install).
507
-
508
- # [5.2.9] - 2026-05-25
509
-
510
- ### Added
511
-
512
- - **OpenAI provider: multi-role prompt array support.** `options.prompt` now accepts either the legacy object form (`{ path|content, settings }`, auto-wrapped as a single `system`-role segment per the OpenAI Model Spec) OR an array form (`[{ role, path|content, settings }, ...]`) where each segment becomes its own message with its declared role. Valid roles: `system`, `developer`, `user`, `assistant`; order is preserved; role defaults to `system` if omitted; invalid roles throw. New internal helpers `normalizePrompt()` + `VALID_PROMPT_ROLES` canonicalize input; the request pipeline threads `promptSegments` end-to-end (per-segment load, per-role logging, per-segment error surfacing, `formatHistory` unshifts segments in declared order). `module.exports._internals` exposes `normalizePrompt`, `formatHistory`, `VALID_PROMPT_ROLES` for unit tests — not part of the public API. Backwards compatible: existing callers passing `prompt: { content: '...' }` are auto-wrapped as a single `system` segment with no consumer changes.
513
- - **`test/helpers/ai-request-payload.js`** — 16 standalone tests covering the BEM → OpenAI payload transformation with no network and no assistant. Exercises `normalizePrompt` (undefined/null/empty handling, legacy object → single system segment, array-form role preservation, role-defaulting, invalid-role throw, full OpenAI Model Spec role coverage) and `formatHistory` (single-system emit, multi-segment order, empty-array → user-only, prompt+history+new-user interleaving, assistant `output_text` typing, history limit, `dedupeConsecutiveRoles` trailing-user drop, content trim/strip).
514
-
515
- ### Changed
516
-
517
- - **Default OpenAI model bumped to `gpt-5.4-mini`** (was `gpt-5-mini`). Updated in `src/manager/libraries/ai/providers/openai.js` (`DEFAULT_MODEL`), `src/manager/libraries/ai/index.js` (usage example in JSDoc), and `src/manager/libraries/infer-contact.js` (`inferContactWithAI`). The pricing table in the OpenAI provider already includes `gpt-5.4-mini`, so no further config changes are required.
518
- - **`inferContactWithAI` maxTokens doubled to 2048** (was 1024). Gives the model headroom for the richer multi-field contact response without truncation.
519
-
520
- # [5.2.8] - 2026-05-25
521
-
522
- ### Changed
523
-
524
- - **`/user/signup` precedence flip for `activity.client`.** `routes/user/signup/post.js` now spreads `assistant.request.client` FIRST and `settings.context.client` (the browser's `getContext()` payload) LAST, so the browser-supplied values win for the `client` block. `activity.client.language` is now `navigator.language` (e.g. `en-US`) instead of the raw `Accept-Language` header list (e.g. `en-US,en;q=0.9,fr;q=0.8`); falls back to the header when no browser context was sent (bots, non-browser clients). `activity.geolocation` precedence is unchanged — Cloudflare headers (`cf-ipcountry`, etc.) still win, since the browser doesn't know its own geo. Final shape mirrors `assistant.request`: geolocation is header-authoritative, client is browser-authoritative.
525
-
526
- # [5.2.7] - 2026-05-24
527
-
528
- ### Fixed
529
-
530
- - **`inferContact` silent-failure logging.** When the AI inference returned an empty result (either the AI call failed and returned null, or `gpt-5-mini` returned a parsed response with all-empty fields, or the response shape was missing `firstName`), the whole flow silently swallowed the failure and the signup's `user.personal.name` got written as `null`/`null`. Confirmed live on Somiibo: signed up `ian.wiedenman.business@gmail.com` twice on the same backend — first signup inferred nothing (empty name written), second signup correctly inferred "Ian Wiedenman". Same email, same code, transient AI hiccup, zero log trail. Added three unconditional diagnostic logs to `src/manager/libraries/infer-contact.js` (AI returned null, AI parsed response had all fields empty, AI response missing firstName) plus a log in `src/manager/routes/user/signup/post.js#inferUserContact` when the helper returns null. Next silent failure will at least leave a breadcrumb.
531
-
532
- # [5.2.6] - 2026-05-24
533
-
534
- ### Added
535
-
536
- - **`scripts/test-helper-providers.js`** — small CLI for live-test verification. Run from any consumer's `functions/` dir: `node <bem>/scripts/test-helper-providers.js find <email>` (check SendGrid + Beehiiv state without dashboards) or `purge <email>` (remove from both providers). Used during end-to-end consent-pipeline testing.
537
- - **Email-template namespacing.** `general/email` route now translates `:` in `settings.id` to a folder separator, so `general:download-app-link` resolves to `templates/general/download-app-link.js`. Existing `templates/download-app-link.js` moved into `templates/general/` to match.
538
-
539
- ### Changed
540
-
541
- - **Payment webhook + dispute-alert routes now accept either `BACKEND_MANAGER_WEBHOOK_KEY` (preferred) or `BACKEND_MANAGER_KEY` (legacy).** Phase 1 of the webhook-key migration plan in `TODO-WEBHOOK-KEY-UPGRADE.md` — non-breaking dual-acceptance so consumers can roll over at their own pace. `src/manager/routes/payments/webhook/post.js:28` + `src/manager/routes/payments/dispute-alert/post.js:20` validate against either env var. Test-processor self-fire in `src/manager/routes/payments/intent/processors/test.js:158` now uses `BACKEND_MANAGER_WEBHOOK_KEY` directly. All 13 payment test files (`test/routes/payments/*.js`, `test/events/payments/journey-*.js`) updated to use `BACKEND_MANAGER_WEBHOOK_KEY` for webhook URLs. `src/test/utils/http-client.js` + `src/test/runner.js` thread the new env var through the test context. `docs/stripe-webhook-forwarding.md` reflects the dual-key acceptance. Phase 2 (drop the legacy fallback) is tracked in `TODO-WEBHOOK-KEY-LEGACY-REMOVAL.md`.
542
- - **`npx mgr emulator` no longer wraps the emulator in a keep-alive subprocess.** Previously spawned a sleep-86400 wrapper via `runWithEmulator`; now spawns the emulator child directly and uses a clean SIGINT handler. Cleaner shutdown, no orphaned subprocesses, no `node-powertools` dep on the boot path. `npx mgr test`'s auto-start path uses the same helper.
543
-
544
- ### Fixed
545
-
546
- - **Finished the v5.2.3 SendGrid timeout bump that missed 5 sites.** v5.2.3's CHANGELOG claimed "All 9 fetch sites updated" but actually 5 sites in `sendgrid.js` (including `upsertContacts:118` — the hot path used by `Marketing.sync()`) were still using `timeout: 15000`. Confirmed live on Somiibo: a signup with marketing consent granted silently dropped both SendGrid and Beehiiv with `Request timed out` after ~18s. Now every API call in `sendgrid.js` goes through the `SENDGRID_TIMEOUT_MS` (60s) constant. The only remaining literal is the S3 CSV download in `getSegmentContacts:578` (30s — not a SendGrid API call).
547
- - **`beehiiv.js` timeouts unified under a new `BEEHIIV_TIMEOUT_MS` (60s) constant.** Two hot-path sites (`addSubscriber:71` and the unsub endpoint at `:455`) were still on `timeout: 15000`. Same silent-drop failure mode as SendGrid above. Every Beehiiv API call now uses the constant.
548
- - **Audited entire BEM codebase for short timeouts. Zero prod-path timeouts under 60s remain.**
549
- - **Bumped 6 sites from 10-15s → 60s:**
550
- - `src/manager/libraries/email/validation.js:136` — ZeroBounce mailbox check
551
- - `src/manager/libraries/email/generators/newsletter.js:520, 549` — parent-server `/newsletter-sources` GET + PUT
552
- - `src/manager/routes/payments/intent/processors/test.js:163` — test-processor self-fire webhook
553
- - `src/manager/routes/marketing/email-preferences/post.js:169, 179` — SendGrid ASM suppression POST + DELETE
554
- - **Bumped 9 sites from 30s → 60s:**
555
- - `src/manager/libraries/infer-contact.js:46` — PeopleDataLabs AI enrichment
556
- - `src/manager/libraries/email/providers/sendgrid.js:578` — S3 CSV download for segment exports
557
- - `src/manager/functions/core/actions/api/user/delete.js:34` + `src/manager/routes/user/delete.js:51` — internal sign-out fan-out
558
- - `src/manager/events/firestore/payments-webhooks/analytics.js:228, 301` — Facebook + Reddit Conversions API
559
- - `src/manager/routes/user/oauth2/providers/discord.js:25` + `…/google.js:30` — OAuth token-revoke
560
- - `src/manager/helpers/analytics.js:331` — itwcw-package-analytics event fire
561
- - **Bumped 2 sites from 30s → 120s:**
562
- - `src/manager/events/cron/daily/ghostii-auto-publisher.js:106` — gpt-image-1 hero image generation (regularly 30-60s)
563
- - `src/manager/events/cron/daily/ghostii-auto-publisher.js:177` — gpt-5+web-search blog post generation (regularly 60-90s)
564
- - **Audit rule going forward:** any external-API call in BEM prod paths uses **60s minimum** (120s+ for LLM/image generation). Short timeouts are silent-failure machines in serverless code where Cloud Function timeouts already give us a generous outer bound. `_legacy/` files, `src/test/` infra, and `src/cli/` commands keep their existing timeouts — they're not on the request hot path.
565
-
566
- # [5.2.5] - 2026-05-22
567
-
568
- ### Added
569
-
570
- - **`utilities.trim(input)`** in `src/manager/helpers/utilities.js`. Walks objects/arrays recursively and trims whitespace on every string. Does NOT strip HTML. Middleware uses it to clean up incoming request data without mangling URLs, Markdown, or any payload with `<`/`>`/`&`.
571
- - **`findContact(email)`** on the SendGrid + Beehiiv providers, and **`findSubscriber(email, publicationId)`** on Beehiiv. Both extracted from the existing `removeContact`/`removeSubscriber` search-by-email logic so tests (and other call sites) can verify provider state without forcing a delete.
572
- - **`test/marketing/consent-lifecycle.js`** — 5-phase live integration test (gated on `TEST_EXTENDED_MODE=true`) that walks two long-lived sentinel accounts through pre-check → sync → declined-stays-out → unsubscribe → validation-gate, asserting actual SendGrid + Beehiiv state at every step. Uses `pollProvider()` to handle SendGrid's async background jobs (upsert/delete can take 10-60s+ to surface).
573
-
574
- ### Changed
575
-
576
- - **Middleware no longer strips HTML by default.** The auto-`sanitize-html` pass on incoming request data was mangling legitimate input — URL query strings (`?a=1&b=2` → `?a=1&amp;b=2`), Markdown, and any payload containing `<`/`>`/`&`. The middleware now only trims whitespace by default. HTML sanitization is opt-in per route via `Manager.Middleware(req, res).run('route', { sanitize: true })`. Sanitize at the HTML-insertion site (`utilities.sanitize()` in your template/email render) rather than at the request boundary. The existing schema-level `sanitize: false` opt-out still works when route-level sanitize is on.
577
- - **Test sentinel accounts `consent-granted` and `consent-declined` now use the `_test.allow_*` prefix** (`_test.allow_consent-granted@...`, `_test.allow_consent-declined@...`). The `_test.*` validation block from v5.2.3 also blocks the previous `_test.consent-*` names from reaching providers — `_test.allow_*` is the carved-out exception specifically for live-provider integration tests.
578
- - **Beehiiv API timeouts bumped to 60s** on the two remaining stragglers (`findSubscriber`, `resolveSegmentIds`) to match the SendGrid+Beehiiv 60s convention from v5.2.3.
579
-
580
- ### Removed
581
-
582
- - **`cleanupMarketingProviders()` function and its pre-run/post-run hooks** in `src/test/test-accounts.js` + `src/test/runner.js`. No longer needed — the validation gate added in v5.2.3 blocks `_test.*` emails from reaching SendGrid + Beehiiv upstream, so there's nothing to clean up. The new `consent-lifecycle.js` test (which intentionally round-trips real contacts via `_test.allow_*`) manages its own pre-check force-clean inline.
583
-
584
- # [5.2.3] - 2026-05-22
585
-
586
- ### Added
587
-
588
- - **`marketing.sendgrid.listId` in `templates/backend-manager-config.json`.** Empty-string placeholder for OMEGA's `sendgrid/ensure/list.js` to populate at brand-onboarding time. Mirrors the existing `marketing.beehiiv.publicationId` convention.
589
- - **`_test.*` local-part block** in `src/manager/libraries/email/data/blocked-local-patterns.js`. Test-suite accounts (`_test.<scenario>@somiibo.com`) are now blocked from reaching SendGrid + Beehiiv. The carved-out exception is `_test.allow_*` — used for live-provider integration tests that intentionally need to round-trip a real contact.
590
-
591
- ### Changed
592
-
593
- - **`Marketing.add()` and `Marketing.sync()` now use the full `validate()` pipeline** instead of just `isCorporate()`. Single SSOT for "is this a valid marketing email" — runs format → disposable → corporate → localPart in one call. Stricter behavior: disposable-domain emails (mailinator etc.) and junk local-parts (`noreply`, `test*`, `_test.*`) are now blocked from marketing lists. They were previously waved through because the gate only checked corporate domains.
594
- - **SendGrid list-ID lookup is now config-only.** `src/manager/libraries/email/providers/sendgrid.js#getListId()` reads `Manager.config.marketing.sendgrid.listId` and returns null if missing — no more runtime API call, no more fuzzy-match-by-brand-name. Old fuzzy logic kept commented out as `getListIdByFuzzyMatch()` backstop. **Brands must run OMEGA's sendgrid service to populate `listId` before this version sees their list assignments work** (without it, contacts land in SendGrid's global "All Contacts" pool, not the brand list).
595
- - **Beehiiv publication-ID lookup is now config-only.** `src/manager/libraries/email/providers/beehiiv.js#getPublicationId()` reads `Manager.config.marketing.beehiiv.publicationId` and returns null if missing — same shape as SendGrid above. Old fuzzy logic kept commented out as backstop. Beehiiv side already preferred config when set, but now there's no API fallback.
596
- - **`marketing.beehiiv.publicationId` is now an always-present empty string in the config template** (was a commented-out hint). Matches the SendGrid `listId` shape and means `Manager.config.marketing.beehiiv.publicationId` always returns `""` (never `undefined`) for legacy brands.
597
- - **SendGrid API timeouts bumped from 10s → 60s** via a new top-level `SENDGRID_TIMEOUT_MS` constant in `sendgrid.js`. All 9 fetch sites updated. Catches the intermittent SendGrid backend hiccups that were dropping signups silently with "Request timed out".
598
-
599
- # [5.2.2] - 2026-05-21
600
-
601
- ### Added
602
-
603
- - **`consent` is now a protected user field.** `templates/firestore.rules` includes `consent` in `isWritingProtectedUserField()` so a logged-in user cannot retroactively forge their own consent record from the client — only the signup route + webhook processors can mutate it server-side. New rule test in `test/rules/user.js`.
604
- - **`BaseCommand.getLogsPath(name)` / `getTempPath(name)`** in `src/cli/commands/base-command.js`. Two explicit helpers so the folder convention is the SSOT and easy to change later. `getLogsPath()` writes human-readable logs (`serve.log`, `emulator.log`, `test.log`, `logs.log`) to `<projectDir>/functions/` alongside firebase-tools' own `*-debug.log` files. `getTempPath()` writes transient internal-only stuff (`*.log.reset` sentinels, `bem-reload-trigger.js`, `test-mode.json`) to `<projectDir>/.temp/`.
605
- - **`BaseCommand.sweepStaleLogs()`** wipes BEM's own `.log` files in `functions/` and `.reset` sentinels in `.temp/` on every emulator/serve boot and on `npx mgr setup`. Deliberately does NOT touch firebase-tools' debug logs (`firestore-debug.log`, `database-debug.log`, etc.) so users can grep them after a crash.
606
- - **`npx mgr setup` cleanup step.** `cleanupGeneratedArtifacts()` now removes the watch trigger file (existing behavior) plus calls `sweepStaleLogs()` to clean up old BEM-owned logs/sentinels from previous runs.
607
-
608
- # [5.2.1] - 2026-05-21
609
-
610
- ### Added
611
-
612
- - **`BACKEND_MANAGER_WEBHOOK_KEY` in `templates/_.env`.** The `.env` scaffold the setup CLI copies into consumer projects now declares the webhook key alongside `BACKEND_MANAGER_KEY`. Required for the new `/marketing/webhook` + `/marketing/webhook/forward` routes shipped in 5.2.0. Existing consumers should add it to their own `.env` manually.
613
-
614
- # [5.2.0] - 2026-05-21
615
-
616
- ### Added
617
-
618
- - **Marketing consent capture (Phase A-B).** Canonical `consent.{legal,marketing}` sub-tree on every user doc (`status` + `grantedAt` / `revokedAt` with timestamp/source/ip/text). Signup route (`src/manager/routes/user/signup/post.js`) builds the record from the client payload using server-side time + IP, defending against client-clock spoofing. `mailer.sync(uid)` is gated on `consent.marketing.status === 'granted'` — opted-out signups never enter SendGrid/Beehiiv marketing lists.
619
- - **Email-preferences route (Phase D).** `POST /marketing/email-preferences` now supports authenticated opt-in/opt-out (writes user doc + hits both providers via `email.sync()`/`email.remove()`) in addition to the existing HMAC anonymous unsub flow. Anonymous unsub also writes the consent revoke on the user doc with the right `source`.
620
- - **Cross-provider unsubscribe webhooks (Phase E).** New `POST /marketing/webhook?provider={sendgrid|beehiiv}&key=...` dispatcher with per-provider processor modules. SendGrid events (`unsubscribe`, `group_unsubscribe`, `spamreport`, `bounce`, `dropped`) and Beehiiv events (`subscription.unsubscribed`, `.deleted`, `.paused`) flip `consent.marketing.status` to `revoked`, attribute via `source`, and propagate to the OTHER provider. Idempotent via `marketing-webhooks/{eventId}` docs.
621
- - **Parent BEM forwarder.** `POST /marketing/webhook/forward` lets a parent BEM (one with `config.parent === 'self'`) fan webhook events out to sibling brands sharing a SendGrid account or Beehiiv publication. New `Manager.getParentUrl()`, `Manager.getParentApiUrl()`, `Manager.isParent()` helpers — children store the parent's `brand.url` with NO `api.` subdomain and the helper inserts `api.` at call time.
622
- - **Self-contained TEST_EXTENDED_MODE.** `src/test/runner.js` + `src/test/test-accounts.js` now do pre + post-run cleanup of SendGrid/Beehiiv contacts (the only third-party state we can't wipe at start). Pure Firestore/Auth state is still wiped only at start, per existing convention.
623
- - **New docs.** `docs/consent.md` (consent system + webhook flows + migration template). `docs/test-framework.md` updated with the post-run-cleanup exception.
624
-
625
- ### Changed
626
-
627
- - `src/test/runner.js`, `src/test/utils/http-client.js`, `src/cli/commands/test.js`, plus emulator/serve/watch CLIs: renamed `hostingUrl` → `apiUrl` to match the rest of the codebase. Touches most test files for the matching context-API rename.
628
- - `src/manager/libraries/email/generators/newsletter.js`: uses `Manager.getParentApiUrl()` instead of reading `Manager.config.parent` directly so the `'self'` sentinel and the missing `api.` subdomain are both handled in one place.
629
- - `src/manager/libraries/email/providers/beehiiv.js`: exposes `getPublicationId` so generators and tests can read it without reaching into the module.
630
- - Disposable-domain blacklist refreshed (8 new domains) via `prepare-package`'s pre-hook.
631
-
632
- ### Fixed
633
-
634
- - `mailer.sync()` and `mailer.add()` short-circuit when the target user's `consent.marketing.status === 'revoked'` so we never re-add an opted-out user.
635
- - Payment processor + cancel route touchups picked up by the runner-API rename pass — no behavior change, just keeping signatures aligned.
636
-
637
- # [5.1.4] - 2026-05-18
638
-
639
- ### Fixed
640
-
641
- - **`POST /admin/post` response `path`** now returns the full `.md` file path (e.g. `src/_posts/2026/guest/2026-05-14-my-post.md`) instead of the parent directory. Consumers (e.g. the sponsorship system) were treating it as a file path — which it was named to be — and downstream deletes failed with `"sha" wasn't supplied` because GitHub got a directory listing. The parent directory is now exposed separately as `directory` for consumers that still want it. Updated `test/routes/admin/create-post.js` accordingly.
642
-
643
- # [5.1.3] - 2026-05-18
644
-
645
- ### Added
646
-
647
- - **Live `TEST_EXTENDED_MODE` sync between `npx mgr test` and the running emulator.** Test command writes an allowlisted env subset to `<projectRoot>/.temp/test-mode.json` pre-flight; emulator's function workers watch the file via `fs.watch` and mutate their own `process.env` in place — flag flips take effect within ~50ms with no env coordination across terminals. No more "restart the emulator with `TEST_EXTENDED_MODE=true`" dance. Health endpoint re-reads the file as a freshness guard. New helper `src/test/utils/test-mode-file.js` is the SSOT for the file format and allowlist.
648
- - **Real BEM Manager in test contexts.** `run-tests.js` sets `BEM_TEST_RUNNER=1` before loading any BEM code; `Manager.init()` auto-detects this and skips Functions/server/Sentry wiring + `admin.initializeApp()` (which can't run outside a real Functions runtime). Result: tests receive `{ Manager, assistant }` in their context and can call `Manager.AI()`, `Manager.Email()`, `Manager.User()`, etc. exactly like production — no hand-rolled stubs.
649
- - **Newsletter markdown + summary outputs.** `lib/markdown-renderer.js` walks the same `structure` JSON the HTML is rendered from (no AI cost) and emits `newsletter.md` — each section/dispatch is a standalone `## heading` block ready to paste into Beehiiv's editor one block at a time with ad blocks inserted between dispatches. A separate `summary.md` (2-3 sentence editorial recap) is written alongside.
650
- - **`summary` and `tags` fields on the structure schema.** `summary` is ≤600 chars, used for the `summary.md` body and as a share snippet (distinct from preheader, which is an inbox hook). `tags` is 0-5 lowercase kebab-case topical tags, passed to Beehiiv's `content_tags` on draft creation.
651
- - **GitHub asset host writes MD + summary alongside HTML.** `lib/image-host.js` accepts `markdown` + `summary` parameters and uploads `newsletter.md` / `summary.md` to `{brandId}/{campaignId}/` in the same atomic two-commit upload as PNGs + HTML. URLs surface on `assets.markdownUrl` / `assets.summaryUrl`.
652
- - **Beehiiv fallback alert email.** When Beehiiv draft creation fails (e.g. free-plan `SEND_API_NOT_ENTERPRISE_PLAN`), the generator sends an internal alert via `sender: 'internal'` (alerts@{brandDomain}) to `brand.contact.email` containing the failure reason + subject/preheader/tags + direct links to HTML/MD/summary/folder. The newsletter is never stuck. Best-effort; failure to send is logged but never blocks the Firestore campaign-doc write.
653
- - **Universal AI system-prompt injections.** `Manager.AI()` `normalizeOptions()` now prepends two rules (em-dash ban, confidentiality) to every system prompt — every caller picks them up automatically.
654
- - **GPT-5 Codex family pricing** (`gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.1-codex-max`, `gpt-5.1-codex`, `gpt-5.1-codex-mini`, `gpt-5-codex`, `codex-mini-latest`) added to the OpenAI provider's MODEL_TABLE.
655
- - **`docs/common-mistakes.md`** and **`docs/key-files.md`** — extracted from CLAUDE.md to keep the architectural overview under 250 lines.
656
-
657
- ### Changed
658
-
659
- - **SVG illustrator default flipped to `gpt-5.3-codex`.** Codex is the markup/code-specialized GPT-5 variant; SVG is structured markup, so it's the right fit. Anthropic remains supported as a fallback provider.
660
- - **`structure` schema now requires `summary` and `tags`.** Existing generators pick these up automatically — the AI prompt was updated to instruct on both.
661
- - **CTAs removed from generated section bodies.** The AI cannot author URLs reliably (no browse access, no real source URLs), so any link it produced was invented. Newsletters are self-contained reads; outbound links come exclusively from the template shell's sponsorship blocks (`marketing.beehiiv.content.sponsorships[]`). Test fixtures updated.
662
- - **CLAUDE.md restructured** into the standard skeleton (Identity → Recommended skills → Quick Start → Architecture → CLI → File Conventions → Doc-update parity → Documentation index). Per-subsystem details extracted to `docs/`.
663
- - **Consumer default CLAUDE.md** updated for the new `logs:read` / `logs:tail` / `firestore:*` / `auth:*` CLI commands.
664
- - **Runner mode reporting.** The old `TEST_EXTENDED_MODE mismatch` warning is gone (made impossible by the live sync) — replaced with a "Mode: EXTENDED/normal" line sourced from the emulator's health-endpoint confirmation.
665
-
666
- # [5.1.2] - 2026-05-14
667
-
668
- ### Changed
669
-
670
- - **Broadened `test` / `example` local-part blocks** in `email/data/blocked-local-patterns.js`:
671
- - `/^test[._-]/` → `/^test/` — now catches `testuser`, `test123abc`, etc., not only `test.user` / `test_123` / `test-foo`.
672
- - Added `/^example/` — catches `example`, `exampleuser`, `example.user`, `examples`, etc.
673
- - Both patterns are anchored to the start of the local part, so legitimate addresses that contain (but don't start with) those substrings are still allowed: `rachel.tester`, `contestant`, `exam` all pass.
674
-
675
- # [5.1.1] - 2026-05-13
676
-
677
- ### Added
678
-
679
- - **Corporate / social-media domain blacklist** — new `corporate` check in `email/validation.js` blocks marketing-list adds from corporate social-media domains (meta.com, instagram.com, soundcloud.com, tiktok.com, x.com, reddit.com, linkedin.com, youtube.com, discord.com, telegram.org, signal.org, and more — 21 domains total). Added to `DEFAULT_CHECKS` so every existing caller of `validate()` picks it up automatically. New `isCorporate(emailOrDomain)` helper exported alongside `isDisposable()`.
680
- - **Defense-in-depth guards** in `Marketing.add()` and `Marketing.sync()` — even when validation is bypassed (e.g. testing mode), corporate domains are blocked before any Beehiiv or SendGrid call.
681
- - **13 new tests** in `test/helpers/email-validation.js` covering the `corporate` validate-check, the `isCorporate()` helper, case-insensitivity, edge cases, and check-ordering behavior. Suite: 44 pass, 0 fail, 2 skip (ZeroBounce-only).
682
-
683
- ### Changed
684
-
685
- - **Email data files reorganized** — all blacklists now live in `src/manager/libraries/email/data/` (one folder, next to their consumer), instead of being scattered one level up alongside unrelated libraries:
686
- - `disposable-domains.json`, `custom-disposable-domains.json`, `corporate-domains.json` — domain blocklists
687
- - `blocked-local-parts.json` — categorized local-part blocklist (`generic`, `system`, `junk`, `placeholder`), extracted from a hardcoded `Set` in `validation.js`
688
- - `blocked-local-patterns.js` — regex patterns, extracted from `validation.js` (kept as JS so RegExp literals stay native)
689
- - **`scripts/update-disposable-domains.js`** — prepare-step downloader now writes to `email/data/disposable-domains.json`.
690
-
691
- # [5.1.0] - 2026-05-13
692
-
693
- ### Added
694
-
695
- #### Newsletter generation system
696
- - **Three production-quality templates** with distinct aesthetics, each owning its own content schema and AI prompt — switching templates produces fundamentally different content, not a recolor:
697
- - `clean` — Stripe/Linear marketing aesthetic. Safe, conservative, works everywhere.
698
- - `editorial` — Magazine-style: masthead, drop-cap intro, numbered sections, pull-quotes, italic signoff.
699
- - `field-report` — Wire-service correspondent × Bloomberg terminal. Dispatch kickers, datelines, mono data callouts, end-of-dispatch terminators, correspondent signoff.
700
- - **Schema-per-template architecture** (`lib/structure.js` is now a generic dispatcher). `BASE_SCHEMA` declares universals (subject, preheader, signoff, citations); each template extends with its own fields via `schema.properties` + `schema.required`. `clean` + `editorial` share `CLASSIC_SCHEMA` (`{intro, sections: [{title, body, cta?, image_prompt}]}`); `field-report` declares `{tldr, dateline, dispatches: [{kicker, headline, byline, location, lede, dispatch, dataPoints, cta, image_prompt}]}`.
701
- - **Templates export `buildPrompt({brand, newsletterConfig, sources})`** — each template fully owns its AI brief. `clean` + `editorial` use the shared classic prompt; `field-report` has its own wire-service-correspondent voice.
702
- - **Graceful omission everywhere** — every template's `build()` handles missing optional fields (returns `''` for omitted blocks instead of throwing). Empty sections/dispatches drop entirely (no hollow stubs).
703
- - **Asset hosting pipeline (`lib/image-host.js`)** — `uploadAssets({ images, html, brandId, campaignId, subject })` uploads section PNGs + `newsletter.html` to `itw-creative-works/newsletter-assets/{brandId}/{campaignId}/` via Git Trees API. PNG magic-byte verification + strict path regex. Public-safety guarantees baked in.
704
-
705
- #### Daily cron pipeline (production-ready)
706
- - **`marketing-newsletter-generate.js` now runs the FULL pipeline end-to-end**: fetch sources → AI structure → AI SVG → upload PNGs to GitHub → render HTML with embedded CDN URLs → upload `newsletter.html` to the same folder → upload Beehiiv draft (fails gracefully on free plan with `SEND_API_NOT_ENTERPRISE_PLAN`) → write `marketing-campaigns/{newId}` Firestore doc with `assets: { folderUrl, htmlUrl, imageUrls, beehiivPostId, campaignId }`.
707
- - **Reserved Firestore doc ID is used as the GitHub folder name** so URLs and the campaign doc always match.
708
-
709
- #### Unified AI library
710
- - **`src/manager/libraries/ai/`** — `Manager.AI(assistant).request({ provider: 'openai' | 'anthropic' | 'claude-code', ... })` dispatches to either provider with a consistent options + return shape. OpenAI is the default (back-compat); Anthropic added for SVG generation; `claude-code` provider added via `@anthropic-ai/claude-agent-sdk` for credit-less subscription-backed generation.
711
- - New env var: `BACKEND_MANAGER_ANTHROPIC_API_KEY` / `ANTHROPIC_API_KEY`.
712
-
713
- #### Iteration test as mirror of production
714
- - **Fixture mode is the default** (`npx mgr test bem:marketing/newsletter-generate`) — loads `test/marketing/fixtures/<active-template>.json` and renders straight through MJML. ~35ms, $0, deterministic. Used in CI to catch layout regressions for free.
715
- - **EXTENDED mode** (`TEST_EXTENDED_MODE=1`) is now a TRUE mirror of the production cron: always uploads to GitHub, always tries Beehiiv draft, no per-side-effect opt-outs.
716
- - **Three fixture JSONs** shipped at `test/marketing/fixtures/{clean,editorial,field-report}.json` — one per template, each matching its template's content shape. Convention enforced: adding a new template REQUIRES a matching fixture (default test run fails with `fixture not found` otherwise).
717
- - **17-test fixture suite** (`test/marketing/newsletter-templates.js`) covers graceful omission, empty-section drop, CTA conditional rendering, citation rendering, sponsorship rendering, button padding regression guard, long subjects, minimum-viable structures, template metadata, schema export contract.
718
-
719
- #### Other additions
720
- - **`Manager.Utilities().slugify()`** — canonical URL slug builder. Strips non-alphanumeric chars, collapses hyphens, trims, lowercases. Single source of truth shared by admin/post + any consumer that needs to predict slugs (e.g. sponsorship platform).
721
- - **`routes/admin/post/deduplicate-image-alts.js`** — utility that suffixes alt-text when two images share alt-text but have different URLs (prevents filename collisions on upload). Wired into both `routes/admin/post/post.js` and `actions/api/admin/create-post.js`. Unit-tested.
722
- - New env vars in `templates/_.env`: `BACKEND_MANAGER_ANTHROPIC_API_KEY`, `ANTHROPIC_API_KEY`.
723
- - `templates/_.gitignore` now includes `.temp/` in the BEM defaults section.
724
-
725
- #### Documentation split
726
- - **CLAUDE.md reduced from ~1500 lines to a navigable table of contents.**
727
- - **21 new `docs/*.md` deep references**: architecture, directory-structure, code-patterns, file-naming, environment-detection, response-headers, routes, schemas, sanitization, auth-hooks, common-operations, admin-post-route, payment-system, marketing-campaigns, mcp, usage-rate-limiting, ai-library, marketing-fields, stripe-webhook-forwarding, testing, cli-firestore-auth, cli-logs.
728
-
729
- ### Changed
730
-
731
- - **Config restructuring**: `marketing.newsletter` → `marketing.beehiiv.content`. Nesting under the provider that publishes the result makes the coupling explicit and leaves room for future `marketing.sendgrid.content` for promo email blasts. `marketing.beehiiv.enabled` now gates the whole content pipeline (no separate `newsletter.enabled` flag — disabling beehiiv disables newsletter generation as a side effect, since there's nowhere for the generated content to land).
732
- - **Marketing library** (`libraries/email/marketing/index.js`) — now prefers `settings.contentHtml` over running the markdown pipeline. Lets generators produce pre-rendered HTML directly. UTM tagging still runs on the chosen HTML either way.
733
- - **`Manager.AI()`** now points to the unified `libraries/ai/index.js`. `libraries/openai.js` is a thin compatibility shim that re-exports `libraries/ai/providers/openai.js`.
734
- - **Footer no longer renders a hand-rolled `${brandUrl}/unsubscribe` link** — Beehiiv and SendGrid both auto-append CAN-SPAM-compliant unsubscribe links on sent emails. The hand-rolled one was a dead second link. Fixture suite now has a regression guard preventing accidental re-adds.
735
- - **Beehiiv provider** (`libraries/email/providers/beehiiv.js`) — `createPost()` accepts an explicit `publicationId` arg (bypasses singleton-Manager lookup); `getPublicationId()` null-guards an uninitialized Manager singleton (matters for test stubs).
736
- - **`admin/post.js` + `actions/api/admin/create-post.js`** — replaced inline slugify implementations with calls to the new `Manager.Utilities().slugify()`. Wired alt-text dedup helper into the post-creation flow.
737
- - **`.temp/` moved** from `functions/.temp/` to project root (matches UJM/BXM/electron-manager convention).
738
- - **`src/defaults/CLAUDE.md`** — strengthened framework-docs callout for consumer projects; clarified the Default/Custom marker boundary.
739
-
740
- ### Removed
741
-
742
- - **`marketing.newsletter.provider/sectionStyle`** — `provider` defaults now live in code (openai for structure, anthropic for SVG), overridable per-run via env vars. `sectionStyle` was a free-form hint string the AI ignored.
743
- - **`marketing.beehiiv.uploadAs`** — vestigial config from the abandoned Beehiiv-send-path detour.
744
- - **`NEWSLETTER_GITHUB_UPLOAD` env flag** — redundant. EXTENDED mode now always uploads (production-equivalent run).
745
- - **Footer's hand-rolled unsubscribe link** (see Changed).
746
-
747
- # [5.0.203] - 2026-05-13
748
- ### Fixed
749
- - `Settings.resolve()` now surfaces a clear `No schema for <METHOD> request: expected <schema>/<method>.js or <schema>/index.js` error (code 500) when both the method-specific schema (e.g. `delete.js`) and the `index.js` fallback are absent. Previously the raw Node `Cannot find module .../<schema>/index.js` error propagated to consumers, leaking the require stack and surfacing the internal `/workspace/...` deploy path.
750
- - Schema files that exist but throw (syntax error, runtime error) are now re-thrown directly instead of being silently masked by an unintended fallback to `index.js`. The real error surfaces, making bugs in schema files debuggable.
751
-
752
- # [5.0.202] - 2026-05-12
753
- ### Added
754
- - **`src/defaults/CLAUDE.md`** — new file shipped to consumer projects, marker-wrapped with `# ========== Default Values ==========` / `# ========== Custom Values ==========`. Framework section stays live-synced across `npx mgr setup` while the Custom section is preserved verbatim. Aligns BEM with EM/BXM/UJM, which already ship a consumer-facing default CLAUDE.md.
755
- - **`src/utils/merge-line-files.js`** — new file. Implements the line-based merge for the marker-wrapped Default/Custom sections (copied verbatim from `electron-manager`'s utility). Reusable across `.env`, `.gitignore`, `CLAUDE.md`, and any other line-based files BEM might ship in the future.
756
- - **`copyDefaults()` method** in `src/cli/commands/setup.js`. New defaults-shipping mechanism for BEM (which previously had no `src/defaults/` directory at all). Mirrors EM's `copyDefaults` pattern: iterates `src/defaults/**`, renames `_.foo` → `.foo`, routes files matching `MERGEABLE_BASENAMES` (`['.env', '.gitignore', 'CLAUDE.md']`) through `mergeLineBasedFiles`, copies non-mergeable files only if they don't already exist. Wired into `runSetup()` BEFORE `runTests()` so test inspections see the merged state.
757
-
758
- # [5.0.201] - 2026-05-08
759
- ### Changed
760
- - Account deletion confirmation email now includes a "Deletion details:" block with the user's account email, UID, and deletion timestamp (UTC) — matching the pattern used in the data-request download email.
761
-
762
- # [5.0.200] - 2026-05-05
763
- ### Changed
764
- - Bumped `uuid` from `^13.0.2` to `^14.0.0`. uuid v14 is ESM-only, but Node 22+'s native `require(esm)` support means existing CommonJS call sites (`require('uuid').v4`, `.v5`, etc.) work unchanged.
765
-
766
- # [5.0.199] - 2026-04-23
767
- ### Fixed
768
- - Storage helper (`Manager.storage().get()` / `.set()`) was referencing `_.get` and `_.set` without a lodash namespace import — lodash is destructured at the top of the file, so `_` was undefined and every call crashed. Destructured `get` and `set` (aliased as `_get` / `_set`) and updated both call sites.
769
-
770
- # [5.0.198] - 2026-04-10
771
- ### Security
772
- - Added Stripe idempotency keys on all Stripe write operations to prevent duplicate charges, refunds, customers, and coupons from webhook retries, concurrent requests, or user double-clicks. Keys are scoped to stable resource identifiers and Stripe caches responses for 24 hours.
773
- - `bem-dispute-refund-{chargeId}` on auto-refunds from dispute alert Firestore triggers
774
- - `bem-customer-create-{uid}` on Stripe customer creation
775
- - `bem-coupon-{couponId}` on coupon creation in the intent route
776
- - `bem-refund-{resourceId}` on manual subscription refunds
777
-
778
- # [5.0.197] - 2026-04-10
779
- ### Added
780
- - `--retry=N` flag on `npx mgr setup` — re-runs the full setup sequence up to N times, stopping early as soon as all checks pass. Useful for test cases that only succeed after a prior run creates fixtures or indexes propagate.
781
-
782
- # [5.0.196] - 2026-04-10
783
- ### Changed
784
- - Moved disposable domain fetch from `prepublishOnly` lifecycle hook to `prepare-package`'s new `hooks.before` config. The fetch now runs on every `npm run prepare` / `npm install` / `npm publish`, so fresh domains land in both the git working tree and the published tarball — no more drift between git and npm.
785
- - Bumped `prepare-package` devDep to ^2.1.0 (required for hooks support)
786
-
787
- # [5.0.195] - 2026-04-10
788
- ### Fixed
789
- - 24-hour cancellation guard in `payments/cancel` was comparing `Date.now()` (milliseconds) against `startDateUNIX` (seconds), producing an "age" of ~56 years for every subscription — guard never fired and users could cancel brand-new subscriptions. Now multiplies `startDateUNIX` by 1000 before subtraction.
790
- ### Changed
791
- - Standardized CLI examples in `CLAUDE.md` and `README.md` to use `npx mgr` instead of the deprecated `npx bm` alias
792
-
793
- # [5.0.194] - 2026-04-08
794
- ### Fixed
795
- - Fix email template data merge: caller's `settings.data` is now deep-merged at root of template data tree, removing the broken `data.` prefix indirection that caused empty order confirmation emails since 5.0.185
796
- ### Added
797
- - `preview` as a top-level setting on `email.send()` (alongside `subject`)
798
- - `logs:read` CLI: `--search`, `--order`, `--filter` flags and increased default limit to 300
799
- ### Changed
800
- - Email templates now access caller data at root (`{{order.id}}`, `{{body.message}}`) instead of under `data.*`
801
-
802
- # [5.0.192] - 2026-04-02
803
- ### Added
804
- - Setup test to create `hooks/auth/` and `hooks/cron/daily/` directories in consumer projects during `npx bm setup`
805
-
806
- # [5.0.186] - 2026-04-01
807
- ### Fixed
808
- - Move markdown rendering and UTM link tagging to run after `_.merge()` so caller overrides to `body.message` and `email.body` are properly processed
809
-
810
- # [5.0.185] - 2026-04-01
811
- ### Changed
812
- - Use `_.merge` for dynamic template data so callers can override any nested field (e.g. `email.preview`, `personalization.name`, `data.body.*`)
813
- - Set email schema `template` default to `'default'` instead of `undefined`
814
-
815
- # [5.0.184] - 2026-03-31
816
- ### Changed
817
- - Renamed email template shortcuts from `main/` to `core/` prefix across constants and all consumer files
818
- - Added new templates: `core/plain` and `core/marketing/promotional`
819
-
820
- # [5.0.177] - 2026-03-29
821
- ### Changed
822
- - `payment-recovered` transition now sends email to internal team only — customer no longer receives a "Payment received" notification
823
-
824
- # [5.0.176] - 2026-03-30
825
- ### Fixed
826
- - Chargeblast `alert.created` events use `alertId` instead of `id` — normalizer now accepts either field
827
- - Dispute charge matching now uses `charges.search()` instead of invoice search, fixing cases where Stripe invoices had `charge: null` even when paid (via balance/credit). Single reliable strategy: amount + ±2 day date window + card last4
828
- ### Changed
829
- - Dispute `on-write` trigger is now processor-agnostic — Stripe-specific match/refund logic extracted to `processors/stripe.js`, matching the pattern used by payments-webhooks
830
-
831
- # [5.0.174] - 2026-03-27
832
- ### Fixed
833
- - Payments-orders `metadata.created` timestamp no longer overwritten on subsequent webhook events (renewals, cancellations, payment failures)
834
-
835
- # [5.0.168] - 2026-03-21
836
- ### Fixed
837
- - Immediately suspend subscription on payment denial (PAYMENT.SALE.DENIED, invoice.payment_failed) instead of waiting for the processor to give up retrying — recovery via PAYMENT.SALE.COMPLETED restores active status
838
-
839
- # [5.0.167] - 2026-03-20
840
- ### Changed
841
- - Extracted `resolveTemperature()` helper for consistency with `resolveFormatting()` and `resolveReasoning()`
842
-
843
- # [5.0.166] - 2026-03-20
844
- ### Added
845
- - `reasoning: true` feature flag to GPT-5.x and o-series models in MODEL_TABLE
846
- - New GPT-5.4-mini and GPT-5.4-nano model entries with pricing
847
-
848
- ### Changed
849
- - Reasoning parameter is now conditionally included in API requests only when the model supports it
850
- - `resolveReasoning()` validates model support and warns when reasoning is requested for unsupported models
851
-
852
- # [5.0.165] - 2026-03-20
853
- ### Changed
854
- - Serve command now reads hosting port from `firebase.json` emulator config before falling back to default 5000
855
- - Notification test fixtures migrated from flat `createdAt`/`updatedAt` to nested `metadata.created`/`metadata.updated` objects matching standard BEM metadata format
856
-
857
- # [5.0.164] - 2026-03-18
858
- ### Added
859
- - Default field backfill in campaign seed setup — missing fields are restored from seed defaults without overwriting user edits
860
-
861
- # [5.0.163] - 2026-03-18
862
- ### Changed
863
- - Refactored campaign POST/PUT routes to generic field passthrough — schema-validated fields flow through automatically via shared `buildCampaignDoc()` utility, no manual field assignments needed
864
- - Extracted `normalizeSendAt()` and `DOC_LEVEL_FIELDS` into `routes/marketing/campaign/utils.js`
865
-
866
- # [5.0.161] - 2026-03-18
867
- ### Added
868
- - Port conflict detection in `serve` command — checks and kills blocking processes before starting Firebase server
869
-
870
- ### Changed
871
- - Unblocked common team/role email local parts (`user`, `email`, `mail`, `hello`, `info`, `admin`, `support`, `contact`) from validation blocklist, as these are legitimate addresses
872
-
873
- # [5.0.160] - 2026-03-18
874
- ### Added
875
- - Beehiiv `resolveSegmentIds()` — fetches segments from API, builds name→ID cache (same pattern as SendGrid)
876
- - Beehiiv segment resolution in `sendCampaign()` — SSOT keys auto-translate to Beehiiv segment IDs
877
-
878
- ### Changed
879
- - Beehiiv `createPost()` now receives resolved segment IDs instead of raw SSOT keys
880
-
881
- # [5.0.159] - 2026-03-18
882
- ### Added
883
- - Audience-specific email discount codes: `UPGRADE15`, `COMEBACK20`, `MISSYOU25`, `TRYAGAIN10` with eligibility validation per user
884
- - `{discount.code}` and `{discount.percent}` campaign template variables
885
- - `test: true` flag on campaign route — sends real Single Send to `test_admin` segment only
886
- - `test_admin` segment in SSOT (targets `hello@itwcreativeworks.com`)
887
- - `trial_claimed` custom field (`user_subscription_trial_claimed`) for marketing sync
888
- - `subscription_churned_paid` and `subscription_churned_trial` segments (replaces `subscription_churned`)
889
- - 4 audience-specific recurring sale seed campaigns with tailored messaging + discount codes
890
- - Full marketing campaign system documentation in CLAUDE.md, README.md, and BEM:patterns skill
891
-
892
- ### Changed
893
- - Template variable resolution now recursive — walks all string values in settings (future-proof)
894
- - UTM values resolved through template vars (`{holiday.name}_sale` → `black_friday_sale`)
895
- - UTM sanitizer strips apostrophes before underscore conversion
896
- - Payment intent + discount routes now pass user object for discount eligibility checking
897
- - Discount code `validate()` accepts optional user param for eligibility checks (backwards compatible)
898
-
899
- # [5.0.158] - 2026-03-17
900
- ### Added
901
- - Newsletter generator system (`libraries/email/generators/newsletter.js`) — fetches sources from parent server, AI assembles branded content with subject/preheader
902
- - Daily pre-generation cron (`cron/daily/marketing-newsletter-generate.js`) — generates newsletter content 24 hours before sendAt for calendar review
903
- - `marketing.newsletter.enabled` and `marketing.newsletter.categories` config options
904
- - `generator` field on campaign docs — tells cron to run content generation instead of sending directly
905
-
906
- ### Changed
907
- - Seed campaign IDs are now timing-agnostic: `_recurring-sale`, `_recurring-newsletter`
908
- - Recurrence timing removed from enforced fields — consuming projects can freely change schedule
909
- - Newsletter subject/preheader are now AI-generated (empty in seed template)
910
- - Frequent cron skips generator campaigns (handled by daily pre-generation cron)
911
- - Admin cron route now passes `libraries` to cron handlers
912
-
913
- # [5.0.157] - 2026-03-17
914
- ### Added
915
- - Campaign template variables via `powertools.template()` — `{brand.name}`, `{season.name}`, `{holiday.name}`, `{date.month}`, `{date.year}`, `{date.full}`
916
- - Separate SEASONS (Winter/Spring/Summer/Fall) and HOLIDAYS (New Year, Valentine's Day, Black Friday, Christmas, etc.) maps
917
- - Audit logging in `getSegmentContacts()` — logs export start, poll status, download count, timeout
918
-
919
- ### Changed
920
- - Seed sale campaign: quarterly → monthly on 15th, uses `{holiday.name}` template vars, targets free + cancelled + churned users, excludes paid
921
- - Prune cron calls segment export with 3-minute timeout for large segments
922
-
923
- ### Fixed
924
- - S3 presigned URL download broken by wonderful-fetch cache buster — set `cacheBreaker: false`
925
- - CSV header parsing: normalize to lowercase for case-insensitive column matching
926
-
927
- # [5.0.156] - 2026-03-17
928
- ### Added
929
- - Marketing campaign system with full CRUD routes (`POST/GET/PUT/DELETE /marketing/campaign`)
930
- - Calendar-backed scheduling: campaigns stored in `marketing-campaigns` Firestore collection, picked up by `bm_cronFrequent`
931
- - Multi-provider campaign dispatch: SendGrid (Single Send) + Beehiiv (Post) + Push (FCM)
932
- - Recurring campaigns with `recurrence` field — cron creates history docs and advances `sendAt`
933
- - Markdown → HTML conversion at send time for campaign content
934
- - UTM auto-tagging on brand domain links for both marketing and transactional emails (`libraries/email/utm.js`)
935
- - Shared notification library (`libraries/notification.js`) extracted from admin route
936
- - SEGMENTS SSOT dictionary in `constants.js` — 22 segments (subscription, lifecycle, engagement)
937
- - Runtime segment ID resolution: `resolveSegmentIds()` maps SSOT keys to SendGrid segment IDs
938
- - Contact pruning cron (`cron/daily/marketing-prune.js`) — monthly re-engagement + deletion of inactive contacts
939
- - SendGrid `getSegmentContacts()` and `bulkDeleteContacts()` for segment export + batch deletion
940
- - Seed campaigns via `npx bm setup`: `_recurring-quarterly-sale` (SendGrid) and `_recurring-weekly-newsletter` (Beehiiv) with enforced fields
941
- - `marketing.prune.enabled` config option (default: true)
942
- - Provider name extraction from OAuth on signup (Google, Facebook, etc.)
943
- - Personalized greetings in welcome, checkup, deletion, and data request emails
944
-
945
- ### Changed
946
- - `sendCampaign()` refactored for multi-provider dispatch with automatic SSOT segment key → provider ID translation
947
- - `POST /admin/notification` slimmed down to use shared notification library
948
- - Setup test data files (`required-indexes.js`, `seed-campaigns.js`) moved to `helpers/` directory
949
-
950
- # [5.0.155] - 2026-03-16
951
- ### Added
952
- - Setup test now ensures consuming project `functions/package.json` has `"private": true` to prevent accidental npm publish
953
-
954
- # [5.0.154] - 2026-03-16
955
- ### Changed
956
- - Add `display` property to all marketing FIELDS entries so display names are defined in the SSOT
957
- - Beehiiv provider now maps fields to display names instead of raw keys
958
- - Add `skip` flag for per-provider field creation control (e.g., SendGrid has first/last name built-in)
959
-
960
- ### Added
961
- - `user_personal_name_first` and `user_personal_name_last` fields to FIELDS dictionary (skipped for SendGrid which has them built-in)
962
-
963
- # [5.0.152] - 2026-03-16
964
- ### Fixed
965
- - Email queue documents all stored at `emails-queue/NaN` — `powertools.random()` doesn't support string generation, replaced with `pushid()`
966
-
967
- # [5.0.151] - 2026-03-16
968
- ### Fixed
969
- - AI contact inference was silently broken — `ai.request()` returns `{content, tokens, ...}` but code read `result.firstName` instead of `result.content.firstName`, so AI was never used
970
- - OpenAI API key not passed to AI library — now explicitly passes `BACKEND_MANAGER_OPENAI_API_KEY`
971
-
972
- ### Added
973
- - `POST /admin/infer-contact` route for testing/debugging contact inference (admin-only, supports batch)
974
- - `user_personal_company` custom field in FIELDS constant for marketing provider sync
975
- - Company passthrough in `Marketing.add()` → SendGrid and Beehiiv providers
976
- - Test suite for admin/infer-contact route
977
- - Standalone test script (`scripts/test-infer-contact.js`)
978
-
979
- ### Changed
980
- - Improved AI prompt: rejects placeholders/gibberish, always infers company from domain, preserves hyphenated name capitalization
981
- - Disabled regex fallback — returns empty when AI can't infer a real name
982
- - All 3 inferContact callsites (marketing/contact, user/signup, legacy add-marketing-contact) now extract and pass company
983
-
984
- # [5.0.150] - 2026-03-16
985
- ### Added
986
- - `marketing` config section in `backend-manager-config.json` — per-brand control over SendGrid and Beehiiv provider availability
987
- - Beehiiv provider reads `publicationId` from config (skips fuzzy-match API call) with in-memory cache
988
-
989
- ### Changed
990
- - Provider availability resolved once in Marketing constructor from `config.marketing` + env vars instead of per-request
991
- - Removed `providers` parameter from `add()`, `sync()`, `remove()` and all route/schema callers
992
-
993
- ### Removed
994
- - `DEFAULT_PROVIDERS` constant — no longer needed with config-driven provider resolution
995
- - Provider-selection tests — no longer applicable
996
-
997
- # [5.0.149] - 2026-03-14
998
- ### Added
999
- - Modular email library (`libraries/email/`) — replaces monolithic `libraries/email.js` with provider-based architecture
1000
- - Marketing contact providers: SendGrid (`providers/sendgrid.js`) and Beehiiv (`providers/beehiiv.js`) with add/remove/sync operations
1001
- - Email validation library (`libraries/email/validation.js`) — format, local-part, and disposable domain checks with configurable check selection
1002
- - Runtime SendGrid custom field ID resolution — fetches field definitions from API and caches name→ID mapping (no hardcoded IDs)
1003
- - 15 marketing custom fields synced to SendGrid/Beehiiv: brand, auth, subscription, payment, and attribution data
1004
- - `PUT /marketing/contact` admin route for triggering contact sync by UID
1005
- - `POST /marketing/contact` now syncs full custom field data on signup
1006
- - Marketing contact sync in payment webhook pipeline — subscription changes automatically update SendGrid/Beehiiv custom fields
1007
- - `mailer.sync(uid)` method for full contact re-sync from Firestore user doc
1008
- - `resolveFieldValues()` in `constants.js` — SSOT for building custom field payloads from user docs
1009
- - `User.resolveSubscription()` now includes `everPaid` field for marketing segmentation
1010
- - `TEST_EXTENDED_MODE` propagation from emulator to Firebase function workers
1011
- - `TEST_EXTENDED_MODE` mismatch detection between test runner and emulator via health check
1012
- - Email queue cron processor (`cron/frequent/email-queue.js`) — processes deferred emails every 10 minutes via the full `email.send()` pipeline
1013
- - Feedback route review URL builder with full site URLs
1014
- - 28 email validation unit tests, 7 marketing contact route tests, 5 marketing lifecycle integration tests
1015
-
1016
- ### Changed
1017
- - Refactored `libraries/email.js` into modular `libraries/email/` directory (index, constants, validation, providers)
1018
- - `POST /marketing/contact` validation now uses configurable check selection instead of boolean `skipValidation`
1019
- - `DELETE /marketing/contact` uses new provider-based removal
1020
- - Marketing contact schemas updated to match new validation options
1021
- - `on-delete` auth event now uses new email library for contact removal
1022
- - `saveToEmailQueue` now stores raw settings instead of pre-built SendGrid email, so queued emails re-enter the full build pipeline
1023
- - Renamed `email-queue` collection to `emails-queue`
1024
- - Feedback schema: renamed `like`/`dislike` fields to `positive`/`negative`
1025
- - Feedback review prompt logic now checks total positive feedback length (50+ chars)
1026
- - Renamed `GET /app` route to `GET /brand` (completes app→brand migration)
1027
-
1028
- ### Removed
1029
- - Monolithic `libraries/email.js` — replaced by modular `libraries/email/` directory
1030
-
1031
- # [5.0.148] - 2026-03-14
1032
- ### Added
1033
- - Semantic email sender system — pass `sender: 'orders'` to `Email.send()` to auto-resolve from address, display name, and SendGrid ASM group
1034
- - 7 sender categories: `orders`, `hello`, `account`, `marketing`, `security`, `newsletter`, `internal`
1035
- - 7 dedicated SendGrid ASM groups for granular unsubscribe control
1036
- - 4 new email tests for sender resolution, override precedence, and fallback behavior
1037
-
1038
- ### Changed
1039
- - Migrated all email call sites from `group:` to `sender:` parameter
1040
- - `sendOrderEmail()` now accepts optional `sender` parameter (defaults to `'orders'`)
1041
- - `replyTo` now defaults to the resolved from address instead of brand default
1042
-
1043
- # [5.0.147] - 2026-03-14
1044
- ### Added
1045
- - 24-hour cancellation guard on `POST /payments/cancel` — blocks cancellations for subscriptions younger than 24 hours
1046
-
1047
- # [5.0.146] - 2026-03-13
1048
- ### Added
1049
- - Promo discount support in payment analytics — `resolveActualValue()` computes effective price accounting for trials ($0) and percentage discounts
1050
- - Promo discount details (code, percent, savings, totalToday) in new-subscription order confirmation emails
1051
-
1052
- ### Changed
1053
- - `trackPayment()` and `resolvePaymentEvent()` now accept `order` parameter to access discount data
1054
-
1055
- # [5.0.144] - 2026-03-13
1056
- ### Added
1057
- - `User.resolveSubscription()` static method that derives calculated subscription fields (plan, active, trialing, cancelling) from raw user data
1058
-
1059
- # [5.0.143] - 2026-03-13
1060
- ### Changed
1061
- - `sendOrderEmail()` now accepts a `copy` parameter to control whether admin receives a copy (defaults to `true` for backward compat)
1062
- - Abandoned cart reminder emails no longer send admin copies (`copy: false`) to reduce inbox noise
1063
-
1064
- # [5.0.140] - 2026-03-12
1065
- ### Fixed
1066
- - Chargebee meta_data backfill not including `orderId`, causing `getOrderId()` to fail on future webhooks
1067
- - `orderId` resolution now falls back to `pass_thru_content` orderId when `getOrderId()` returns null
1068
-
1069
- ### Changed
1070
- - `setMetaData()` API simplified to accept `(resource, meta)` instead of individual params, writing the full meta object to both subscription and customer
1071
-
1072
- # [5.0.139] - 2026-03-12
1073
- ### Fixed
1074
- - Chargebee hosted page checkout failing to resolve UID from webhooks because `subscription[meta_data]` is not supported by Chargebee's hosted page API
1075
- - Webhook pipeline now falls back to resolving UID from hosted page `pass_thru_content` when meta_data is missing
1076
-
1077
- ### Added
1078
- - `resolveUidFromHostedPage()` in Chargebee library to search recent hosted pages by subscription ID and extract UID from `pass_thru_content`
1079
- - `setMetaData()` in Chargebee library to backfill meta_data on subscriptions and customers after first UID resolution
1080
- - Automatic meta_data backfill on subscription + customer after resolving UID from pass_thru_content, so future webhooks resolve directly
1081
-
1082
- ### Changed
1083
- - Chargebee intent now uses `pass_thru_content` instead of `subscription.meta_data` to carry UID/orderId through checkout
1084
-
1085
- # [5.0.132] - 2026-03-13
1086
- ### Fixed
1087
- - Abandoned cart cron crashing with `FAILED_PRECONDITION` due to missing `payments-carts` composite index (status + nextReminderAt)
1088
- - Abandoned cart email subject and template using raw `productId` instead of resolved `productName` and `brandName`
1089
-
1090
- ### Changed
1091
- - Index sync (`npx bm setup`) now auto-merges local and live indexes instead of prompting to choose one direction
1092
- - Added `payments-carts` composite index to `required-indexes.js`
1093
-
1094
- # [5.0.131] - 2026-03-11
1095
- ### Changed
1096
- - Analytics config restructured: consolidated `googleAnalytics`, `meta`, and `tiktok` under unified `analytics.providers` namespace with `google`, `meta`, and `tiktok` keys
1097
- - Google Analytics secret moved from config (`googleAnalytics.secret`) to env var (`GOOGLE_ANALYTICS_SECRET`)
1098
- - Meta pixel ID read from `analytics.providers.meta.id` instead of `meta.pixelId`
1099
- - TikTok pixel code read from `analytics.providers.tiktok.id` instead of `tiktok.pixelCode`
1100
- - Flattened `owner` field from `{ uid: string }` to plain UID string in feedback docs, notifications, and `getDocumentWithOwnerUser()` default path
1101
- - Moved `created` timestamp inside `metadata` object in feedback documents
1102
- - Added `GOOGLE_ANALYTICS_SECRET`, `META_ACCESS_TOKEN`, `TIKTOK_ACCESS_TOKEN` to `.env` template
1103
- - Bumped version to 5.0.131
1104
-
1105
- # [5.0.129] - 2026-03-11
1106
- ### Added
1107
- - Usage proxy system: `setUser()` to bill usage to a different user, `addMirror()`/`setMirrors()` to write usage to additional Firestore docs in parallel
1108
- - Admin post route image rewriting: `extractImages()` now returns a URL map and rewrites markdown body to use `@post/` prefix for uploaded images
1109
- - `metadata` object on user schema with `created` and `updated` timestamps
1110
- - Firestore security rules: added `metadata` to server-only write fields
1111
- - Test for admin post creation route
1112
- - CLAUDE.md and README.md documentation for usage proxy and admin post route
1113
-
1114
- ### Changed
1115
- - User schema: moved `activity.lastActivity` to `metadata.updated` and `activity.created` to `metadata.created`
1116
- - `before-signin` event handler writes to `metadata.updated` instead of `activity.lastActivity`
1117
- - Admin user sync route writes to `metadata.created`/`metadata.updated` instead of `activity.created`/`activity.lastActivity`
1118
- - `Usage.update()` refactored to execute primary + mirror writes in parallel via `Promise.all()`
1119
- - Bumped version to 5.0.129
1120
-
1121
- # [5.0.123] - 2026-03-10
1122
- ### Added
1123
- - Dispute alert system: `POST /payments/dispute-alert` endpoint with Chargeblast processor for ingesting payment dispute webhooks
1124
- - Firestore trigger (`payments-disputes/{alertId}`) that matches disputes to Stripe invoices by date/amount/card, auto-refunds, and cancels subscriptions
1125
- - Discount code system: `GET /payments/discount` validation endpoint and `discount-codes.js` library (FLASH20, SAVE10, WELCOME15)
1126
- - Discount code integration in payment intent flow — auto-creates/reuses Stripe and Chargebee coupons with deterministic IDs
1127
- - Meta Conversions API and TikTok Events API tracking alongside existing GA4 in payment analytics
1128
- - Subscription renewal tracking as payment events (fires on `invoice.payment_succeeded` / `PAYMENT.SALE.COMPLETED` even without a state transition)
1129
- - `attribution`, `discount`, and `supplemental` fields on payment intent schema for checkout context tracking
1130
- - Intent data (attribution, discount, supplemental) propagated to order objects during webhook on-write
1131
- - `meta.pixelId` and `tiktok.pixelCode` fields in config template
1132
- - Journey test accounts for discount and attribution flows
1133
- - Tests for discount validation and dispute alert endpoints
1134
-
1135
- ### Changed
1136
- - Renamed config key `google_analytics` → `googleAnalytics`
1137
- - Payment analytics rewritten with independent per-platform fire functions (`fireGA4`, `fireMeta`, `fireTikTok`)
1138
- - Test runner module resolution now tries normal resolution first before falling back to search paths
1139
- - reCAPTCHA marketing contact test skipped when `TEST_EXTENDED_MODE` is not set
1140
-
1141
- # [5.0.122] - 2026-03-09
1142
- ### Added
1143
- - Abandoned cart reminder system: sends escalating emails at 15min, 3h, 24h, 48h, 72h to users who visit checkout but don't complete payment
1144
- - `payments-carts/{uid}` Firestore collection with security rules (client-side write, server-side completion)
1145
- - `bm_cronFrequent` Cloud Function running every 10 minutes for sub-daily cron jobs
1146
- - Shared cron runner (`cron/runner.js`) consolidating daily and frequent cron orchestrators
1147
- - `main/order/refunded` and `main/order/abandoned-cart` email templates
1148
- - Firestore rules test for `payments-carts` (12 test cases)
1149
-
1150
- ### Changed
1151
- - Migrated v1 email templates to v2 SendGrid template IDs
1152
- - `cron/daily.js` and `cron/frequent.js` now delegate to shared `cron/runner.js`
1153
- - Payment analytics tracking now fires independently of transitions
1154
-
1155
- # [5.0.120] - 2026-03-09
1156
- ### Added
1157
- - reCAPTCHA verification on `POST /payments/intent` route (reads `verification.g-recaptcha-response` from request body)
1158
- - Shared `libraries/recaptcha.js` module for reCAPTCHA token verification (replaces duplicate helpers)
1159
- - `verification` field in `payments/intent` schema to accept the reCAPTCHA token object
1160
-
1161
- ### Security
1162
- - reCAPTCHA failure responses now return generic "Request could not be verified" (403) instead of revealing the verification mechanism
1163
- - reCAPTCHA verification runs in all environments except automated tests (`isTesting()`)
1164
-
1165
- ### Changed
1166
- - Marketing contact routes (`POST /marketing/contact`, `bm_api add-marketing-contact`) now use shared `recaptcha.verify()` instead of inline helpers
1167
- - Marketing reCAPTCHA checks skip during automated tests (consistent with payment intent)
1168
-
1169
- # [5.0.119] - 2026-03-07
1170
- ### Added
1171
- - `POST /marketing/email-preferences` route for unsubscribe/resubscribe via SendGrid ASM suppression groups
1172
- - HMAC signature verification (`UNSUBSCRIBE_HMAC_KEY`) on unsubscribe links to prevent forged requests
1173
- - HMAC signature generation in email library when building unsubscribe URLs
1174
- - `UNSUBSCRIBE_HMAC_KEY` environment variable in template `.env`
1175
- - Test suite for email-preferences endpoint (10 tests covering sig verification, validation, auth)
1176
-
1177
- ### Changed
1178
- - Unsubscribe URL in emails no longer includes `appName` and `appUrl` params (replaced by HMAC sig)
1179
-
1180
- # [5.0.118] - 2026-03-06
1181
- ### Added
1182
- - Chargebee payment processor with full pipeline support (intent, webhook, cancel, refund, portal).
1183
- - Chargebee shared library (`payment/processors/chargebee.js`) with raw HTTP API wrapper, unified subscription/one-time transformers, and both Items model (new) and Plans model (legacy) product resolution.
1184
- - Chargebee webhook processor supporting subscription lifecycle events (`subscription_created`, `subscription_cancelled`, `subscription_renewed`, `payment_failed`, `payment_refunded`, etc.) and one-time invoice events.
1185
- - Chargebee intent processor for hosted page checkout (subscriptions and one-time purchases) with deterministic item price IDs (`{itemId}-{frequency}`).
1186
- - Chargebee cancel processor with immediate cancellation during trials and end-of-term cancellation otherwise.
1187
- - Chargebee refund processor with 7-day full/prorated refund logic (matching Stripe/PayPal behavior).
1188
- - Chargebee portal processor for self-service subscription management via Chargebee Portal Sessions.
1189
- - Backwards compatibility for legacy Chargebee subscriptions: reads `cf_clientorderid`/`cf_uid` custom fields alongside new `meta_data` JSON format.
1190
- - Chargebee test suite: `to-unified-subscription`, `to-unified-one-time`, and `parse-webhook` group tests with fixtures covering all status mappings, product resolution (Items + legacy Plans), and edge cases.
1191
- - Chargebee customer name extraction from `shipping_address`/`billing_address` in webhook on-write pipeline.
1192
- - `chargebee` config keys in product templates (`itemId`, `legacyPlanIds`).
1193
-
1194
- ### Changed
1195
- - `CHARGEBEE_SITE` environment variable is now set from config in Manager init (matching PayPal pattern), so the Chargebee library doesn't need a Manager reference.
1196
-
1197
- # [5.0.111] - 2026-03-05
1198
- ### Changed
1199
- - PayPal client ID is now read from `backend-manager-config.json` (`payment.processors.paypal.clientId`) instead of requiring a `PAYPAL_CLIENT_ID` environment variable.
1200
- - PayPal auth now auto-detects sandbox vs live environment by trying both endpoints in parallel on first auth, with live taking priority.
1201
-
1202
- # [5.0.109] - 2026-03-04
1203
- ### Added
1204
- - Immediate trial cancellation: cancelling during a free trial now terminates the subscription instantly instead of scheduling cancel at period end, preventing free premium access for the remainder of the trial.
1205
- - Intent status tracking: `payments-intents/{orderId}` is now updated with `status: completed/failed` and completion timestamp after webhook processing.
1206
- - `journey-payments-trial-cancel` test suite covering the full trial → cancel → immediate cancellation flow.
1207
-
1208
- ### Changed
1209
- - Stripe and test cancel processors now detect trialing state and dispatch immediate cancel (`customer.subscription.deleted`) vs period-end cancel (`customer.subscription.updated`).
1210
-
1211
- # [5.0.106] - 2026-03-04
1212
- ### Added
1213
- - `GET /payments/trial-eligibility`: returns whether the authenticated user is eligible for a free trial (checks for any previous subscription orders in `payments-orders`).
1214
-
1215
- ### Fixed
1216
- - Payment frequency mapping now supports `daily` and `weekly` in addition to `monthly` and `annually` across Stripe (`resolvePriceId`), PayPal (`resolvePlanId`), and test processor (`createSubscriptionIntent`). Previously, these frequencies silently fell back to `monthly`.
1217
- - Updated docs (CLAUDE.md, README.md) to list all four supported frequency values.
1218
-
1219
- # [5.0.104] - 2026-03-02
1220
- ### Added
1221
- - `POST /payments/cancel`: cancels subscription at period end via processor abstraction (Stripe sets `cancel_at_period_end=true`; test processor writes webhook directly into the Firestore pipeline).
1222
- - `POST /payments/portal`: creates Stripe Billing Portal session with cancellation disabled (users must use the cancel endpoint).
1223
- - Payment transition pipeline: `transitions/index.js` detects all subscription state changes (new-subscription, payment-failed, payment-recovered, cancellation-requested, subscription-cancelled, plan-changed) and one-time transitions (purchase-completed, purchase-failed). Handlers fire-and-forget, send transactional emails.
1224
- - Payment analytics: `analytics.js` tracks GA4 payment events for all transitions (non-blocking, skipped in tests).
1225
- - Shared payment processor libraries: `payment/processors/stripe.js` (toUnifiedSubscription, toUnifiedOneTime, resolveCustomer, resolvePriceId, fetchResource), `payment/processors/paypal.js`, `payment/processors/test.js`, `payment/order-id.js`.
1226
- - `Email` library (`libraries/email.js`): shared transactional email via SendGrid, used by transition handlers and admin routes.
1227
- - `infer-contact.js` library: infers user name from payment processor data, auto-fills on first purchase.
1228
- - `routes/user/data-request/` (get/post/delete): GDPR data request endpoints.
1229
- - `cron/daily/data-requests.js`: daily cron to process pending GDPR data requests.
1230
- - CLI commands: `auth` (get/list/delete/set-claims), `firestore` (get/set/query/delete), `firebase-init`, `emulator` (renamed from `emulators`).
1231
- - `setup-tests/firestore-indexes-required.js`: validates required Firestore indexes exist before tests run.
1232
- - Comprehensive payment test suite: journey tests for one-time purchase, one-time failure, payment failure, plan change, cancel endpoint; route validation tests for cancel and portal; unit tests for `toUnifiedOneTime()`, `stripe-parse-webhook`, `infer-contact`, `email`; real Stripe CLI fixtures.
1233
- - Dedicated isolated test accounts for every mutating payment test (no shared state between tests).
1234
-
1235
- ### Changed
1236
- - `admin/email/post.js`, `general/email/post.js`: refactored to delegate to shared Email library (~400 lines removed from each).
1237
- - `marketing/contact/post.js`, `api/general/add-marketing-contact.js`: delegate to infer-contact + marketing library.
1238
- - `user/signup/post.js`: rewritten with new middleware pattern.
1239
- - `auth/on-create.js`: simplified, inline logic moved to middleware.
1240
- - `api/admin/send-email.js`: removed `ensureUnique` and SendGrid contact name lookup (handled by Email library).
1241
- - All admin routes: middleware pattern cleanup.
1242
- - `config.payment.products` now supports `type: 'one-time'` products with `prices.once` key.
1243
- - Test runner: improved discovery, filtering, and output formatting.
1244
-
1245
- ### Removed
1246
- - `src/manager/libraries/stripe.js`, `src/manager/libraries/test.js`: replaced by `payment/processors/` shared libs.
1247
- - `REFACTOR-BEM-API.md`, `REFACTOR-MIDDLEWARE.md`, `REFACTOR-PAYMENT.md`: work completed, files deleted.
1248
- - `bin/bem`: replaced by `bin/backend-manager`.
1249
-
1250
- # [5.0.84] - 2026-02-19
1251
- ### BREAKING
1252
- - Moved `config.products` to `config.payment.products`. All product lookups now use `config.payment.products`.
1253
- - Renamed `subscription.trial.activated` to `subscription.trial.claimed` across the entire subscription schema, API responses, analytics properties, and tests.
1254
- - Renamed analytics user property `plan_id` to `subscription_id` and `plan_trial_activated` to `subscription_trial_claimed`.
1255
- - Removed `Manager.getApp()` method (previously fetched from ITW Creative Works endpoint).
1256
- - Removed `Manager.SubscriptionResolver()` factory method.
1257
- - Removed deprecated `RUNTIME_CONFIG` .env loading from config merge.
1258
- - Test accounts now use `subscription.*` instead of `plan.*`.
1259
-
1260
- ### Added
1261
- - Stripe payment integration with shared library (`src/manager/libraries/stripe.js`) and `toUnified()` transformer that maps Stripe subscription states to the unified subscription schema.
1262
- - Test payment processor library that delegates to Stripe's transformer with `processor: 'test'`.
1263
- - Payment webhook route (`POST /payments/webhook`) with processor-specific handlers for Stripe (with signature verification) and test, including idempotent event storage in `payments-webhooks` Firestore collection.
1264
- - Payment intent route (`POST /payments/intent`) for creating checkout sessions with processor-specific handlers.
1265
- - Firestore trigger (`bm_paymentsWebhookOnWrite`) that processes stored webhook events and updates user subscription documents.
1266
- - Payment schemas for webhook and intent validation.
1267
- - `payment.processors` config section for Stripe, PayPal, Chargebee, and Coinbase configuration.
1268
- - `npx bm stripe` CLI command for standalone Stripe webhook forwarding.
1269
- - Auto-start Stripe CLI webhook forwarding with `npx bm emulator` (gracefully skips when prerequisites are missing).
1270
- - `Manager.version` property exposing the BEM package version.
1271
- - Journey test accounts for payment lifecycle testing (upgrade, cancel, suspend, trial).
1272
- - Stripe fixture data for subscription states (active, trialing, canceled).
1273
- - Tests for `stripe-to-unified` transformer, payment webhook route, and payment intent route.
1274
- - Test cleanup for payment-related Firestore collections (`payments-subscriptions`, `payments-webhooks`, `payments-intents`).
1275
-
1276
- ### Changed
1277
- - Cron schedule from `every 24 hours` to `0 0 * * *` (explicit midnight UTC).
1278
- - Test runner now passes full config object (with convenience aliases) for payment processor access.
1279
- - Unauthenticated usage tests now use relative assertions instead of absolute values.
1280
-
1281
- ### Removed
1282
- - Removed `PAYPAL_CLIENT_ID` and `CHARGEBEE_SITE` from `.env` template (now configured via `payment.processors` in config).
1283
-
1284
- # [5.0.39] - 2025-01-12
1285
- ### Added
1286
- - New test infrastructure with Firebase emulator support for reliable, isolated testing.
1287
- - Test runner with emulator auto-detection and startup.
1288
- - Test types: standalone, suite (sequential with shared state), group (independent).
1289
- - Built-in test accounts with SSOT configuration (basic, admin, premium-active, etc.).
1290
- - Firestore security rules testing support.
1291
- - HTTP client with auth helpers (`http.as('admin').command()`).
1292
- - Rich assertion library (`isSuccess`, `isError`, `hasProperty`, etc.).
1293
- - New `bm emulator` command for standalone emulator management.
1294
- - Enhanced `bm test` with path filtering and parallel test support.
1295
-
1296
- ### Changed
1297
- - Reorganized test files to `test/functions/` with `admin/`, `user/`, `general/` categories.
1298
- - Standardized auth test naming to `unauthenticated-rejected`.
1299
- - Auth rejection tests moved to end of test files (before cleanup).
1300
-
1301
- ### Fixed
1302
- - Changed unauthenticated API error from 500 to 401 with proper "Authentication required" message.
1303
-
1304
- ### Removed
1305
- - Removed legacy test files (moved to `test/_legacy/`).
1306
- - Removed deprecated CLI files and templates.
1307
- - Consolidated test account creation from API endpoint to test runner.
1308
-
1309
- # [5.0.31] - 2025-01-17
1310
- ### Changed
1311
- - Refactored CLI to modular command architecture with individual command classes and test files for better maintainability.
1312
- - Migrated from deprecated `.runtimeconfig.json` to `.env` file with `RUNTIME_CONFIG` environment variable.
1313
-
1314
- ### Removed
1315
- - Removed deprecated Firebase config commands (`config:get`, `config:set`, `config:unset`).
1316
-
1317
- ### Fixed
1318
- - Fixed `install:local` command to save to dependencies instead of devDependencies.
1319
- - Fixed reserved word conflicts with `package` parameter.
1320
- - Fixed template file path resolution in setup tests.
1321
-
1322
- # [5.0.0] - 2025-07-10
1323
- ### ⚠️ BREAKING
1324
- - Node.js version requirement is now `22`.
1325
- - `Manager.init()` no longer wraps the initializeApp() in `try/catch` block.
1326
- - `Settings()` API tries to look for a method-specific file first (e.g., `name/get.js`, `name/post.js`, etc.) before falling back to `name/index.js`. This allows for more modular and organized code structure. Also, `name.js` is no longer valid, we now look for `name/index.js` this is to make it consistent with the `Middleware()` API.
1327
- - `Middleware()` API now tries to load method-specific files (e.g., `name/get.js`, `name/post.js`, etc.) before falling back to `name/index.js`.
1328
- - `ai.request()` no longer accepts `options.message.images`. Use `options.message.attachments` instead.
1329
-
1330
- # [4.2.22] - 2024-12-19
1331
- ### Changed
1332
- - `Manager.install()` now automatically binds the fn with the proper `this` context (this may be breaking).
1333
-
1334
- # [4.1.0] - 2024-12-19
1335
- ### Changed
1336
- - Attach `schema` to `bm-properties` response header.
1337
- - `assistant.request.url` is now properly set for all environments (development, production, etc) and works whether called from custom domain or Firebase default function domain.
1338
-
1339
- ## [4.0.0] - 2024-05-08
1340
- ### ⚠️ BREAKING
1341
- - Require Node.js version `18` or higher.
1342
- - Updated `firebase-functions` to `6.0.1` (now need to require `firebase-functions/v1` to use v1 functions or `firebase-functions/v2` to use v2 functions).
1343
-
1344
- ## [3.2.109] - 2024-05-08
1345
- ### Changed
1346
- - Replaced all `methods` references with `routes`. This should be changed in your code as well.
1347
-
1348
- ## [3.2.32] - 2024-01-30
1349
- ### Changed
1350
- - Modified `.assistant().errorify()` to have defaults of `log`, `sentry`, and `send` to `false` if not specified to prevent accidental logging and premature sending of errors.
1351
-
1352
- ## [3.2.30] - 2024-01-30
1353
- ### Changed
1354
- - Modified `.assistant()` token/key check to use `options.apiKey || data.apiKey`
1355
-
1356
- ## [3.2.0] - 2024-01-19
1357
- ### Added
1358
- - Added `.settings()` API. Put your settings in `./schemas/*.js` and access them with `assistant.settings.*`.
1359
-
1360
- ## [3.1.0] - 2023-12-19
1361
- ### Added
1362
- - Added `.analytics()` API GA4 support.
1363
-
1364
- #### New Analytics Format
1365
- ```js
1366
- analytics.send({
1367
- name: 'tutorial_begin',
1368
- params: {
1369
- tutorial_id: 'tutorial_1',
1370
- tutorial_name: 'the_beginning',
1371
- tutorial_step: 1,
1372
- },
1373
- });
1374
- ```
1375
- - Added `.usage()` API to track user usage.
1376
- - Added `.middleware()` API to help setup http functions.
1377
- - Added `.respond()` function to `assistant.js` to help with http responses.
1378
-
1379
- ## [3.0.0] - 2023-09-05
1380
- ### ⚠️ BREAKING
1381
- - Updated `firebase-admin` from `9.12.0` --> `11.10.1`
1382
- - Updated `firebase-functions` from `3.24.1` --> `4.4.1`
1383
- - This project now requires `firebase-tools` from `10.9.2` --> `12.5.2`
1384
-
1385
- - Updated required Node.js version from `12` --> `16`
1386
-
1387
- - Updated `@google-cloud/storage` from `5.20.5` --> `7.0.1`
1388
- - Updated `fs-jetpack` from `4.3.1` --> `5.1.0`
1389
- - Updated `uuid` from `8.3.2` --> `9.0.0`
1390
-
1391
- - Removed `backend-assistant` dependency and moved to custom library within this module at `./src/manager/helpers/assistant.js`
1392
- - Replaced `require('firebase-functions/lib/logger/compat')` with the updated `require('firebase-functions/logger/compat')`
1393
- - Changed default for `options.setupFunctionsLegacy` from `true` --> `false`
1394
- - `.analytics()` is broken due to GA4 updates and should not be used until the next feature release
1395
- - Updated geolocation and client data retrieval to new format:
1396
- #### New Way
1397
- ```js
1398
- const assistant = new Assistant();
1399
-
1400
- // Get geolocation data
1401
- assistant.request.geolocation.ip;
1402
- assistant.request.geolocation.continent;
1403
- assistant.request.geolocation.country;
1404
- assistant.request.geolocation.region;
1405
- assistant.request.geolocation.city;
1406
- assistant.request.geolocation.latitude;
1407
- assistant.request.geolocation.longitude;
1408
-
1409
- // Get Client data
1410
- assistant.request.client.userAgent;
1411
- assistant.request.client.language;
1412
- assistant.request.client.platform;
1413
- assistant.request.client.mobile;
1414
- ```
1415
-
1416
- #### Old Way
1417
- ```js
1418
- const assistant = new Assistant();
1419
-
1420
- // Get geolocation data
1421
- assistant.request.ip;
1422
- assistant.request.continent;
1423
- assistant.request.country;
1424
- assistant.request.region;
1425
- assistant.request.city;
1426
- assistant.request.latitude;
1427
- assistant.request.longitude;
1428
-
1429
- // Get Client data
1430
- assistant.request.userAgent;
1431
- assistant.request.language;
1432
- assistant.request.platform;
1433
- assistant.request.mobile;
1434
- ```
1435
-
1436
- ## [2.6.0] - 2023-09-05
1437
- ### Added
1438
- - Identity Platform auth/before-create.js
1439
- - Identity Platform auth/before-signin.js
1440
- - Disable these by passing `options.setupFunctionsIdentity: false`