backend-manager 5.0.102 → 5.0.103

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/CLAUDE.md CHANGED
@@ -535,6 +535,62 @@ If any prerequisite is missing, webhook forwarding is silently skipped with an i
535
535
 
536
536
  The forwarding URL is: `http://localhost:{hostingPort}/backend-manager/payments/webhook?processor=stripe&key={BACKEND_MANAGER_KEY}`
537
537
 
538
+ ## CLI Utility Commands
539
+
540
+ Quick commands for reading/writing Firestore and managing Auth users directly from the terminal. Works in any BEM consumer project (requires `functions/service-account.json` for production, or `--emulator` for local).
541
+
542
+ ### Firestore Commands
543
+
544
+ ```bash
545
+ npx bm firestore:get <path> # Read a document
546
+ npx bm firestore:set <path> '<json>' # Write/merge a document
547
+ npx bm firestore:set <path> '<json>' --no-merge # Overwrite a document entirely
548
+ npx bm firestore:query <collection> # Query a collection (default limit 25)
549
+ --where "field==value" # Filter (repeatable for AND)
550
+ --orderBy "field:desc" # Sort
551
+ --limit N # Limit results
552
+ npx bm firestore:delete <path> # Delete a document (prompts for confirmation)
553
+ ```
554
+
555
+ ### Auth Commands
556
+
557
+ ```bash
558
+ npx bm auth:get <uid-or-email> # Get user by UID or email (auto-detected via @)
559
+ npx bm auth:list [--limit N] [--page-token T] # List users (default 100)
560
+ npx bm auth:delete <uid-or-email> # Delete user (prompts for confirmation)
561
+ npx bm auth:set-claims <uid-or-email> '<json>' # Set custom claims
562
+ ```
563
+
564
+ ### Shared Flags
565
+
566
+ | Flag | Description |
567
+ |------|-------------|
568
+ | `--emulator` | Target local emulator instead of production |
569
+ | `--force` | Skip confirmation on destructive operations |
570
+ | `--raw` | Compact JSON output (for piping to `jq` etc.) |
571
+
572
+ ### Examples
573
+
574
+ ```bash
575
+ # Read a user document from production
576
+ npx bm firestore:get users/abc123
577
+
578
+ # Write to emulator
579
+ npx bm firestore:set users/test123 '{"name":"Test User"}' --emulator
580
+
581
+ # Query with filters
582
+ npx bm firestore:query users --where "subscription.status==active" --limit 10
583
+
584
+ # Look up auth user by email
585
+ npx bm auth:get user@example.com
586
+
587
+ # Set admin claims
588
+ npx bm auth:set-claims user@example.com '{"admin":true}'
589
+
590
+ # Delete from emulator (no confirmation needed)
591
+ npx bm firestore:delete users/test123 --emulator
592
+ ```
593
+
538
594
  ## Usage & Rate Limiting
539
595
 
540
596
  ### Overview
@@ -836,6 +892,9 @@ The `test` processor generates Stripe-shaped data and auto-fires webhooks to the
836
892
  | Config template | `templates/backend-manager-config.json` |
837
893
  | CLI entry | `src/cli/index.js` |
838
894
  | Stripe webhook forwarding | `src/cli/commands/stripe.js` |
895
+ | Firebase init helper (CLI) | `src/cli/commands/firebase-init.js` |
896
+ | Firestore CLI commands | `src/cli/commands/firestore.js` |
897
+ | Auth CLI commands | `src/cli/commands/auth.js` |
839
898
  | Intent creation | `src/manager/routes/payments/intent/post.js` |
840
899
  | Webhook ingestion | `src/manager/routes/payments/webhook/post.js` |
841
900
  | Webhook processing (on-write) | `src/manager/events/firestore/payments-webhooks/on-write.js` |
package/README.md CHANGED
@@ -741,6 +741,16 @@ npx backend-manager <command>
741
741
  | `bem clean:npm` | Clean and reinstall npm modules |
742
742
  | `bem firestore:indexes:get` | Get Firestore indexes |
743
743
  | `bem cwd` | Show current working directory |
744
+ | `bem firestore:get <path>` | Read a Firestore document |
745
+ | `bem firestore:set <path> '<json>'` | Write/merge a Firestore document |
746
+ | `bem firestore:query <collection>` | Query a Firestore collection |
747
+ | `bem firestore:delete <path>` | Delete a Firestore document |
748
+ | `bem auth:get <uid-or-email>` | Get an Auth user by UID or email |
749
+ | `bem auth:list` | List Auth users |
750
+ | `bem auth:delete <uid-or-email>` | Delete an Auth user |
751
+ | `bem auth:set-claims <uid-or-email> '<json>'` | Set custom claims on an Auth user |
752
+
753
+ All Firestore and Auth commands support `--emulator` to target the local emulator, `--force` to skip confirmation, and `--raw` for compact JSON output.
744
754
 
745
755
  ## Environment Variables
746
756
 
package/TODO-MARKETING.md CHANGED
@@ -1,3 +1,56 @@
1
1
  1. Determine name via AI on signup
2
2
  2. Add to sendgrid marketig cotacts
3
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??
4
+
5
+
6
+ When user sings up, use AI to generate first name, last name, company???
7
+ When user doc is updated, sync with sendgrid and beehiiv?? like name, premium status, etc
8
+ if admin is set to true from something else then we send an emergency critical email to alert us
9
+
10
+ need to confirm we hv hooks/events setup properly
11
+ handlerPath = `${Manager.cwd}/events/${handlerName}.js`;
12
+ liek bm_cronDaily, is it able to find the BEM path right? what about if we want to add our own per project?
13
+
14
+ implement BEM hooks (removed in muddleware semantic system)
15
+
16
+ https://firebase.google.com/docs/functions/2nd-gen-upgrade
17
+
18
+ ------------
19
+ You are to extract the first name, last name, and company from the provided email.
20
+
21
+ If you can get the company from the email domain, include that as well but DO NOT set the company to generic email providers like gmail, yahoo, etc.
22
+
23
+ You may use a single initial if the email does not provide a full first name.
24
+
25
+ For example:
26
+ jonsnow123@gmail.com, jon.snow123@gmail.com
27
+ First Name: Jon
28
+ Last Name: Snow
29
+ Company:
30
+
31
+ jsnow123@gmail.com, j.snow123@gmail.com
32
+ First Name: J
33
+ Last Name: Snow
34
+ Company:
35
+
36
+ jon.snow@acme.com
37
+ First Name: Jon
38
+ Last Name: Snow
39
+ Company: Acme
40
+
41
+ jsnow@acme.com
42
+ First Name: J
43
+ Last Name: Snow
44
+ Company: Acme
45
+
46
+ jon123@gmail.com
47
+ First Name: Jon
48
+ Last Name:
49
+ Company:
50
+
51
+ every time we touch, cascada
52
+ just dance, lady gaga
53
+ over drake,
54
+ Time, hans zimmer
55
+ yellow, coldplay
56
+ yess bitch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.102",
3
+ "version": "5.0.103",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -0,0 +1,226 @@
1
+ const BaseCommand = require('./base-command');
2
+ const chalk = require('chalk');
3
+ const inquirer = require('inquirer');
4
+ const { initFirebase } = require('./firebase-init');
5
+
6
+ class AuthCommand extends BaseCommand {
7
+ async execute() {
8
+ const argv = this.main.argv;
9
+ const args = argv._ || [];
10
+ const subcommand = args[0]; // e.g., 'auth:get' or 'auth:set-claims'
11
+ const action = subcommand.split(':').slice(1).join(':'); // handles 'auth:set-claims'
12
+
13
+ // Initialize Firebase
14
+ const isEmulator = argv.emulator || false;
15
+ let firebase;
16
+
17
+ try {
18
+ firebase = initFirebase({
19
+ firebaseProjectPath: this.firebaseProjectPath,
20
+ emulator: isEmulator,
21
+ });
22
+ } catch (error) {
23
+ this.logError(`Firebase init failed: ${error.message}`);
24
+ return;
25
+ }
26
+
27
+ const { admin, projectId } = firebase;
28
+ const target = isEmulator ? 'emulator' : 'production';
29
+ this.log(chalk.gray(` Target: ${projectId} (${target})\n`));
30
+
31
+ // Dispatch to subcommand handler
32
+ switch (action) {
33
+ case 'get':
34
+ return await this.get(admin, args, argv);
35
+ case 'list':
36
+ return await this.list(admin, args, argv);
37
+ case 'delete':
38
+ return await this.del(admin, args, argv, isEmulator);
39
+ case 'set-claims':
40
+ return await this.setClaims(admin, args, argv);
41
+ default:
42
+ this.logError(`Unknown auth subcommand: ${action}`);
43
+ this.log(chalk.gray(' Available: auth:get, auth:list, auth:delete, auth:set-claims'));
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Resolve a user identifier to a UserRecord.
49
+ * Accepts UID or email address (auto-detected via @).
50
+ */
51
+ async resolveUser(admin, identifier) {
52
+ if (!identifier) {
53
+ return null;
54
+ }
55
+
56
+ // If it contains '@', treat as email
57
+ if (identifier.includes('@')) {
58
+ return await admin.auth().getUserByEmail(identifier);
59
+ }
60
+
61
+ return await admin.auth().getUser(identifier);
62
+ }
63
+
64
+ /**
65
+ * Get a user by UID or email.
66
+ * Usage: npx bm auth:get user@email.com
67
+ * npx bm auth:get abc123uid
68
+ */
69
+ async get(admin, args, argv) {
70
+ const identifier = args[1];
71
+
72
+ if (!identifier) {
73
+ this.logError('Usage: npx bm auth:get <uid-or-email>');
74
+ return;
75
+ }
76
+
77
+ try {
78
+ const user = await this.resolveUser(admin, identifier);
79
+ this.output(user.toJSON(), argv);
80
+ } catch (error) {
81
+ if (error.code === 'auth/user-not-found') {
82
+ this.logWarning(`User not found: ${identifier}`);
83
+ return;
84
+ }
85
+ this.logError(`Failed to get user: ${error.message}`);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * List users.
91
+ * Usage: npx bm auth:list [--limit 100] [--page-token TOKEN]
92
+ */
93
+ async list(admin, args, argv) {
94
+ const limit = parseInt(argv.limit, 10) || 100;
95
+ const pageToken = argv['page-token'] || argv.pageToken || undefined;
96
+
97
+ try {
98
+ const result = await admin.auth().listUsers(limit, pageToken);
99
+
100
+ const users = result.users.map(user => ({
101
+ uid: user.uid,
102
+ email: user.email || null,
103
+ displayName: user.displayName || null,
104
+ disabled: user.disabled,
105
+ createdAt: user.metadata.creationTime,
106
+ lastSignIn: user.metadata.lastSignInTime,
107
+ customClaims: user.customClaims || {},
108
+ }));
109
+
110
+ this.log(chalk.gray(` Found ${users.length} user(s)\n`));
111
+
112
+ if (result.pageToken) {
113
+ this.log(chalk.gray(` Next page: --page-token ${result.pageToken}\n`));
114
+ }
115
+
116
+ this.output(users, argv);
117
+ } catch (error) {
118
+ this.logError(`Failed to list users: ${error.message}`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Delete a user.
124
+ * Usage: npx bm auth:delete user@email.com [--force]
125
+ */
126
+ async del(admin, args, argv, isEmulator) {
127
+ const identifier = args[1];
128
+
129
+ if (!identifier) {
130
+ this.logError('Usage: npx bm auth:delete <uid-or-email> [--force]');
131
+ return;
132
+ }
133
+
134
+ // Resolve user first to show who we're deleting
135
+ let user;
136
+ try {
137
+ user = await this.resolveUser(admin, identifier);
138
+ } catch (error) {
139
+ if (error.code === 'auth/user-not-found') {
140
+ this.logWarning(`User not found: ${identifier}`);
141
+ return;
142
+ }
143
+ this.logError(`Failed to resolve user: ${error.message}`);
144
+ return;
145
+ }
146
+
147
+ this.log(chalk.gray(` User: ${user.uid} (${user.email || 'no email'})`));
148
+
149
+ // Require confirmation for production (skip for emulator or --force)
150
+ if (!isEmulator && !argv.force) {
151
+ const { confirmed } = await inquirer.prompt([{
152
+ type: 'confirm',
153
+ name: 'confirmed',
154
+ message: `Delete user "${user.uid}" (${user.email || 'no email'}) from PRODUCTION?`,
155
+ default: false,
156
+ }]);
157
+
158
+ if (!confirmed) {
159
+ this.log(chalk.gray(' Aborted.'));
160
+ return;
161
+ }
162
+ }
163
+
164
+ try {
165
+ await admin.auth().deleteUser(user.uid);
166
+ this.logSuccess(`User deleted: ${user.uid}`);
167
+ } catch (error) {
168
+ this.logError(`Failed to delete user: ${error.message}`);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Set custom claims on a user.
174
+ * Usage: npx bm auth:set-claims user@email.com '{"admin": true}'
175
+ */
176
+ async setClaims(admin, args, argv) {
177
+ const identifier = args[1];
178
+ const jsonString = args[2];
179
+
180
+ if (!identifier || !jsonString) {
181
+ this.logError('Usage: npx bm auth:set-claims <uid-or-email> \'<json>\'');
182
+ return;
183
+ }
184
+
185
+ let claims;
186
+ try {
187
+ claims = JSON.parse(jsonString);
188
+ } catch (error) {
189
+ this.logError(`Invalid JSON: ${error.message}`);
190
+ return;
191
+ }
192
+
193
+ let user;
194
+ try {
195
+ user = await this.resolveUser(admin, identifier);
196
+ } catch (error) {
197
+ if (error.code === 'auth/user-not-found') {
198
+ this.logWarning(`User not found: ${identifier}`);
199
+ return;
200
+ }
201
+ this.logError(`Failed to resolve user: ${error.message}`);
202
+ return;
203
+ }
204
+
205
+ try {
206
+ await admin.auth().setCustomUserClaims(user.uid, claims);
207
+ this.logSuccess(`Custom claims set for ${user.uid}:`);
208
+ this.output(claims, argv);
209
+ } catch (error) {
210
+ this.logError(`Failed to set claims: ${error.message}`);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Output data as JSON.
216
+ */
217
+ output(data, argv) {
218
+ if (argv.raw) {
219
+ this.log(JSON.stringify(data));
220
+ } else {
221
+ this.log(JSON.stringify(data, null, 2));
222
+ }
223
+ }
224
+ }
225
+
226
+ module.exports = AuthCommand;
@@ -0,0 +1,121 @@
1
+ const path = require('path');
2
+ const jetpack = require('fs-jetpack');
3
+ const JSON5 = require('json5');
4
+ const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulator-config');
5
+
6
+ /**
7
+ * Initialize firebase-admin for CLI commands.
8
+ *
9
+ * @param {object} options
10
+ * @param {string} options.firebaseProjectPath - Project root (from main.firebaseProjectPath)
11
+ * @param {boolean} options.emulator - Whether to target the local emulator
12
+ * @returns {{ admin: object, projectId: string }}
13
+ */
14
+ function initFirebase({ firebaseProjectPath, emulator }) {
15
+ const functionsDir = path.join(firebaseProjectPath, 'functions');
16
+
17
+ // Load .env so env vars like GCLOUD_PROJECT are available
18
+ const envPath = path.join(functionsDir, '.env');
19
+ if (jetpack.exists(envPath)) {
20
+ require('dotenv').config({ path: envPath });
21
+ }
22
+
23
+ // Resolve firebase-admin from the consumer project's node_modules (peer dep)
24
+ const admin = require(path.join(functionsDir, 'node_modules', 'firebase-admin'));
25
+
26
+ // Already initialized
27
+ if (admin.apps.length > 0) {
28
+ const projectId = admin.apps[0].options.projectId || 'unknown';
29
+ return { admin, projectId };
30
+ }
31
+
32
+ if (emulator) {
33
+ // Load emulator ports from firebase.json
34
+ const emulatorPorts = loadEmulatorPorts(firebaseProjectPath);
35
+
36
+ // Set emulator env vars so firebase-admin connects to emulator
37
+ process.env.FIRESTORE_EMULATOR_HOST = process.env.FIRESTORE_EMULATOR_HOST
38
+ || `127.0.0.1:${emulatorPorts.firestore}`;
39
+ process.env.FIREBASE_AUTH_EMULATOR_HOST = process.env.FIREBASE_AUTH_EMULATOR_HOST
40
+ || `127.0.0.1:${emulatorPorts.auth}`;
41
+
42
+ const projectId = resolveProjectId(firebaseProjectPath, functionsDir);
43
+
44
+ admin.initializeApp({ projectId });
45
+
46
+ return { admin, projectId };
47
+ }
48
+
49
+ // Production: use service-account.json
50
+ const serviceAccountPath = path.join(functionsDir, 'service-account.json');
51
+ if (!jetpack.exists(serviceAccountPath)) {
52
+ throw new Error(
53
+ `Missing service-account.json at ${serviceAccountPath}\n`
54
+ + ` Download it from Firebase Console > Project Settings > Service Accounts`,
55
+ );
56
+ }
57
+
58
+ const serviceAccount = JSON.parse(jetpack.read(serviceAccountPath));
59
+ const projectId = serviceAccount.project_id;
60
+
61
+ admin.initializeApp({
62
+ credential: admin.credential.cert(serviceAccount),
63
+ databaseURL: `https://${projectId}.firebaseio.com`,
64
+ });
65
+
66
+ return { admin, projectId };
67
+ }
68
+
69
+ function loadEmulatorPorts(projectDir) {
70
+ const emulatorPorts = { ...DEFAULT_EMULATOR_PORTS };
71
+ const firebaseJsonPath = path.join(projectDir, 'firebase.json');
72
+
73
+ if (jetpack.exists(firebaseJsonPath)) {
74
+ try {
75
+ const firebaseConfig = JSON5.parse(jetpack.read(firebaseJsonPath));
76
+
77
+ if (firebaseConfig.emulators) {
78
+ for (const name of Object.keys(DEFAULT_EMULATOR_PORTS)) {
79
+ emulatorPorts[name] = firebaseConfig.emulators[name]?.port || DEFAULT_EMULATOR_PORTS[name];
80
+ }
81
+ }
82
+ } catch (e) {
83
+ // Use defaults
84
+ }
85
+ }
86
+
87
+ return emulatorPorts;
88
+ }
89
+
90
+ function resolveProjectId(projectDir, functionsDir) {
91
+ // Try backend-manager-config.json
92
+ const configPath = path.join(functionsDir, 'backend-manager-config.json');
93
+ if (jetpack.exists(configPath)) {
94
+ try {
95
+ const config = JSON5.parse(jetpack.read(configPath));
96
+ if (config.firebaseConfig?.projectId) {
97
+ return config.firebaseConfig.projectId;
98
+ }
99
+ } catch (e) {
100
+ // Fall through
101
+ }
102
+ }
103
+
104
+ // Try .firebaserc
105
+ const rcPath = path.join(projectDir, '.firebaserc');
106
+ if (jetpack.exists(rcPath)) {
107
+ try {
108
+ const rc = JSON.parse(jetpack.read(rcPath));
109
+ if (rc.projects?.default) {
110
+ return rc.projects.default;
111
+ }
112
+ } catch (e) {
113
+ // Fall through
114
+ }
115
+ }
116
+
117
+ // Fallback to env
118
+ return process.env.GCLOUD_PROJECT || 'demo-project';
119
+ }
120
+
121
+ module.exports = { initFirebase };