backend-manager 5.0.149 → 5.0.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -14,6 +14,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ # [5.0.150] - 2026-03-16
18
+ ### Added
19
+ - `marketing` config section in `backend-manager-config.json` — per-brand control over SendGrid and Beehiiv provider availability
20
+ - Beehiiv provider reads `publicationId` from config (skips fuzzy-match API call) with in-memory cache
21
+
22
+ ### Changed
23
+ - Provider availability resolved once in Marketing constructor from `config.marketing` + env vars instead of per-request
24
+ - Removed `providers` parameter from `add()`, `sync()`, `remove()` and all route/schema callers
25
+
26
+ ### Removed
27
+ - `DEFAULT_PROVIDERS` constant — no longer needed with config-driven provider resolution
28
+ - Provider-selection tests — no longer applicable
29
+
17
30
  # [5.0.149] - 2026-03-14
18
31
  ### Added
19
32
  - Modular email library (`libraries/email/`) — replaces monolithic `libraries/email.js` with provider-based architecture
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.149",
3
+ "version": "5.0.150",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -29,7 +29,6 @@ Module.prototype.main = function () {
29
29
 
30
30
  // Admin-only options
31
31
  const tags = isAdmin ? (requestPayload.tags || []) : [];
32
- const providers = isAdmin ? (requestPayload.providers || ['sendgrid', 'beehiiv']) : ['sendgrid', 'beehiiv'];
33
32
  const skipValidation = isAdmin ? (requestPayload.skipValidation || false) : false;
34
33
 
35
34
  // Validate email is provided
@@ -114,7 +113,6 @@ Module.prototype.main = function () {
114
113
  firstName,
115
114
  lastName,
116
115
  source,
117
- providers,
118
116
  });
119
117
  }
120
118
 
@@ -22,7 +22,6 @@ Module.prototype.main = function () {
22
22
 
23
23
  // Extract parameters
24
24
  const email = (requestPayload.email || '').trim().toLowerCase();
25
- const providers = requestPayload.providers || ['sendgrid', 'beehiiv'];
26
25
 
27
26
  // Validate email is provided
28
27
  if (!email) {
@@ -31,7 +30,7 @@ Module.prototype.main = function () {
31
30
 
32
31
  // Remove from providers
33
32
  const mailer = Manager.Email(assistant);
34
- const providerResults = await mailer.remove(email, { providers });
33
+ const providerResults = await mailer.remove(email);
35
34
 
36
35
  // Log result
37
36
  assistant.log('remove-marketing-contact result:', {
@@ -84,8 +84,6 @@ const SENDERS = {
84
84
  },
85
85
  };
86
86
 
87
- // Default marketing providers — SSOT for all provider loops
88
- const DEFAULT_PROVIDERS = ['sendgrid', 'beehiiv'];
89
87
 
90
88
  // SendGrid limit for scheduled emails (72 hours, but use 71 for buffer)
91
89
  const SEND_AT_LIMIT = 71;
@@ -234,7 +232,6 @@ module.exports = {
234
232
  GROUPS,
235
233
  SENDERS,
236
234
  FIELDS,
237
- DEFAULT_PROVIDERS,
238
235
  SEND_AT_LIMIT,
239
236
  sanitizeImagesForEmail,
240
237
  encode,
@@ -74,16 +74,9 @@ Email.prototype.build = function (settings) {
74
74
  };
75
75
 
76
76
  /**
77
- * Sync a user's data to marketing providers (SendGrid + Beehiiv).
77
+ * Add a new contact to enabled marketing providers (lightweight, no full user doc needed).
78
78
  *
79
- * @param {object} userDoc - Full user document from Firestore
80
- * @param {object} [options] - { providers: array of provider names (default: DEFAULT_PROVIDERS) }
81
- * @returns {{ sendgrid?: object, beehiiv?: object }}
82
- */
83
- /**
84
- * Add a new contact to marketing providers (lightweight, no full user doc needed).
85
- *
86
- * @param {object} options - { email, firstName, lastName, source, customFields, providers }
79
+ * @param {object} options - { email, firstName, lastName, source, customFields }
87
80
  * @returns {{ sendgrid?: object, beehiiv?: object }}
88
81
  */
89
82
  Email.prototype.add = function (options) {
@@ -91,25 +84,23 @@ Email.prototype.add = function (options) {
91
84
  };
92
85
 
93
86
  /**
94
- * Sync a user's full data to marketing providers (SendGrid + Beehiiv).
87
+ * Sync a user's full data to enabled marketing providers.
95
88
  *
96
- * @param {object} userDoc - Full user document from Firestore
97
- * @param {object} [options] - { providers: array of provider names (default: DEFAULT_PROVIDERS) }
89
+ * @param {string|object} userDocOrUid - UID string or full user document from Firestore
98
90
  * @returns {{ sendgrid?: object, beehiiv?: object }}
99
91
  */
100
- Email.prototype.sync = function (userDoc, options) {
101
- return this._marketing.sync(userDoc, options);
92
+ Email.prototype.sync = function (userDocOrUid) {
93
+ return this._marketing.sync(userDocOrUid);
102
94
  };
103
95
 
104
96
  /**
105
- * Remove a contact from all marketing providers.
97
+ * Remove a contact from all enabled marketing providers.
106
98
  *
107
99
  * @param {string} email - Email address to remove
108
- * @param {object} [options] - { providers: array of provider names (default: DEFAULT_PROVIDERS) }
109
100
  * @returns {{ sendgrid?: object, beehiiv?: object }}
110
101
  */
111
- Email.prototype.remove = function (email, options) {
112
- return this._marketing.remove(email, options);
102
+ Email.prototype.remove = function (email) {
103
+ return this._marketing.remove(email);
113
104
  };
114
105
 
115
106
  /**
@@ -25,7 +25,7 @@
25
25
  */
26
26
  const _ = require('lodash');
27
27
 
28
- const { TEMPLATES, GROUPS, SENDERS, DEFAULT_PROVIDERS } = require('../constants.js');
28
+ const { TEMPLATES, GROUPS, SENDERS } = require('../constants.js');
29
29
  const sendgridProvider = require('../providers/sendgrid.js');
30
30
  const beehiivProvider = require('../providers/beehiiv.js');
31
31
 
@@ -36,11 +36,19 @@ function Marketing(assistant) {
36
36
  self.Manager = assistant.Manager;
37
37
  self.admin = self.Manager.libraries.admin;
38
38
 
39
+ // Resolve provider availability from config + env
40
+ const marketing = self.Manager.config?.marketing || {};
41
+
42
+ self.providers = {
43
+ sendgrid: marketing.sendgrid?.enabled !== false && !!process.env.SENDGRID_API_KEY,
44
+ beehiiv: marketing.beehiiv?.enabled !== false && !!process.env.BEEHIIV_API_KEY,
45
+ };
46
+
39
47
  return self;
40
48
  }
41
49
 
42
50
  /**
43
- * Add a new contact to all providers (lightweight — no full user doc needed).
51
+ * Add a new contact to enabled providers (lightweight — no full user doc needed).
44
52
  * Used by newsletter subscribe and admin bulk import.
45
53
  *
46
54
  * @param {object} options
@@ -49,33 +57,29 @@ function Marketing(assistant) {
49
57
  * @param {string} [options.lastName]
50
58
  * @param {string} [options.source] - UTM source
51
59
  * @param {object} [options.customFields] - Extra SendGrid custom fields (keyed by field ID)
52
- * @param {Array<string>} [options.providers] - Which providers (default: all available)
53
60
  * @returns {{ sendgrid?: object, beehiiv?: object }}
54
61
  */
55
62
  Marketing.prototype.add = async function (options) {
56
63
  const self = this;
57
64
  const assistant = self.assistant;
58
- const { email, firstName, lastName, source, customFields, providers } = options;
65
+ const { email, firstName, lastName, source, customFields } = options;
59
66
 
60
67
  if (!email) {
61
68
  assistant.warn('Marketing.add(): No email provided, skipping');
62
69
  return {};
63
70
  }
64
71
 
65
- const shouldAdd = !assistant.isTesting() || process.env.TEST_EXTENDED_MODE;
66
- const addProviders = providers || DEFAULT_PROVIDERS;
67
- const results = {};
68
-
69
- if (!shouldAdd) {
72
+ if (assistant.isTesting() && !process.env.TEST_EXTENDED_MODE) {
70
73
  assistant.log('Marketing.add(): Skipping providers (testing mode)');
71
- return results;
74
+ return {};
72
75
  }
73
76
 
74
77
  assistant.log('Marketing.add():', { email });
75
78
 
79
+ const results = {};
76
80
  const promises = [];
77
81
 
78
- if (addProviders.includes('sendgrid') && process.env.SENDGRID_API_KEY) {
82
+ if (self.providers.sendgrid) {
79
83
  promises.push(
80
84
  sendgridProvider.addContact({
81
85
  email,
@@ -86,7 +90,7 @@ Marketing.prototype.add = async function (options) {
86
90
  );
87
91
  }
88
92
 
89
- if (addProviders.includes('beehiiv') && process.env.BEEHIIV_API_KEY) {
93
+ if (self.providers.beehiiv) {
90
94
  promises.push(
91
95
  beehiivProvider.addContact({
92
96
  email,
@@ -109,14 +113,11 @@ Marketing.prototype.add = async function (options) {
109
113
  * Upserts the contact with all custom fields derived from the user doc.
110
114
  *
111
115
  * @param {string|object} userDocOrUid - UID string (fetches from Firestore) or full user document object
112
- * @param {object} [options]
113
- * @param {Array<string>} [options.providers] - Which providers to sync to (default: all available)
114
116
  * @returns {{ sendgrid?: object, beehiiv?: object }}
115
117
  */
116
- Marketing.prototype.sync = async function (userDocOrUid, options) {
118
+ Marketing.prototype.sync = async function (userDocOrUid) {
117
119
  const self = this;
118
120
  const assistant = self.assistant;
119
- const { providers } = options || {};
120
121
 
121
122
  // Resolve UID to user doc if string
122
123
  let userDoc;
@@ -145,13 +146,9 @@ Marketing.prototype.sync = async function (userDocOrUid, options) {
145
146
  return {};
146
147
  }
147
148
 
148
- const shouldSync = !assistant.isTesting() || process.env.TEST_EXTENDED_MODE;
149
- const syncProviders = providers || DEFAULT_PROVIDERS;
150
- const results = {};
151
-
152
- if (!shouldSync) {
149
+ if (assistant.isTesting() && !process.env.TEST_EXTENDED_MODE) {
153
150
  assistant.log('Marketing.sync(): Skipping providers (testing mode)');
154
- return results;
151
+ return {};
155
152
  }
156
153
 
157
154
  assistant.log('Marketing.sync():', { email });
@@ -159,9 +156,10 @@ Marketing.prototype.sync = async function (userDocOrUid, options) {
159
156
  const firstName = _.get(userDoc, 'personal.name.first');
160
157
  const lastName = _.get(userDoc, 'personal.name.last');
161
158
  const source = _.get(userDoc, 'attribution.utm.tags.utm_source');
159
+ const results = {};
162
160
  const promises = [];
163
161
 
164
- if (syncProviders.includes('sendgrid') && process.env.SENDGRID_API_KEY) {
162
+ if (self.providers.sendgrid) {
165
163
  promises.push(
166
164
  sendgridProvider.buildFields(userDoc).then((customFields) =>
167
165
  sendgridProvider.addContact({
@@ -174,7 +172,7 @@ Marketing.prototype.sync = async function (userDocOrUid, options) {
174
172
  );
175
173
  }
176
174
 
177
- if (syncProviders.includes('beehiiv') && process.env.BEEHIIV_API_KEY) {
175
+ if (self.providers.beehiiv) {
178
176
  promises.push(
179
177
  beehiivProvider.addContact({
180
178
  email,
@@ -194,38 +192,33 @@ Marketing.prototype.sync = async function (userDocOrUid, options) {
194
192
  };
195
193
 
196
194
  /**
197
- * Remove a contact from all providers.
195
+ * Remove a contact from all enabled providers.
198
196
  *
199
197
  * @param {string} email - Email address to remove
200
- * @param {object} [options]
201
- * @param {Array<string>} [options.providers] - Which providers to remove from (default: all available)
202
198
  * @returns {{ sendgrid?: object, beehiiv?: object }}
203
199
  */
204
- Marketing.prototype.remove = async function (email, options) {
200
+ Marketing.prototype.remove = async function (email) {
205
201
  const self = this;
206
202
  const assistant = self.assistant;
207
- const { providers } = options || {};
208
203
 
209
204
  if (!email) {
210
205
  assistant.warn('Marketing.remove(): No email provided, skipping');
211
206
  return {};
212
207
  }
213
208
 
214
- const removeProviders = providers || DEFAULT_PROVIDERS;
215
- const results = {};
216
-
217
209
  assistant.log('Marketing.remove():', { email });
218
210
 
211
+ const results = {};
219
212
  const promises = [];
220
213
 
221
- if (removeProviders.includes('sendgrid') && process.env.SENDGRID_API_KEY) {
214
+ if (self.providers.sendgrid) {
222
215
  promises.push(
223
216
  sendgridProvider.removeContact(email)
224
217
  .then((r) => { results.sendgrid = r; })
225
218
  );
226
219
  }
227
220
 
228
- if (removeProviders.includes('beehiiv') && process.env.BEEHIIV_API_KEY) {
221
+ if (self.providers.beehiiv) {
229
222
  promises.push(
230
223
  beehiivProvider.removeContact(email)
231
224
  .then((r) => { results.beehiiv = r; })
@@ -259,8 +252,8 @@ Marketing.prototype.sendCampaign = async function (settings) {
259
252
  const Manager = self.Manager;
260
253
  const assistant = self.assistant;
261
254
 
262
- if (!process.env.SENDGRID_API_KEY) {
263
- return { success: false, error: 'SENDGRID_API_KEY not set' };
255
+ if (!self.providers.sendgrid) {
256
+ return { success: false, error: 'SendGrid not enabled' };
264
257
  }
265
258
 
266
259
  const templateId = TEMPLATES[settings.template] || settings.template || TEMPLATES['default'];
@@ -136,7 +136,22 @@ async function removeSubscriber(email, publicationId) {
136
136
  * @param {string} brandName
137
137
  * @returns {string|null} Publication ID or null
138
138
  */
139
+ let _publicationIdCache = null;
140
+
139
141
  async function getPublicationId() {
142
+ if (_publicationIdCache) {
143
+ return _publicationIdCache;
144
+ }
145
+
146
+ // Use publicationId from config if set (skips API call)
147
+ const configPubId = Manager.config?.marketing?.beehiiv?.publicationId;
148
+
149
+ if (configPubId) {
150
+ _publicationIdCache = configPubId;
151
+ return configPubId;
152
+ }
153
+
154
+ // Fuzzy-match by brand name
140
155
  const brandName = Manager.config.brand?.name;
141
156
 
142
157
  if (!brandName) {
@@ -168,6 +183,7 @@ async function getPublicationId() {
168
183
  );
169
184
 
170
185
  if (matchedPub) {
186
+ _publicationIdCache = matchedPub.id;
171
187
  return matchedPub.id;
172
188
  }
173
189
 
@@ -18,7 +18,6 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
18
18
 
19
19
  // Extract parameters
20
20
  const email = (settings.email || '').trim().toLowerCase();
21
- const providers = settings.providers;
22
21
 
23
22
  // Validate email is provided
24
23
  if (!email) {
@@ -27,7 +26,7 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
27
26
 
28
27
  // Remove from providers
29
28
  const mailer = Manager.Email(assistant);
30
- const providerResults = await mailer.remove(email, { providers });
29
+ const providerResults = await mailer.remove(email);
31
30
 
32
31
  // Log result
33
32
  assistant.log('marketing/contact delete result:', {
@@ -5,7 +5,6 @@
5
5
  const recaptcha = require('../../../libraries/recaptcha.js');
6
6
  const { validate: validateEmail, ALL_CHECKS } = require('../../../libraries/email/validation.js');
7
7
  const { inferContact } = require('../../../libraries/infer-contact.js');
8
- const { DEFAULT_PROVIDERS } = require('../../../libraries/email/constants.js');
9
8
 
10
9
  module.exports = async ({ assistant, Manager, settings, analytics }) => {
11
10
 
@@ -23,7 +22,6 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
23
22
 
24
23
  // Admin-only options
25
24
  const tags = isAdmin ? settings.tags : [];
26
- const providers = isAdmin ? settings.providers : DEFAULT_PROVIDERS;
27
25
  const skipValidation = isAdmin ? settings.skipValidation : false;
28
26
 
29
27
  // Email validation — run free checks before reCAPTCHA/rate limit
@@ -105,7 +103,6 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
105
103
  firstName,
106
104
  lastName,
107
105
  source,
108
- providers,
109
106
  });
110
107
  }
111
108
 
@@ -1,9 +1,6 @@
1
1
  /**
2
2
  * Schema for DELETE /marketing/contact
3
3
  */
4
- const { DEFAULT_PROVIDERS } = require('../../../libraries/email/constants.js');
5
-
6
4
  module.exports = () => ({
7
5
  email: { types: ['string'], default: undefined, required: true },
8
- providers: { types: ['array'], default: DEFAULT_PROVIDERS },
9
6
  });
@@ -1,15 +1,12 @@
1
1
  /**
2
2
  * Schema for POST /marketing/contact
3
3
  */
4
- const { DEFAULT_PROVIDERS } = require('../../../libraries/email/constants.js');
5
-
6
4
  module.exports = () => ({
7
5
  email: { types: ['string'], default: undefined, required: true },
8
6
  firstName: { types: ['string'], default: '' },
9
7
  lastName: { types: ['string'], default: '' },
10
8
  source: { types: ['string'], default: 'unknown' },
11
9
  tags: { types: ['array'], default: [] },
12
- providers: { types: ['array'], default: DEFAULT_PROVIDERS },
13
10
  skipValidation: { types: ['boolean'], default: false },
14
11
  'g-recaptcha-response': { types: ['string'], default: undefined },
15
12
  });
@@ -122,6 +122,15 @@
122
122
  // Add more products/tiers here
123
123
  ],
124
124
  },
125
+ marketing: {
126
+ sendgrid: {
127
+ enabled: true,
128
+ },
129
+ beehiiv: {
130
+ enabled: false,
131
+ // publicationId: 'pub_xxxxx', // Set to skip fuzzy-match API call
132
+ },
133
+ },
125
134
  firebaseConfig: {
126
135
  apiKey: '123-456',
127
136
  authDomain: 'PROJECT-ID.firebaseapp.com',
@@ -206,46 +206,7 @@ module.exports = {
206
206
  },
207
207
  },
208
208
 
209
- // Test 7: Admin can specify providers
210
- {
211
- name: 'admin-specify-providers',
212
- auth: 'admin',
213
- timeout: 30000,
214
-
215
- async run({ http, assert, config, state }) {
216
- const testEmail = TEST_EMAILS.valid(config.domain);
217
- state.testEmail = testEmail;
218
-
219
- const response = await http.command('general:add-marketing-contact', {
220
- email: testEmail,
221
- source: 'bem-test',
222
- providers: ['sendgrid'], // Only SendGrid, not Beehiiv
223
- // No firstName/lastName - should be inferred as "Rachel Greene"
224
- });
225
-
226
- assert.isSuccess(response, 'Add marketing contact with specific providers should succeed');
227
-
228
- // Only check providers if TEST_EXTENDED_MODE is set (external APIs are called)
229
- if (process.env.TEST_EXTENDED_MODE) {
230
- // Should only have sendgrid result
231
- if (response.data?.providers) {
232
- assert.hasProperty(response.data.providers, 'sendgrid', 'Should have SendGrid result');
233
- }
234
- state.sendgridAdded = response.data?.providers?.sendgrid?.success;
235
- // Beehiiv not called since we only specified sendgrid
236
- }
237
- },
238
-
239
- async cleanup({ state, http }) {
240
- if (!process.env.TEST_EXTENDED_MODE || !state.testEmail) {
241
- return;
242
- }
243
-
244
- await http.command('general:remove-marketing-contact', { email: state.testEmail });
245
- },
246
- },
247
-
248
- // Test 8: Mailbox verification (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
209
+ // Test 7: Mailbox verification (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
249
210
  {
250
211
  name: 'mailbox-validation',
251
212
  auth: 'admin',
@@ -208,44 +208,7 @@ module.exports = {
208
208
  },
209
209
  },
210
210
 
211
- // Test 7: Admin can specify providers
212
- {
213
- name: 'add-admin-specify-providers',
214
- auth: 'admin',
215
- timeout: 30000,
216
-
217
- async run({ http, assert, config, state }) {
218
- const testEmail = TEST_EMAILS.valid(config.domain);
219
- state.testEmail = testEmail;
220
-
221
- const response = await http.post('marketing/contact', {
222
- email: testEmail,
223
- source: 'bem-test',
224
- providers: ['sendgrid'], // Only SendGrid, not Beehiiv
225
- // No firstName/lastName - should be inferred as "Rachel Greene"
226
- });
227
-
228
- assert.isSuccess(response, 'Add marketing contact with specific providers should succeed');
229
-
230
- // Provider calls only happen in extended mode
231
- if (process.env.TEST_EXTENDED_MODE) {
232
- // Should only have sendgrid result (since we specified providers: ['sendgrid'])
233
- assert.hasProperty(response.data.providers, 'sendgrid', 'Should have SendGrid result');
234
- state.sendgridAdded = response.data?.providers?.sendgrid?.success;
235
- // Beehiiv not called since we only specified sendgrid
236
- }
237
- },
238
-
239
- async cleanup({ state, http }) {
240
- if (!process.env.TEST_EXTENDED_MODE || !state.testEmail) {
241
- return;
242
- }
243
-
244
- await http.delete('marketing/contact', { email: state.testEmail });
245
- },
246
- },
247
-
248
- // Test 8: Mailbox verification (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
211
+ // Test 7: Mailbox verification (only runs if TEST_EXTENDED_MODE and ZEROBOUNCE_API_KEY are set)
249
212
  {
250
213
  name: 'add-mailbox-validation',
251
214
  auth: 'admin',
File without changes