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 +59 -0
- package/README.md +10 -0
- package/TODO-MARKETING.md +53 -0
- package/package.json +1 -1
- package/src/cli/commands/auth.js +226 -0
- package/src/cli/commands/firebase-init.js +121 -0
- package/src/cli/commands/firestore.js +261 -0
- package/src/cli/commands/index.js +2 -0
- package/src/cli/index.js +16 -0
- package/src/manager/libraries/payment-processors/stripe.js +37 -0
- package/src/manager/routes/payments/cancel/post.js +66 -0
- package/src/manager/routes/payments/cancel/processors/stripe.js +22 -0
- package/src/manager/routes/payments/cancel/processors/test.js +93 -0
- package/src/manager/routes/payments/intent/processors/stripe.js +1 -30
- package/src/manager/routes/payments/portal/post.js +54 -0
- package/src/manager/routes/payments/portal/processors/stripe.js +62 -0
- package/src/manager/routes/payments/portal/processors/test.js +18 -0
- package/src/manager/routes/user/delete.js +2 -1
- package/src/manager/schemas/payments/cancel/post.js +18 -0
- package/src/manager/schemas/payments/portal/post.js +10 -0
- package/src/test/runner.js +18 -1
- package/src/test/test-accounts.js +92 -0
- package/templates/firestore.rules +1 -0
- package/test/events/payments/journey-payments-cancel-endpoint.js +79 -0
- package/test/helpers/stripe-to-unified-one-time.js +304 -0
- package/test/routes/payments/cancel.js +124 -0
- package/test/routes/payments/intent.js +7 -14
- package/test/routes/payments/portal.js +89 -0
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
|
@@ -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 };
|