backend-manager 5.0.49 → 5.0.51

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 (24) hide show
  1. package/package.json +1 -1
  2. package/src/cli/commands/setup-tests/hosting-rewrites.js +5 -5
  3. package/src/manager/functions/core/actions/api/admin/backup.js +1 -1
  4. package/src/manager/functions/core/actions/api/admin/create-post.js +1 -1
  5. package/src/manager/functions/core/actions/api/admin/cron.js +1 -1
  6. package/src/manager/functions/core/actions/api/admin/database-read.js +4 -2
  7. package/src/manager/functions/core/actions/api/admin/database-write.js +4 -2
  8. package/src/manager/functions/core/actions/api/admin/edit-post.js +1 -1
  9. package/src/manager/functions/core/actions/api/admin/firestore-query.js +4 -2
  10. package/src/manager/functions/core/actions/api/admin/firestore-read.js +4 -2
  11. package/src/manager/functions/core/actions/api/admin/firestore-write.js +4 -2
  12. package/src/manager/functions/core/actions/api/admin/get-stats.js +4 -2
  13. package/src/manager/functions/core/actions/api/admin/payment-processor.js +4 -2
  14. package/src/manager/functions/core/actions/api/admin/run-hook.js +1 -1
  15. package/src/manager/functions/core/actions/api/admin/send-email.js +4 -2
  16. package/src/manager/functions/core/actions/api/admin/send-notification copy.js +1 -1
  17. package/src/manager/functions/core/actions/api/admin/send-notification.js +4 -2
  18. package/src/manager/functions/core/actions/api/admin/sync-users.js +1 -1
  19. package/src/manager/functions/core/actions/api/admin/write-repo-content.js +1 -1
  20. package/src/manager/functions/core/actions/api.js +1 -40
  21. package/src/manager/helpers/bem-router.js +51 -0
  22. package/src/manager/index.js +32 -2
  23. package/test/functions/admin/firestore-query.js +1 -1
  24. package/test/functions/general/add-marketing-contact.js +5 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.49",
3
+ "version": "5.0.51",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -14,10 +14,10 @@ class HostingRewritesTest extends BaseTest {
14
14
  // Check first rule is correct
15
15
  const firstIsCorrect = firstRewrite?.source === '{/backend-manager,/backend-manager/**}' && firstRewrite?.function === 'bm_api';
16
16
 
17
- // Check no duplicates exist (only one backend-manager rule allowed)
18
- const backendManagerCount = rewrites.filter(r => r.source?.startsWith('/backend-manager')).length;
17
+ // Check no duplicates exist (only one bm_api rule allowed)
18
+ const bmApiCount = rewrites.filter(r => r.function === 'bm_api').length;
19
19
 
20
- return firstIsCorrect && backendManagerCount === 1;
20
+ return firstIsCorrect && bmApiCount === 1;
21
21
  }
22
22
 
23
23
  async fix() {
@@ -26,8 +26,8 @@ class HostingRewritesTest extends BaseTest {
26
26
  // Set default
27
27
  hosting.rewrites = hosting.rewrites || [];
28
28
 
29
- // Remove any existing backend-manager rewrites (with or without wildcards)
30
- hosting.rewrites = hosting.rewrites.filter(rewrite => !rewrite.source?.startsWith('/backend-manager'));
29
+ // Remove any existing bm_api rewrites
30
+ hosting.rewrites = hosting.rewrites.filter(rewrite => rewrite.function !== 'bm_api');
31
31
 
32
32
  // Add to top
33
33
  hosting.rewrites.unshift({
@@ -19,7 +19,7 @@ Module.prototype.main = function () {
19
19
  payload.data.payload.deletionRegex = payload.data.payload.deletionRegex ? powertools.regexify(payload.data.payload.deletionRegex) : payload.data.payload.deletionRegex;
20
20
 
21
21
  if (!payload.user.roles.admin && assistant.isProduction()) {
22
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
22
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
23
23
  }
24
24
 
25
25
  // https://googleapis.dev/nodejs/firestore/latest/v1.FirestoreAdminClient.html#exportDocuments
@@ -27,7 +27,7 @@ Module.prototype.main = function () {
27
27
  try {
28
28
  // Perform checks
29
29
  if (!payload.user.roles.admin && !payload.user.roles.blogger) {
30
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
30
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
31
31
  }
32
32
 
33
33
  // Log payload
@@ -13,7 +13,7 @@ Module.prototype.main = function () {
13
13
 
14
14
  // Check if the user is an admin
15
15
  if (!payload.user.roles.admin && assistant.isProduction()) {
16
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
16
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
17
17
  }
18
18
 
19
19
  // Check if the ID is set
@@ -15,8 +15,10 @@ Module.prototype.main = function () {
15
15
  payload.data.payload.options = payload.data.payload.options || {};
16
16
 
17
17
  // Perform checks
18
- if (!payload.user.roles.admin) {
19
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
18
+ if (!payload.user.authenticated) {
19
+ return reject(assistant.errorify(`Authentication required.`, {code: 401}));
20
+ } else if (!payload.user.roles.admin) {
21
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
20
22
  } else if (!payload.data.payload.path) {
21
23
  return reject(assistant.errorify(`<path> parameter required`, {code: 400}));
22
24
  }
@@ -16,8 +16,10 @@ Module.prototype.main = function () {
16
16
  payload.data.payload.options = payload.data.payload.options || {};
17
17
 
18
18
  // Perform checks
19
- if (!payload.user.roles.admin) {
20
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
19
+ if (!payload.user.authenticated) {
20
+ return reject(assistant.errorify(`Authentication required.`, {code: 401}));
21
+ } else if (!payload.user.roles.admin) {
22
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
21
23
  } else if (!payload.data.payload.path) {
22
24
  return reject(assistant.errorify(`<path> parameter required`, {code: 400}));
23
25
  }
@@ -21,7 +21,7 @@ Module.prototype.main = function () {
21
21
  try {
22
22
  // Perform checks
23
23
  if (!payload.user.roles.admin && !payload.user.roles.blogger) {
24
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
24
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
25
25
  }
26
26
 
27
27
  // Check for GitHub configuration
@@ -14,8 +14,10 @@ Module.prototype.main = function () {
14
14
 
15
15
  return new Promise(async function(resolve, reject) {
16
16
  // Perform checks
17
- if (!payload.user.roles.admin) {
18
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
17
+ if (!payload.user.authenticated) {
18
+ return reject(assistant.errorify(`Authentication required.`, {code: 401}));
19
+ } else if (!payload.user.roles.admin) {
20
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
19
21
  }
20
22
 
21
23
  // Run queries
@@ -15,8 +15,10 @@ Module.prototype.main = function () {
15
15
  payload.data.payload.options = payload.data.payload.options || {};
16
16
 
17
17
  // Perform checks
18
- if (!payload.user.roles.admin) {
19
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
18
+ if (!payload.user.authenticated) {
19
+ return reject(assistant.errorify(`Authentication required.`, {code: 401}));
20
+ } else if (!payload.user.roles.admin) {
21
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
20
22
  } else if (!payload.data.payload.path) {
21
23
  return reject(assistant.errorify(`<path> parameter required`, {code: 400}));
22
24
  }
@@ -18,8 +18,10 @@ Module.prototype.main = function () {
18
18
  payload.data.payload.options.metadataTag = typeof payload.data.payload.options.metadataTag === 'undefined' ? 'admin:firestore-write' : payload.data.payload.options.metadataTag;
19
19
 
20
20
  // Perform checks
21
- if (!payload.user.roles.admin) {
22
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
21
+ if (!payload.user.authenticated) {
22
+ return reject(assistant.errorify(`Authentication required.`, {code: 401}));
23
+ } else if (!payload.user.roles.admin) {
24
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
23
25
  } else if (!payload.data.payload.path) {
24
26
  return reject(assistant.errorify(`Path parameter required.`, {code: 400}));
25
27
  }
@@ -19,8 +19,10 @@ Module.prototype.main = function () {
19
19
  payload.data.payload.update = payload.data.payload.update || false;
20
20
 
21
21
  // Perform checks
22
- if (!payload.user.roles.admin) {
23
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
22
+ if (!payload.user.authenticated) {
23
+ return reject(assistant.errorify(`Authentication required.`, {code: 401}));
24
+ } else if (!payload.user.roles.admin) {
25
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
24
26
  }
25
27
 
26
28
  // Get stats ref
@@ -16,8 +16,10 @@ Module.prototype.main = function () {
16
16
 
17
17
  return new Promise(async function(resolve, reject) {
18
18
  // Check for admin
19
- if (!payload.user.roles.admin) {
20
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
19
+ if (!payload.user.authenticated) {
20
+ return reject(assistant.errorify(`Authentication required.`, {code: 401}));
21
+ } else if (!payload.user.roles.admin) {
22
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
21
23
  }
22
24
 
23
25
  const productId = payload?.data?.payload?.payload?.details?.productIdGlobal;
@@ -17,7 +17,7 @@ Module.prototype.main = function () {
17
17
  return new Promise(async function(resolve, reject) {
18
18
  // Perform checks
19
19
  if (!payload.user.roles.admin && assistant.isProduction()) {
20
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
20
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
21
21
  }
22
22
 
23
23
  // Check for required options
@@ -25,8 +25,10 @@ Module.prototype.main = function () {
25
25
  self.sendgrid = sendgrid;
26
26
 
27
27
  // Check if user is admin
28
- if (!payload.user.roles.admin) {
29
- return reject(assistant.errorify(`Admin required.`, { code: 401 }));
28
+ if (!payload.user.authenticated) {
29
+ return reject(assistant.errorify(`Authentication required.`, {code: 401}));
30
+ } else if (!payload.user.roles.admin) {
31
+ return reject(assistant.errorify(`Admin required.`, { code: 403 }));
30
32
  }
31
33
 
32
34
  // Check for SendGrid key
@@ -55,7 +55,7 @@ Module.prototype.main = function () {
55
55
 
56
56
  // Check if user is admin
57
57
  if (!payload.user.roles.admin) {
58
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
58
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
59
59
  }
60
60
 
61
61
  // Check if title and body are set
@@ -71,8 +71,10 @@ Module.prototype.main = function () {
71
71
  assistant.log('Resolved notification payload', notification)
72
72
 
73
73
  // Check if user is admin
74
- if (!payload.user.roles.admin) {
75
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
74
+ if (!payload.user.authenticated) {
75
+ return reject(assistant.errorify(`Authentication required.`, {code: 401}));
76
+ } else if (!payload.user.roles.admin) {
77
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
76
78
  }
77
79
 
78
80
  // Check if title and body are set
@@ -15,7 +15,7 @@ Module.prototype.main = function () {
15
15
 
16
16
  // If the user is not an admin, reject
17
17
  if (!payload.user.roles.admin && assistant.isProduction()) {
18
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
18
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
19
19
  }
20
20
 
21
21
  // Get lastPageToken from meta/stats
@@ -21,7 +21,7 @@ Module.prototype.main = function () {
21
21
  try {
22
22
  // Perform checks
23
23
  if (!payload.user.roles.admin && !payload.user.roles.blogger) {
24
- return reject(assistant.errorify(`Admin required.`, {code: 401}));
24
+ return reject(assistant.errorify(`Admin required.`, {code: 403}));
25
25
  }
26
26
 
27
27
  // Check for GitHub configuration
@@ -48,28 +48,7 @@ Module.prototype.main = function() {
48
48
 
49
49
  return new Promise(async function(resolve, reject) {
50
50
  return libraries.cors(req, res, async () => {
51
- // Detect new-style RESTful request vs legacy command-based request
52
- // New style: command with '/' (e.g., 'general/uuid') or URL path without command
53
- // Legacy: command with ':' (e.g., 'general:generate-uuid')
54
- const urlPath = req.path || '';
55
- const command = assistant.request.data.command || '';
56
-
57
- // Strip prefix: /backend-manager/ (hosting rewrite) or /bm_api/ (direct function URL) or just leading slash
58
- const pathAfterBm = urlPath
59
- .replace(/^\/(backend-manager|bm_api)\/?/, '')
60
- .replace(/^\//, '');
61
-
62
- // New style if: command contains '/' OR (URL has path AND no command)
63
- const isNewStyleCommand = command.includes('/') && !command.includes(':');
64
- const isNewStyleUrl = pathAfterBm.length > 0 && !command;
65
- const isNewStyleRequest = isNewStyleCommand || isNewStyleUrl;
66
-
67
- if (isNewStyleRequest) {
68
- // Route path is either from command or URL
69
- const routePath = isNewStyleCommand ? command : pathAfterBm;
70
- return self.routeToMiddleware(routePath);
71
- }
72
-
51
+ // Legacy command-based API only - new-style requests are routed via RequestRouter
73
52
  // Set properties
74
53
  self.payload.data = assistant.request.data;
75
54
  self.payload.user = await assistant.authenticate();
@@ -401,24 +380,6 @@ Module.prototype.resolveApiPath = function (command) {
401
380
  }
402
381
  };
403
382
 
404
- Module.prototype.routeToMiddleware = function (routePath) {
405
- const self = this;
406
- const Manager = self.Manager;
407
- const req = self.req;
408
- const res = self.res;
409
-
410
- // Set paths for BEM internal routes/schemas
411
- // __dirname is src/manager/functions/core/actions, so we need to go up to src/manager
412
- const bemRoutesDir = path.resolve(__dirname, '../../../routes');
413
- const bemSchemasDir = path.resolve(__dirname, '../../../schemas');
414
-
415
- return Manager.Middleware(req, res).run(routePath, {
416
- routesDir: bemRoutesDir,
417
- schemasDir: bemSchemasDir,
418
- schema: routePath,
419
- });
420
- };
421
-
422
383
  function stripUrl(url) {
423
384
  const newUrl = new URL(url);
424
385
 
@@ -0,0 +1,51 @@
1
+ /**
2
+ * BemRouter
3
+ * Routes incoming requests to either the legacy command-based API
4
+ * or the new RESTful middleware system.
5
+ *
6
+ * Detection rules:
7
+ * - Legacy: command with ':' AND no meaningful route path
8
+ * - Direct function call: /us-central1/bm_api (no path after prefix)
9
+ * - Hosting rewrite: /backend-manager (no path after prefix)
10
+ * - New: URL path like /backend-manager/user/sign-up (has path after prefix)
11
+ */
12
+
13
+ function BemRouter(Manager, req, res) {
14
+ const self = this;
15
+
16
+ self.Manager = Manager;
17
+ self.req = req;
18
+ self.res = res;
19
+ }
20
+
21
+ BemRouter.prototype.resolve = function () {
22
+ const self = this;
23
+ const req = self.req;
24
+
25
+ // Extract command from body/query (legacy format)
26
+ const body = req.body || {};
27
+ const query = req.query || {};
28
+ const command = body.command || query.command || '';
29
+
30
+ // Extract URL path
31
+ const urlPath = req.path || '';
32
+
33
+ // Strip prefix: /backend-manager/ or /bm_api/ or leading slash
34
+ const routePath = urlPath
35
+ .replace(/^\/(backend-manager|bm_api)\/?/, '')
36
+ .replace(/^\//, '');
37
+
38
+ // Legacy if: command contains ':' AND routePath is empty
39
+ // (called via direct function URL or hosting rewrite without a sub-path)
40
+ const isLegacy = command.includes(':') && !routePath;
41
+
42
+ return {
43
+ type: isLegacy ? 'legacy' : 'middleware',
44
+ command: command,
45
+ routePath: routePath,
46
+ isLegacy: isLegacy,
47
+ isNewStyle: !isLegacy,
48
+ };
49
+ };
50
+
51
+ module.exports = BemRouter;
@@ -396,6 +396,21 @@ Manager.prototype._preProcess = function (mod) {
396
396
  });
397
397
  };
398
398
 
399
+ Manager.prototype._processMiddleware = function (req, res, routePath) {
400
+ const self = this;
401
+
402
+ // Set paths for BEM internal routes/schemas
403
+ const bemRoutesDir = path.resolve(__dirname, './routes');
404
+ const bemSchemasDir = path.resolve(__dirname, './schemas');
405
+
406
+ // Route directly through middleware (no hooks for new system)
407
+ return self.Middleware(req, res).run(routePath, {
408
+ routesDir: bemRoutesDir,
409
+ schemasDir: bemSchemasDir,
410
+ schema: routePath,
411
+ });
412
+ };
413
+
399
414
  // Manager.prototype.Assistant = function(ref, options) {
400
415
  // const self = this;
401
416
  // ref = ref || {};
@@ -469,6 +484,12 @@ Manager.prototype.Middleware = function () {
469
484
  return new self.libraries.Middleware(self, ...arguments);
470
485
  };
471
486
 
487
+ Manager.prototype.BemRouter = function (req, res) {
488
+ const self = this;
489
+ self.libraries.BemRouter = self.libraries.BemRouter || require('./helpers/bem-router.js');
490
+ return new self.libraries.BemRouter(self, req, res);
491
+ };
492
+
472
493
  Manager.prototype.EventMiddleware = function (payload) {
473
494
  const self = this;
474
495
  self.libraries.EventMiddleware = self.libraries.EventMiddleware || require('./helpers/event-middleware.js');
@@ -707,8 +728,17 @@ Manager.prototype.setupFunctions = function (exporter, options) {
707
728
  exporter.bm_api =
708
729
  self.libraries.functions
709
730
  .runWith({memory: '256MB', timeoutSeconds: 60 * 5})
710
- // TODO: Replace this with new API
711
- .https.onRequest(async (req, res) => self._process((new (require(`${core}/actions/api.js`))()).init(self, { req: req, res: res, })));
731
+ .https.onRequest(async (req, res) => {
732
+ const route = self.BemRouter(req, res).resolve();
733
+
734
+ if (route.isLegacy) {
735
+ // Legacy command-based API -> goes through api.js + _process() for hooks
736
+ return self._process((new (require(`${core}/actions/api.js`))()).init(self, { req, res }));
737
+ } else {
738
+ // New RESTful middleware system -> direct to middleware (no hooks)
739
+ return self._processMiddleware(req, res, route.routePath);
740
+ }
741
+ });
712
742
 
713
743
  // Setup legacy functions
714
744
  if (options.setupFunctionsLegacy) {
@@ -195,7 +195,7 @@ module.exports = {
195
195
  {
196
196
  name: 'non-admin-rejected',
197
197
  async run({ http, assert }) {
198
- const queryResponse = await http.as('user').command('admin:firestore-query', {
198
+ const queryResponse = await http.as('basic').command('admin:firestore-query', {
199
199
  queries: [{ collection: TEST_COLLECTION }],
200
200
  });
201
201
 
@@ -225,12 +225,12 @@ module.exports = {
225
225
 
226
226
  assert.isSuccess(response, 'Add marketing contact with specific providers should succeed');
227
227
 
228
- // Should only have sendgrid result
229
- if (response.data?.providers) {
230
- assert.hasProperty(response.data.providers, 'sendgrid', 'Should have SendGrid result');
231
- }
232
-
228
+ // Only check providers if TEST_EXTENDED_MODE is set (external APIs are called)
233
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
234
  state.sendgridAdded = response.data?.providers?.sendgrid?.success;
235
235
  // Beehiiv not called since we only specified sendgrid
236
236
  }