backend-manager 5.0.91 → 5.0.93

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 (61) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/CLAUDE.md +14 -6
  3. package/README.md +6 -6
  4. package/TODO-MARKETING.md +3 -0
  5. package/TODO-PAYMENT-v2.md +71 -0
  6. package/TODO.md +7 -0
  7. package/package.json +3 -3
  8. package/src/cli/commands/{emulators.js → emulator.js} +15 -15
  9. package/src/cli/commands/index.js +1 -1
  10. package/src/cli/commands/setup-tests/{emulators-config.js → emulator-config.js} +4 -4
  11. package/src/cli/commands/setup-tests/index.js +2 -2
  12. package/src/cli/commands/setup-tests/project-id-consistency.js +1 -1
  13. package/src/cli/commands/test.js +16 -16
  14. package/src/cli/index.js +4 -4
  15. package/src/manager/events/auth/on-create.js +5 -158
  16. package/src/manager/events/firestore/payments-webhooks/analytics.js +4 -3
  17. package/src/manager/events/firestore/payments-webhooks/on-write.js +56 -6
  18. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +3 -3
  19. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +1 -1
  20. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +32 -28
  21. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +3 -3
  22. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +3 -3
  23. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +3 -3
  24. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +3 -3
  25. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +3 -3
  26. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +3 -3
  27. package/src/manager/functions/core/actions/api/admin/send-email.js +0 -131
  28. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +2 -137
  29. package/src/manager/functions/core/actions/api/general/emails/general:download-app-link.js +2 -2
  30. package/src/manager/functions/core/actions/api/user/sign-up.js +1 -1
  31. package/src/manager/index.js +12 -0
  32. package/src/manager/libraries/email.js +523 -0
  33. package/src/manager/libraries/infer-contact.js +140 -0
  34. package/src/manager/libraries/prompts/infer-contact.md +43 -0
  35. package/src/manager/routes/admin/backup/post.js +4 -3
  36. package/src/manager/routes/admin/email/post.js +11 -428
  37. package/src/manager/routes/admin/hook/post.js +3 -2
  38. package/src/manager/routes/admin/notification/post.js +14 -12
  39. package/src/manager/routes/admin/post/post.js +5 -6
  40. package/src/manager/routes/admin/post/put.js +3 -2
  41. package/src/manager/routes/admin/stats/get.js +19 -10
  42. package/src/manager/routes/general/email/post.js +8 -21
  43. package/src/manager/routes/general/email/templates/download-app-link.js +2 -2
  44. package/src/manager/routes/marketing/contact/post.js +2 -100
  45. package/src/manager/routes/payments/intent/post.js +0 -2
  46. package/src/manager/routes/payments/intent/processors/test.js +9 -10
  47. package/src/manager/routes/user/oauth2/_helpers.js +3 -2
  48. package/src/manager/routes/user/oauth2/delete.js +3 -3
  49. package/src/manager/routes/user/oauth2/get.js +2 -2
  50. package/src/manager/routes/user/oauth2/post.js +9 -9
  51. package/src/manager/routes/user/sessions/delete.js +4 -3
  52. package/src/manager/routes/user/signup/post.js +254 -54
  53. package/src/manager/schemas/admin/email/post.js +13 -8
  54. package/src/test/run-tests.js +1 -1
  55. package/test/functions/admin/send-email.js +1 -88
  56. package/test/helpers/email.js +421 -0
  57. package/test/helpers/infer-contact.js +299 -0
  58. package/test/routes/admin/email.js +41 -90
  59. package/REFACTOR-BEM-API.md +0 -76
  60. package/REFACTOR-MIDDLEWARE.md +0 -62
  61. package/REFACTOR-PAYMENT.md +0 -66
package/CHANGELOG.md CHANGED
@@ -33,7 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33
33
  - Payment schemas for webhook and intent validation.
34
34
  - `payment.processors` config section for Stripe, PayPal, Chargebee, and Coinbase configuration.
35
35
  - `npx bm stripe` CLI command for standalone Stripe webhook forwarding.
36
- - Auto-start Stripe CLI webhook forwarding with `npx bm emulators` (gracefully skips when prerequisites are missing).
36
+ - Auto-start Stripe CLI webhook forwarding with `npx bm emulator` (gracefully skips when prerequisites are missing).
37
37
  - `Manager.version` property exposing the BEM package version.
38
38
  - Journey test accounts for payment lifecycle testing (upgrade, cancel, suspend, trial).
39
39
  - Stripe fixture data for subscription states (active, trialing, canceled).
@@ -57,7 +57,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
57
57
  - Firestore security rules testing support.
58
58
  - HTTP client with auth helpers (`http.as('admin').command()`).
59
59
  - Rich assertion library (`isSuccess`, `isError`, `hasProperty`, etc.).
60
- - New `bm emulators` command for standalone emulator management.
60
+ - New `bm emulator` command for standalone emulator management.
61
61
  - Enhanced `bm test` with path filtering and parallel test support.
62
62
 
63
63
  ### Changed
package/CLAUDE.md CHANGED
@@ -414,10 +414,10 @@ Manager.handlers.bm_api = function (mod, position) {
414
414
  ### Running Tests
415
415
  ```bash
416
416
  # Option 1: Two terminals
417
- npx bm emulators # Terminal 1 - keeps emulators running
418
- npx bm test # Terminal 2 - runs tests
417
+ npx bm emulator # Terminal 1 - keeps emulator running
418
+ npx bm test # Terminal 2 - runs tests
419
419
 
420
- # Option 2: Single command (auto-starts emulators)
420
+ # Option 2: Single command (auto-starts emulator)
421
421
  npx bm test
422
422
  ```
423
423
 
@@ -519,7 +519,7 @@ assert.fail(message) // Explicit fail
519
519
 
520
520
  ## Stripe Webhook Forwarding
521
521
 
522
- BEM auto-starts Stripe CLI webhook forwarding when running `npx bm serve` or `npx bm emulators`. This forwards Stripe test webhooks to the local server so the full payment pipeline works end-to-end during development.
522
+ BEM auto-starts Stripe CLI webhook forwarding when running `npx bm serve` or `npx bm emulator`. This forwards Stripe test webhooks to the local server so the full payment pipeline works end-to-end during development.
523
523
 
524
524
  **Requirements:**
525
525
  - `STRIPE_SECRET_KEY` set in `functions/.env`
@@ -830,14 +830,22 @@ The `test` processor generates Stripe-shaped data and auto-fires webhooks to the
830
830
  | Middleware pipeline | `src/manager/helpers/middleware.js` |
831
831
  | Schema validation | `src/manager/helpers/settings.js` |
832
832
  | Rate limiting | `src/manager/helpers/usage.js` |
833
- | User properties | `src/manager/helpers/user.js` |
833
+ | User properties + schema | `src/manager/helpers/user.js` |
834
834
  | Batch utilities | `src/manager/helpers/utilities.js` |
835
835
  | Main API handler | `src/manager/functions/core/actions/api.js` |
836
836
  | Config template | `templates/backend-manager-config.json` |
837
837
  | CLI entry | `src/cli/index.js` |
838
838
  | Stripe webhook forwarding | `src/cli/commands/stripe.js` |
839
+ | Intent creation | `src/manager/routes/payments/intent/post.js` |
840
+ | Webhook ingestion | `src/manager/routes/payments/webhook/post.js` |
841
+ | Webhook processing (on-write) | `src/manager/events/firestore/payments-webhooks/on-write.js` |
842
+ | Payment analytics | `src/manager/events/firestore/payments-webhooks/analytics.js` |
843
+ | Transition detection | `src/manager/events/firestore/payments-webhooks/transitions/index.js` |
839
844
  | Payment processor libraries | `src/manager/libraries/payment-processors/` |
840
- | Payment transition handlers | `src/manager/events/firestore/payments-webhooks/transitions/` |
845
+ | Stripe library | `src/manager/libraries/payment-processors/stripe.js` |
846
+ | Price ID resolver | `src/manager/libraries/payment-processors/resolve-price-id.js` |
847
+ | Order ID generator | `src/manager/libraries/payment-processors/order-id.js` |
848
+ | Test accounts | `src/test/test-accounts.js` |
841
849
 
842
850
  ## Environment Detection
843
851
 
package/README.md CHANGED
@@ -733,7 +733,7 @@ npx backend-manager <command>
733
733
  | `bem serve` | Start local Firebase emulator |
734
734
  | `bem deploy` | Deploy functions to Firebase |
735
735
  | `bem test [paths...]` | Run integration tests |
736
- | `bem emulators` | Start Firebase emulators (keep-alive mode) |
736
+ | `bem emulator` | Start Firebase emulator (keep-alive mode) |
737
737
  | `bem stripe` | Start Stripe CLI webhook forwarding to local server |
738
738
  | `bem version`, `bem v` | Show BEM version |
739
739
  | `bem clear` | Clear cache and temp files |
@@ -749,7 +749,7 @@ Set these in your `functions/.env` file:
749
749
  | Variable | Description |
750
750
  |----------|-------------|
751
751
  | `BACKEND_MANAGER_KEY` | Admin authentication key |
752
- | `STRIPE_SECRET_KEY` | Stripe secret key (enables auto webhook forwarding in `serve`/`emulators`) |
752
+ | `STRIPE_SECRET_KEY` | Stripe secret key (enables auto webhook forwarding in `serve`/`emulator`) |
753
753
 
754
754
  ## Response Headers
755
755
 
@@ -761,16 +761,16 @@ bm-properties: {"code":200,"tag":"functionName/executionId","usage":{...},"schem
761
761
 
762
762
  ## Testing
763
763
 
764
- BEM includes an integration test framework that runs against Firebase emulators.
764
+ BEM includes an integration test framework that runs against the Firebase emulator.
765
765
 
766
766
  ### Running Tests
767
767
 
768
768
  ```bash
769
769
  # Option 1: Two terminals (recommended for development)
770
- npx bm emulators # Terminal 1 - keeps emulators running
771
- npx bm test # Terminal 2 - runs tests
770
+ npx bm emulator # Terminal 1 - keeps emulator running
771
+ npx bm test # Terminal 2 - runs tests
772
772
 
773
- # Option 2: Single command (auto-starts emulators, shuts down after)
773
+ # Option 2: Single command (auto-starts emulator, shuts down after)
774
774
  npx bm test
775
775
  ```
776
776
 
@@ -0,0 +1,3 @@
1
+ 1. Determine name via AI on signup
2
+ 2. Add to sendgrid marketig cotacts
3
+ 3. then, develop a way to SYNC them to the marketing contacts. we coould sync on payment changes (liek newly subscribed, cancelled, etc) so we can segment them??
@@ -0,0 +1,71 @@
1
+ TODO payments
2
+
3
+ would it be beneficial to... check the time on the event and the time in our datbase and skip if the time in oour database is nwer than the vent, indicatig that it is stale? or is this redundatn since we fetch the latest resource anyway?
4
+
5
+ next, we need to do certain things based on the CHANGE thats happening to the resource (subscription or one time) that is different that simply updating the user doc and the payments-subscriptions or payments-one-time collection. For example:
6
+ * if a new subscription is created, we send a welcome email and grant access to the product (but this cant happen when a user fixes their payment method from a failed payment, only when a new subscription is created)
7
+ * if a subscription is cancelled, we send a cancellation email
8
+ * if a subscription payment fails, we send a paymetn failed "please update your payment method" email
9
+ * if a subscription payment succeeds after previously failing, we send a "payment successful, your access has been restored" email
10
+
11
+ more endpoints to build
12
+ payments/cancel
13
+ * takes a subscrition id and requests to cancel at the end of the billing period (not immediately). this can only be done if the user has an non cancelled subscription.
14
+ * there should be an accompanying frontend form that asks some outboardig quetsions
15
+ * Why are you cancelling (checkboxes + textbox) with randomzed order of the checkboes
16
+ * after doing this, users should still be able to reactivate it
17
+ payments/manage
18
+ * fetches the customer portal link from the payment processor and redirects the user there
19
+ * acessible from the user's account page in biling setion
20
+ * only accessible if the user has a non cancelled subscription
21
+
22
+ Note: * for managing and cancelling, they should be under a sinlge button/dropdown called "manage subscription".
23
+ * it could have links to the 2 pages as items in the dropdown, or we could take them to a dedicated page (or expand an accordian) that has more information about managing it
24
+ * we need to make it hard to cancel, buried in some info, and make it more prominent to manage it, since we want to encourage users to manage their subscription rather than cancelling it.
25
+
26
+ payments/refund
27
+ * takes a subscription id and requests a refund.
28
+ * we can only refund the most recent payment and we can only refund if the subscription is cancelled.
29
+ * we can only refund if the most recent payment in FULL was made less than 7 days ago, otherwise we can only refund a prorated amount.
30
+ payments/reactivate
31
+ * takes a subscription id and reactivates a cancelled subscription. this can only be done if the subscription is still within its billing period, otherwise the user would have to create a new subscription.
32
+ payments/upgrade
33
+ * takes a subscription id and a new plan id and upgrades the user's subscription to the new plan. this can only be done if the user has an active subscription.
34
+
35
+
36
+ trial support:
37
+ when a peyment intent happens, we can only grant a trial if the user has never had a trial which involves checking the paymetns-subscriptions collection for the user to see if any exist. if any exist in any form, then we dont give a trial.
38
+ we also need a test for this
39
+
40
+ block multipl epayments
41
+ check if user has a non-cancelled sub during payment intent creation, if so, block the payment intent from being created. this will prevent multiple payments from being made at the same time and causing issues. we can check the payments-subscriptions collection for ALL usbcriptions belonging to the UID. we can use this for the trial check as well (recycle the results).
42
+ we also need a test for this
43
+
44
+
45
+
46
+ also, can you check and confirm if the usage.js sends back the users current usage in th headers? i think it does? maybe it our tests we can check to ensure that the user is probably having their usage set this way?
47
+
48
+
49
+ MANAGEMENT LINK
50
+ we need an endpoint that returns a management link for the user to manage their subscription. this will involve us looking up the users current subscription, then calling the appropriate method on the payment processor to get a management link. we can only return a management link if the user has an active subscription.
51
+
52
+
53
+ TODO
54
+
55
+ * authorizations ystem that tries to charge the card to see if its valid?
56
+ * bm_cronDaily task that doublechecks subscriptions??
57
+ * could check existing ones eveyr month to ensure they are still vlaud and not messed up from failed webhook processing or could check every day to ensure theres no users with multiple subscirptions active simultaneously?
58
+ * bm_cronDaily yto handle disputes?
59
+ * automatically provide evidence?
60
+
61
+ ATTRIBUTIONS!!!
62
+ // Filter attribution entries older than 30 days
63
+ const attribution = webManager.storage().get('attribution', {});
64
+ const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days in ms
65
+ const filtered = {};
66
+
67
+ for (const [key, entry] of Object.entries(attribution)) {
68
+ if (!entry?.timestamp) continue;
69
+ if ((Date.now() - new Date(entry.timestamp).getTime()) < maxAge) {
70
+ filtered[key] = entry;
71
+ }
package/TODO.md CHANGED
@@ -23,11 +23,18 @@ BEM
23
23
  * Teach it how to mock requests and use test user's SECRET API KEYS to authenticate requests
24
24
  * BEM should create a few test accounts: basic, then one for each plan level
25
25
 
26
+ TODO
27
+ Update deps!!!! theres lots
28
+ * we culd have a test that TRIES EACH IMPORT ?? incase updating it fails due to ESM VULLSHIT?
29
+
26
30
 
27
31
  ADD HEALTHCHECK TO BEM!!!
28
32
  ✗ https://api.clockii.com/backend-manager?command=healthcheck → fetch failed
29
33
 
30
34
  TODO
35
+ PAYMENT
36
+ https://hookdeck.com/webhooks/platforms/guide-to-paypal-webhooks-features-and-best-practices
37
+
31
38
 
32
39
  # TEST REWRRK
33
40
  btw... the account.json needs to be removed. remove it from BEM and the consumong project. when we make our test system, we DO NOT NEED TO STORE THE ACCOUBT IN A JSON file. we just need a source of truth in BEM for what uid/emails to look for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.91",
3
+ "version": "5.0.93",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "projectScripts": {
21
21
  "start": "npx bm setup && npx bm serve",
22
22
  "deploy": "npx bm setup && npx bm deploy",
23
- "emulators": "npx bm setup && npx bm emulators",
23
+ "emulator": "npx bm setup && npx bm emulator",
24
24
  "test": "npx bm setup && npx bm test",
25
25
  "setup": "npx bm setup"
26
26
  },
@@ -49,7 +49,7 @@
49
49
  "@google-cloud/pubsub": "^5.3.0",
50
50
  "@google-cloud/storage": "^7.19.0",
51
51
  "@octokit/rest": "^19.0.13",
52
- "@sendgrid/mail": "^7.7.0",
52
+ "@sendgrid/mail": "^8.1.6",
53
53
  "@sentry/node": "^6.19.7",
54
54
  "body-parser": "^1.20.4",
55
55
  "busboy": "^1.6.0",
@@ -5,12 +5,12 @@ const jetpack = require('fs-jetpack');
5
5
  const JSON5 = require('json5');
6
6
  const powertools = require('node-powertools');
7
7
  const WatchCommand = require('./watch');
8
- const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulators-config');
8
+ const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulator-config');
9
9
 
10
- class EmulatorsCommand extends BaseCommand {
10
+ class EmulatorCommand extends BaseCommand {
11
11
  async execute() {
12
- this.log(chalk.cyan('\n Starting Firebase emulators (keep-alive mode)...\n'));
13
- this.log(chalk.gray(' Emulators will stay running until you press Ctrl+C\n'));
12
+ this.log(chalk.cyan('\n Starting Firebase emulator (keep-alive mode)...\n'));
13
+ this.log(chalk.gray(' Emulator will stay running until you press Ctrl+C\n'));
14
14
 
15
15
  // Warn if TEST_EXTENDED_MODE is enabled
16
16
  if (process.env.TEST_EXTENDED_MODE) {
@@ -26,29 +26,29 @@ class EmulatorsCommand extends BaseCommand {
26
26
  // Start Stripe webhook forwarding in background
27
27
  this.startStripeWebhookForwarding();
28
28
 
29
- // Run emulators with keep-alive command (use single quotes since runWithEmulators wraps in double quotes)
30
- const keepAliveCommand = "echo ''; echo 'Emulators ready. Press Ctrl+C to shut down...'; sleep 86400";
29
+ // Run emulator with keep-alive command (use single quotes since runWithEmulator wraps in double quotes)
30
+ const keepAliveCommand = "echo ''; echo 'Emulator ready. Press Ctrl+C to shut down...'; sleep 86400";
31
31
 
32
32
  try {
33
- await this.runWithEmulators(keepAliveCommand);
33
+ await this.runWithEmulator(keepAliveCommand);
34
34
  } catch (error) {
35
35
  // User pressed Ctrl+C - this is expected
36
- this.log(chalk.gray('\n Emulators stopped.\n'));
36
+ this.log(chalk.gray('\n Emulator stopped.\n'));
37
37
  }
38
38
  }
39
39
 
40
40
  /**
41
- * Run a command with Firebase emulators
42
- * @param {string} command - The command to execute inside emulators:exec
41
+ * Run a command with Firebase emulator
42
+ * @param {string} command - The command to execute inside the Firebase emulator
43
43
  * @returns {Promise<void>}
44
44
  */
45
- async runWithEmulators(command) {
45
+ async runWithEmulator(command) {
46
46
  const projectDir = this.main.firebaseProjectPath;
47
47
 
48
48
  // Load emulator ports from firebase.json
49
49
  const emulatorPorts = this.loadEmulatorPorts(projectDir);
50
50
 
51
- // Check for port conflicts before starting emulators
51
+ // Check for port conflicts before starting emulator
52
52
  const canProceed = await this.checkAndKillBlockingProcesses(emulatorPorts);
53
53
  if (!canProceed) {
54
54
  throw new Error('Port conflicts could not be resolved');
@@ -58,9 +58,9 @@ class EmulatorsCommand extends BaseCommand {
58
58
  // hosting is included so localhost:5002 rewrites work (e.g., /backend-manager -> bm_api)
59
59
  // pubsub is included so scheduled functions (bm_cronDaily) can be triggered in tests
60
60
  // Use double quotes for command wrapper since the command may contain single quotes (JSON strings)
61
- const emulatorsCommand = `BEM_TESTING=true firebase emulators:exec --only functions,firestore,auth,database,hosting,pubsub --ui "${command}"`;
61
+ const emulatorCommand = `BEM_TESTING=true firebase emulators:exec --only functions,firestore,auth,database,hosting,pubsub --ui "${command}"`;
62
62
 
63
- await powertools.execute(emulatorsCommand, {
63
+ await powertools.execute(emulatorCommand, {
64
64
  log: true,
65
65
  cwd: projectDir,
66
66
  });
@@ -90,4 +90,4 @@ class EmulatorsCommand extends BaseCommand {
90
90
  }
91
91
  }
92
92
 
93
- module.exports = EmulatorsCommand;
93
+ module.exports = EmulatorCommand;
@@ -8,7 +8,7 @@ module.exports = {
8
8
  ServeCommand: require('./serve'),
9
9
  DeployCommand: require('./deploy'),
10
10
  TestCommand: require('./test'),
11
- EmulatorsCommand: require('./emulators'),
11
+ EmulatorCommand: require('./emulator'),
12
12
  CleanCommand: require('./clean'),
13
13
  IndexesCommand: require('./indexes'),
14
14
  WatchCommand: require('./watch'),
@@ -25,9 +25,9 @@ const REQUIRED_EMULATORS = {
25
25
  ui: { enabled: true, port: DEFAULT_EMULATOR_PORTS.ui },
26
26
  };
27
27
 
28
- class EmulatorsConfigTest extends BaseTest {
28
+ class EmulatorConfigTest extends BaseTest {
29
29
  getName() {
30
- return 'emulators config in firebase.json';
30
+ return 'emulator config in firebase.json';
31
31
  }
32
32
 
33
33
  async run() {
@@ -75,5 +75,5 @@ class EmulatorsConfigTest extends BaseTest {
75
75
  }
76
76
  }
77
77
 
78
- module.exports = EmulatorsConfigTest;
79
- module.exports.DEFAULT_EMULATOR_PORTS = DEFAULT_EMULATOR_PORTS;
78
+ module.exports = EmulatorConfigTest;
79
+ module.exports.DEFAULT_EMULATOR_PORTS = DEFAULT_EMULATOR_PORTS;
@@ -24,7 +24,7 @@ const FirestoreIndexesInJsonTest = require('./firestore-indexes-in-json');
24
24
  const RealtimeRulesInJsonTest = require('./realtime-rules-in-json');
25
25
  const StorageRulesInJsonTest = require('./storage-rules-in-json');
26
26
  const RemoteconfigTemplateInJsonTest = require('./remoteconfig-template-in-json');
27
- const EmulatorsConfigTest = require('./emulators-config');
27
+ const EmulatorConfigTest = require('./emulator-config');
28
28
  const HostingRewritesTest = require('./hosting-rewrites');
29
29
  const FirestoreIndexesSyncedTest = require('./firestore-indexes-synced');
30
30
  const StorageLifecyclePolicyTest = require('./storage-lifecycle-policy');
@@ -64,7 +64,7 @@ function getTests(context) {
64
64
  new RealtimeRulesInJsonTest(context),
65
65
  new StorageRulesInJsonTest(context),
66
66
  new RemoteconfigTemplateInJsonTest(context),
67
- new EmulatorsConfigTest(context),
67
+ new EmulatorConfigTest(context),
68
68
  new HostingRewritesTest(context),
69
69
  new FirestoreIndexesSyncedTest(context),
70
70
  new StorageLifecyclePolicyTest(context),
@@ -9,7 +9,7 @@ const chalk = require('chalk');
9
9
  * - functions/backend-manager-config.json (firebaseConfig.projectId)
10
10
  * - functions/service-account.json (project_id)
11
11
  *
12
- * Mismatches cause tests to fail when running emulators in separate terminals
12
+ * Mismatches cause tests to fail when running emulator in separate terminals
13
13
  * because different parts of the system connect to different Firestore databases.
14
14
  */
15
15
  class ProjectIdConsistencyTest extends BaseTest {
@@ -4,8 +4,8 @@ const chalk = require('chalk');
4
4
  const jetpack = require('fs-jetpack');
5
5
  const JSON5 = require('json5');
6
6
  const powertools = require('node-powertools');
7
- const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulators-config');
8
- const EmulatorsCommand = require('./emulators');
7
+ const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulator-config');
8
+ const EmulatorCommand = require('./emulator');
9
9
 
10
10
  class TestCommand extends BaseCommand {
11
11
  async execute() {
@@ -42,14 +42,14 @@ class TestCommand extends BaseCommand {
42
42
  // Build the test command
43
43
  const testCommand = this.buildTestCommand(testConfig);
44
44
 
45
- // Check if emulators are already running
46
- const emulatorsRunning = this.areEmulatorsRunning(emulatorPorts);
45
+ // Check if emulator is already running
46
+ const emulatorRunning = this.isEmulatorRunning(emulatorPorts);
47
47
 
48
- if (emulatorsRunning) {
49
- this.log(chalk.cyan('Running tests against EXISTING emulators'));
48
+ if (emulatorRunning) {
49
+ this.log(chalk.cyan('Running tests against EXISTING emulator'));
50
50
  await this.runTestsDirectly(testCommand, functionsDir, emulatorPorts);
51
51
  } else {
52
- this.log(chalk.cyan('Starting emulators and running tests...'));
52
+ this.log(chalk.cyan('Starting emulator and running tests...'));
53
53
  await this.runEmulatorTests(testCommand);
54
54
  }
55
55
  }
@@ -164,16 +164,16 @@ class TestCommand extends BaseCommand {
164
164
  }
165
165
 
166
166
  /**
167
- * Check if emulators are already running
167
+ * Check if emulator is already running
168
168
  */
169
- areEmulatorsRunning(emulatorPorts) {
169
+ isEmulatorRunning(emulatorPorts) {
170
170
  // Check if functions emulator port is in use
171
- // If it is, assume all emulators are running
171
+ // If it is, assume emulator is running
172
172
  return this.isPortInUse(emulatorPorts.functions);
173
173
  }
174
174
 
175
175
  /**
176
- * Run tests directly (emulators already running)
176
+ * Run tests directly (emulator already running)
177
177
  */
178
178
  async runTestsDirectly(testCommand, functionsDir, emulatorPorts) {
179
179
  this.log(chalk.gray(` Hosting: http://127.0.0.1:${emulatorPorts.hosting}`));
@@ -192,16 +192,16 @@ class TestCommand extends BaseCommand {
192
192
  }
193
193
 
194
194
  /**
195
- * Run tests with Firebase emulators (starts emulators, runs tests, shuts down)
195
+ * Run tests with Firebase emulator (starts emulator, runs tests, shuts down)
196
196
  */
197
197
  async runEmulatorTests(testCommand) {
198
- this.log(chalk.gray(' Starting Firebase emulators...\n'));
198
+ this.log(chalk.gray(' Starting Firebase emulator...\n'));
199
199
 
200
- // Use EmulatorsCommand to run tests with emulators
201
- const emulatorsCmd = new EmulatorsCommand(this.main);
200
+ // Use EmulatorCommand to run tests with emulator
201
+ const emulatorCmd = new EmulatorCommand(this.main);
202
202
 
203
203
  try {
204
- await emulatorsCmd.runWithEmulators(testCommand);
204
+ await emulatorCmd.runWithEmulator(testCommand);
205
205
  } catch (error) {
206
206
  // Only exit with error if it wasn't a user-initiated exit
207
207
  if (error.code !== 0) {
package/src/cli/index.js CHANGED
@@ -21,7 +21,7 @@ const InstallCommand = require('./commands/install');
21
21
  const ServeCommand = require('./commands/serve');
22
22
  const DeployCommand = require('./commands/deploy');
23
23
  const TestCommand = require('./commands/test');
24
- const EmulatorsCommand = require('./commands/emulators');
24
+ const EmulatorCommand = require('./commands/emulator');
25
25
  const CleanCommand = require('./commands/clean');
26
26
  const IndexesCommand = require('./commands/indexes');
27
27
  const WatchCommand = require('./commands/watch');
@@ -106,9 +106,9 @@ Main.prototype.process = async function (args) {
106
106
  return await cmd.execute();
107
107
  }
108
108
 
109
- // Emulators (keep-alive mode)
110
- if (self.options['emulators'] || self.options['emulator']) {
111
- const cmd = new EmulatorsCommand(self);
109
+ // Emulator (keep-alive mode)
110
+ if (self.options['emulator'] || self.options['emulators']) {
111
+ const cmd = new EmulatorCommand(self);
112
112
  return await cmd.execute();
113
113
  }
114
114
 
@@ -1,5 +1,3 @@
1
- const fetch = require('wonderful-fetch');
2
- const moment = require('moment');
3
1
  const { FieldValue } = require('firebase-admin/firestore');
4
2
 
5
3
  const MAX_RETRIES = 3;
@@ -15,7 +13,9 @@ const RETRY_DELAY_MS = 1000;
15
13
  * - Checks if user doc already exists (auth.uid) → skips if exists (handles test accounts, provider linking)
16
14
  * - Batch writes user doc + increment count atomically
17
15
  * - Retries up to 3 times with exponential backoff on failure
18
- * - Sends analytics event (non-critical, no retry)
16
+ *
17
+ * Non-critical work (name inference, welcome emails, marketing contact) is handled
18
+ * by the user/signup endpoint, which the frontend calls after account creation.
19
19
  */
20
20
  module.exports = async ({ Manager, assistant, user, context, libraries }) => {
21
21
  const startTime = Date.now();
@@ -71,42 +71,13 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
71
71
  await batch.commit();
72
72
  }, MAX_RETRIES, RETRY_DELAY_MS);
73
73
 
74
- assistant.log(`onCreate: Successfully created user doc for ${user.uid}`);
74
+ assistant.log(`onCreate: Successfully created user doc for ${user.uid} (${Date.now() - startTime}ms)`);
75
75
  } catch (error) {
76
76
  assistant.error(`onCreate: Failed to create user doc after ${MAX_RETRIES} retries:`, error);
77
77
 
78
78
  // Don't reject - the user was already created in Auth
79
- // The user:sign-up endpoint will handle creating the doc if it's missing
80
- return;
81
- }
82
-
83
- // Send emails in dev/production, or in test mode if TEST_EXTENDED_MODE=true
84
- // Note: Must be passed to the emulator
85
- const shouldSendEmails = !assistant.isTesting() || process.env.TEST_EXTENDED_MODE;
86
-
87
- if (!shouldSendEmails) {
88
- assistant.log(`onCreate: Skipping emails/SendGrid (BEM_TESTING=true, TEST_EXTENDED_MODE not set)`);
89
- } else {
90
- assistant.log(`onCreate: Sending emails/adding to SendGrid for ${user.uid}`);
91
-
92
- // Add to marketing lists (SendGrid + Beehiiv) via centralized endpoint
93
- fetch(`${Manager.project.apiUrl}/backend-manager/marketing/contact`, {
94
- method: 'POST',
95
- response: 'json',
96
- body: {
97
- backendManagerKey: process.env.BACKEND_MANAGER_KEY,
98
- email: user.email,
99
- source: 'auth:on-create',
100
- },
101
- }).catch(e => assistant.error('onCreate: add-marketing-contact failed:', e));
102
-
103
- // Send welcome emails (non-blocking, don't fail on error)
104
- sendWelcomeEmail(Manager, assistant, user).catch(e => assistant.error('onCreate: sendWelcomeEmail failed:', e));
105
- sendCheckupEmail(Manager, assistant, user).catch(e => assistant.error('onCreate: sendCheckupEmail failed:', e));
106
- sendFeedbackEmail(Manager, assistant, user).catch(e => assistant.error('onCreate: sendFeedbackEmail failed:', e));
79
+ // The user/signup endpoint will handle creating the doc if it's missing
107
80
  }
108
-
109
- assistant.log(`onCreate: Completed for ${user.uid} (${Date.now() - startTime}ms)`);
110
81
  };
111
82
 
112
83
  /**
@@ -133,127 +104,3 @@ async function retryBatchWrite(assistant, fn, maxRetries, delayMs) {
133
104
 
134
105
  throw lastError; // All retries failed
135
106
  }
136
-
137
- /**
138
- * Send welcome email (immediate)
139
- */
140
- function sendWelcomeEmail(Manager, assistant, user) {
141
- return fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
142
- method: 'POST',
143
- response: 'json',
144
- body: {
145
- backendManagerKey: process.env.BACKEND_MANAGER_KEY,
146
- to: [{ email: user.email }],
147
- categories: ['account/welcome'],
148
- subject: `Welcome to ${Manager.config.brand.name}!`,
149
- template: 'd-b7f8da3c98ad49a2ad1e187f3a67b546',
150
- group: 25928,
151
- copy: false,
152
- ensureUnique: true,
153
- data: {
154
- email: {
155
- preview: `Welcome aboard! I'm Ian, the CEO and founder of ${Manager.config.brand.name}. I'm here to ensure your journey with us gets off to a great start.`,
156
- },
157
- body: {
158
- title: `Welcome to ${Manager.config.brand.name}!`,
159
- message: `
160
- Welcome aboard!
161
- <br><br>
162
- I'm Ian, the founder and CEO of <strong>${Manager.config.brand.name}</strong>, and I'm thrilled to have you with us.
163
- Your journey begins today, and we are committed to supporting you every step of the way.
164
- <br><br>
165
- We are dedicated to ensuring your experience is exceptional.
166
- Feel free to reply directly to this email with any questions you may have.
167
- <br><br>
168
- Thank you for choosing <strong>${Manager.config.brand.name}</strong>. Here's to new beginnings!
169
- `,
170
- },
171
- signoff: {
172
- type: 'personal',
173
- name: 'Ian Wiedenman, CEO',
174
- url: `https://ianwiedenman.com?utm_source=welcome-email&utm_medium=email&utm_campaign=${Manager.config.app.id}`,
175
- urlText: '@ianwieds',
176
- },
177
- },
178
- },
179
- })
180
- .then((json) => {
181
- assistant.log('sendWelcomeEmail(): Success', json);
182
- return json;
183
- });
184
- }
185
-
186
- /**
187
- * Send checkup email (7 days after signup)
188
- */
189
- function sendCheckupEmail(Manager, assistant, user) {
190
- return fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
191
- method: 'POST',
192
- response: 'json',
193
- body: {
194
- backendManagerKey: process.env.BACKEND_MANAGER_KEY,
195
- to: [{ email: user.email }],
196
- categories: ['account/checkup'],
197
- subject: `How's your experience with ${Manager.config.brand.name}?`,
198
- template: 'd-b7f8da3c98ad49a2ad1e187f3a67b546',
199
- group: 25928,
200
- copy: false,
201
- ensureUnique: true,
202
- sendAt: moment().add(7, 'days').unix(),
203
- data: {
204
- email: {
205
- preview: `Checking in from ${Manager.config.brand.name} to see how things are going. Let us know if you have any questions or feedback!`,
206
- },
207
- body: {
208
- title: `How's everything going?`,
209
- message: `
210
- Hi there,
211
- <br><br>
212
- It's Ian again from <strong>${Manager.config.brand.name}</strong>. Just checking in to see how things are going for you.
213
- <br><br>
214
- Have you had a chance to explore all our features? Any questions or feedback for us?
215
- <br><br>
216
- We're always here to help, so don't hesitate to reach out. Just reply to this email and we'll get back to you as soon as possible.
217
- <br><br>
218
- Thank you for choosing <strong>${Manager.config.brand.name}</strong>. Here's to new beginnings!
219
- `,
220
- },
221
- signoff: {
222
- type: 'personal',
223
- name: 'Ian Wiedenman, CEO',
224
- url: `https://ianwiedenman.com?utm_source=checkup-email&utm_medium=email&utm_campaign=${Manager.config.app.id}`,
225
- urlText: '@ianwieds',
226
- },
227
- },
228
- },
229
- })
230
- .then((json) => {
231
- assistant.log('sendCheckupEmail(): Success', json);
232
- return json;
233
- });
234
- }
235
-
236
- /**
237
- * Send feedback email (14 days after signup)
238
- */
239
- function sendFeedbackEmail(Manager, assistant, user) {
240
- return fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
241
- method: 'POST',
242
- response: 'json',
243
- body: {
244
- backendManagerKey: process.env.BACKEND_MANAGER_KEY,
245
- to: [{ email: user.email }],
246
- categories: ['engagement/feedback'],
247
- subject: `Want to share your feedback about ${Manager.config.brand.name}?`,
248
- template: 'd-c1522214c67b47058669acc5a81ed663',
249
- group: 25928,
250
- copy: false,
251
- ensureUnique: true,
252
- sendAt: moment().add(14, 'days').unix(),
253
- },
254
- })
255
- .then((json) => {
256
- assistant.log('sendFeedbackEmail(): Success', json);
257
- return json;
258
- });
259
- }