@stoker-platform/cli 0.5.12

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 (94) hide show
  1. package/LICENSE.md +102 -0
  2. package/init-files/.##gitignore## +102 -0
  3. package/init-files/.devcontainer/devcontainer.json +14 -0
  4. package/init-files/.env/.env +70 -0
  5. package/init-files/.eslintrc.cjs +35 -0
  6. package/init-files/.firebaserc +6 -0
  7. package/init-files/.prettierignore +86 -0
  8. package/init-files/.prettierrc +5 -0
  9. package/init-files/bin/build.js +221 -0
  10. package/init-files/bin/shim.js +159 -0
  11. package/init-files/extensions/firestore-send-email.env +7 -0
  12. package/init-files/external.package.json +4 -0
  13. package/init-files/firebase-rules/database.rules.json +9 -0
  14. package/init-files/firebase-rules/firestore.custom.rules +19 -0
  15. package/init-files/firebase-rules/firestore.indexes.json +0 -0
  16. package/init-files/firebase-rules/firestore.rules +0 -0
  17. package/init-files/firebase-rules/storage.rules +0 -0
  18. package/init-files/firebase.hosting.json +122 -0
  19. package/init-files/firebase.json +52 -0
  20. package/init-files/functions/.##gitignore## +14 -0
  21. package/init-files/functions/.eslintrc.cjs +28 -0
  22. package/init-files/functions/package.json +46 -0
  23. package/init-files/functions/prompts/chat.prompt +17 -0
  24. package/init-files/functions/src/index.ts +457 -0
  25. package/init-files/functions/tsconfig.dev.json +3 -0
  26. package/init-files/functions/tsconfig.json +17 -0
  27. package/init-files/icons/logo-large.png +0 -0
  28. package/init-files/icons/logo-small.png +0 -0
  29. package/init-files/ops.js +25 -0
  30. package/init-files/package.json +53 -0
  31. package/init-files/project-data.json +5 -0
  32. package/init-files/remoteconfig.template.json +1 -0
  33. package/init-files/src/collections/Inbox.ts +444 -0
  34. package/init-files/src/collections/Outbox.ts +270 -0
  35. package/init-files/src/collections/Settings.ts +44 -0
  36. package/init-files/src/collections/Users.ts +138 -0
  37. package/init-files/src/main.ts +245 -0
  38. package/init-files/src/utils.ts +3 -0
  39. package/init-files/src/vite-env.d.ts +1 -0
  40. package/init-files/test/test.ts +5 -0
  41. package/init-files/tsconfig.json +23 -0
  42. package/init-files/vitest.config.ts +9 -0
  43. package/lib/package.json +45 -0
  44. package/lib/src/data/exportToBigQuery.js +41 -0
  45. package/lib/src/data/seedData.js +347 -0
  46. package/lib/src/deploy/applySchema.js +43 -0
  47. package/lib/src/deploy/cloud-functions/getFunctionsData.js +18 -0
  48. package/lib/src/deploy/deployProject.js +116 -0
  49. package/lib/src/deploy/firestore-export/exportFirestoreData.js +29 -0
  50. package/lib/src/deploy/firestore-ttl/deployTTLs.js +127 -0
  51. package/lib/src/deploy/live-update/liveUpdate.js +22 -0
  52. package/lib/src/deploy/maintenance/activateMaintenanceMode.js +9 -0
  53. package/lib/src/deploy/maintenance/disableMaintenanceMode.js +9 -0
  54. package/lib/src/deploy/maintenance/setDeploymentStatus.js +22 -0
  55. package/lib/src/deploy/rules-indexes/generateFirestoreIndexes.js +23 -0
  56. package/lib/src/deploy/rules-indexes/generateFirestoreRules.js +35 -0
  57. package/lib/src/deploy/rules-indexes/generateStorageRules.js +23 -0
  58. package/lib/src/deploy/schema/generateSchema.js +184 -0
  59. package/lib/src/deploy/schema/persistSchema.js +14 -0
  60. package/lib/src/lint/lintSchema.js +1491 -0
  61. package/lib/src/lint/securityReport.js +223 -0
  62. package/lib/src/main.js +460 -0
  63. package/lib/src/migration/firestore/migrateFirestore.js +8 -0
  64. package/lib/src/migration/firestore/operations/deleteField.js +58 -0
  65. package/lib/src/migration/migrateAll.js +30 -0
  66. package/lib/src/ops/auditDenormalized.js +124 -0
  67. package/lib/src/ops/auditPermissions.js +92 -0
  68. package/lib/src/ops/auditRelations.js +186 -0
  69. package/lib/src/ops/explainPreloadQueries.js +65 -0
  70. package/lib/src/ops/getUser.js +10 -0
  71. package/lib/src/ops/getUserPermissions.js +19 -0
  72. package/lib/src/ops/getUserRecord.js +20 -0
  73. package/lib/src/ops/listProjects.js +8 -0
  74. package/lib/src/ops/setUserCollection.js +14 -0
  75. package/lib/src/ops/setUserDocument.js +11 -0
  76. package/lib/src/ops/setUserRole.js +14 -0
  77. package/lib/src/project/addProject.js +935 -0
  78. package/lib/src/project/addRecord.js +9 -0
  79. package/lib/src/project/addRecordPrompt.js +205 -0
  80. package/lib/src/project/addTenant.js +59 -0
  81. package/lib/src/project/buildWebApp.js +10 -0
  82. package/lib/src/project/customDomain.js +157 -0
  83. package/lib/src/project/deleteProject.js +51 -0
  84. package/lib/src/project/deleteRecord.js +11 -0
  85. package/lib/src/project/deleteTenant.js +49 -0
  86. package/lib/src/project/getOne.js +25 -0
  87. package/lib/src/project/getSome.js +28 -0
  88. package/lib/src/project/initProject.js +16 -0
  89. package/lib/src/project/prepareEmulatorData.js +125 -0
  90. package/lib/src/project/setProject.js +13 -0
  91. package/lib/src/project/startEmulators.js +30 -0
  92. package/lib/src/project/updateRecord.js +9 -0
  93. package/lib/tsconfig.tsbuildinfo +1 -0
  94. package/package.json +45 -0
@@ -0,0 +1,935 @@
1
+ import { runChildProcess } from "@stoker-platform/node-client";
2
+ import { writeFile, unlink, readFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import dotenv from "dotenv";
5
+ import { retryOperation } from "@stoker-platform/utils";
6
+ import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
7
+ import { addTenant } from "./addTenant.js";
8
+ import { existsSync } from "fs";
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ export const addProject = async (options) => {
11
+ if (!process.env.GCP_BILLING_ACCOUNT) {
12
+ throw new Error("GCP_BILLING_ACCOUNT not set.");
13
+ }
14
+ if (!process.env.FB_FIRESTORE_REGION) {
15
+ throw new Error("FB_FIRESTORE_REGION not set.");
16
+ }
17
+ if (!process.env.FB_STORAGE_REGION) {
18
+ throw new Error("FB_STORAGE_REGION not set.");
19
+ }
20
+ if (!process.env.FB_DATABASE_REGION) {
21
+ throw new Error("FB_DATABASE_REGION not set.");
22
+ }
23
+ if (!process.env.MAIL_SMTP_PASSWORD) {
24
+ throw new Error("MAIL_SMTP_PASSWORD not set.");
25
+ }
26
+ if (!process.env.FB_AUTH_PASSWORD_POLICY) {
27
+ throw new Error("FB_AUTH_PASSWORD_POLICY not set.");
28
+ }
29
+ if (!process.env.FB_AUTH_PASSWORD_POLICY_UPGRADE) {
30
+ throw new Error("FB_AUTH_PASSWORD_POLICY_UPGRADE not set.");
31
+ }
32
+ if (!process.env.FB_FUNCTIONS_REGION) {
33
+ throw new Error("FB_FUNCTIONS_REGION not set.");
34
+ }
35
+ if (process.env.GCP_PROJECT) {
36
+ throw new Error("GCP_PROJECT should not be set for project creation.");
37
+ }
38
+ const projectEnvFile = join(process.cwd(), ".env", `.env.project.${options.name}`);
39
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
40
+ if (existsSync(projectEnvFile)) {
41
+ dotenv.config({ path: projectEnvFile, override: true, quiet: true });
42
+ }
43
+ if (options.development) {
44
+ dotenv.config({ path: join(process.cwd(), ".env", ".env.dev"), override: true, quiet: true });
45
+ }
46
+ const projectId = options.name;
47
+ process.env.GCP_PROJECT = projectId;
48
+ let projectData = JSON.parse(await readFile(join(process.cwd(), "project-data.json"), "utf8"));
49
+ if (Array.isArray(projectData.projects)) {
50
+ const currentProjects = {};
51
+ for (const project of projectData.projects) {
52
+ // eslint-disable-next-line security/detect-object-injection
53
+ currentProjects[project] = {
54
+ setup_progress: 1000,
55
+ };
56
+ }
57
+ projectData.projects = currentProjects;
58
+ }
59
+ // eslint-disable-next-line security/detect-object-injection
60
+ if (!projectData.projects[projectId]) {
61
+ // eslint-disable-next-line security/detect-object-injection
62
+ projectData.projects[projectId] = {
63
+ setup_progress: 0,
64
+ setup_date: new Date().toISOString(),
65
+ };
66
+ if (!options.testMode) {
67
+ await writeFile(join(process.cwd(), "project-data.json"), JSON.stringify(projectData, null, 4));
68
+ }
69
+ }
70
+ else {
71
+ // eslint-disable-next-line security/detect-object-injection
72
+ const currentProjectData = await readFile(join(process.cwd(), "project-data.json"), "utf8");
73
+ projectData = JSON.parse(currentProjectData);
74
+ // eslint-disable-next-line security/detect-object-injection
75
+ if (projectData.projects[projectId].setup_progress === 1000) {
76
+ console.log("Project already set up.");
77
+ process.exit();
78
+ }
79
+ }
80
+ const projectDataObject = projectData;
81
+ const updateProjectData = async (progress) => {
82
+ // eslint-disable-next-line security/detect-object-injection
83
+ projectDataObject.projects[projectId].setup_progress = progress;
84
+ await writeFile(join(process.cwd(), "project-data.json"), JSON.stringify(projectData, null, 4));
85
+ };
86
+ const getProgress = () => {
87
+ // eslint-disable-next-line security/detect-object-injection
88
+ return projectDataObject.projects[projectId].setup_progress;
89
+ };
90
+ const projectArgs = ["projects:create", projectId];
91
+ if (process.env.GCP_ORGANIZATION)
92
+ projectArgs.push("--organization", process.env.GCP_ORGANIZATION);
93
+ if (process.env.GCP_FOLDER)
94
+ projectArgs.push("--folder", process.env.GCP_FOLDER);
95
+ if (getProgress() < 1) {
96
+ await runChildProcess("firebase", projectArgs).catch(async () => {
97
+ // eslint-disable-next-line security/detect-object-injection
98
+ delete projectDataObject.projects[projectId];
99
+ await writeFile(join(process.cwd(), "project-data.json"), JSON.stringify(projectDataObject, null, 4));
100
+ throw new Error("Error creating Google Cloud Project.");
101
+ });
102
+ await updateProjectData(1);
103
+ }
104
+ if (getProgress() < 2) {
105
+ await runChildProcess("gcloud", [
106
+ "billing",
107
+ "projects",
108
+ "link",
109
+ projectId,
110
+ `--billing-account=${process.env.GCP_BILLING_ACCOUNT}`,
111
+ "--quiet",
112
+ ]).catch(() => {
113
+ throw new Error("Error enabling billing on project.");
114
+ });
115
+ await updateProjectData(2);
116
+ }
117
+ if (getProgress() < 3) {
118
+ await runChildProcess("gcloud", [
119
+ "services",
120
+ "enable",
121
+ "firestore.googleapis.com",
122
+ "firebasedatabase.googleapis.com",
123
+ "firebasestorage.googleapis.com",
124
+ "secretmanager.googleapis.com",
125
+ "cloudfunctions.googleapis.com",
126
+ "deploymentmanager.googleapis.com",
127
+ "artifactregistry.googleapis.com",
128
+ "containerregistry.googleapis.com",
129
+ "cloudbuild.googleapis.com",
130
+ "firebaseextensions.googleapis.com",
131
+ "eventarc.googleapis.com",
132
+ "eventarcpublishing.googleapis.com",
133
+ "run.googleapis.com",
134
+ "compute.googleapis.com",
135
+ "maps-backend.googleapis.com",
136
+ "geocoding-backend.googleapis.com",
137
+ "firebaseappcheck.googleapis.com",
138
+ "recaptchaenterprise.googleapis.com",
139
+ "cloudscheduler.googleapis.com",
140
+ `--project=${projectId}`,
141
+ "--quiet",
142
+ ]).catch(() => {
143
+ throw new Error("Error enabling Google Cloud APIs.");
144
+ });
145
+ await updateProjectData(3);
146
+ }
147
+ if (getProgress() < 4) {
148
+ await runChildProcess("gcloud", [
149
+ "services",
150
+ "enable",
151
+ "aiplatform.googleapis.com",
152
+ "monitoring.googleapis.com",
153
+ "logging.googleapis.com",
154
+ "cloudtrace.googleapis.com",
155
+ `--project=${projectId}`,
156
+ "--quiet",
157
+ ]).catch(() => {
158
+ throw new Error("Error enabling Google Cloud APIs.");
159
+ });
160
+ await updateProjectData(4);
161
+ }
162
+ if (getProgress() < 5) {
163
+ await runChildProcess("firebase", ["apps:create", "WEB", projectId, "--project", projectId]).catch(() => {
164
+ throw new Error("Error creating Firebase web app.");
165
+ });
166
+ await updateProjectData(5);
167
+ }
168
+ const webAppResult = await runChildProcess("firebase", [
169
+ "apps:sdkconfig",
170
+ "WEB",
171
+ "--project",
172
+ projectId,
173
+ "--json",
174
+ ]).catch(() => {
175
+ throw new Error("Error getting Firebase web app config.");
176
+ });
177
+ const webAppResultJson = JSON.parse(webAppResult);
178
+ const webAppConfig = webAppResultJson.result.sdkConfig;
179
+ const appId = webAppConfig.appId;
180
+ const projectNumber = webAppConfig.messagingSenderId;
181
+ const token = await runChildProcess("gcloud", ["auth", "print-access-token"]).catch(() => {
182
+ throw new Error("Error getting Google Cloud identity token.");
183
+ });
184
+ if (getProgress() < 6) {
185
+ if (process.env.FB_GOOGLE_ANALYTICS_ACCOUNT_ID && !options.development) {
186
+ const analyticsResponse = await fetch(`https://firebase.googleapis.com/v1beta1/projects/${projectId}:addGoogleAnalytics`, {
187
+ method: "POST",
188
+ headers: {
189
+ Authorization: `Bearer ${token}`,
190
+ "Content-Type": "application/json",
191
+ "X-Goog-User-Project": projectId,
192
+ },
193
+ body: JSON.stringify({
194
+ analyticsAccountId: process.env.FB_GOOGLE_ANALYTICS_ACCOUNT_ID,
195
+ }),
196
+ });
197
+ const analyticsResponseJson = await analyticsResponse.json();
198
+ console.log(analyticsResponseJson);
199
+ if (!analyticsResponse.ok) {
200
+ throw new Error("Error adding Google Analytics to Firebase project.");
201
+ }
202
+ }
203
+ await updateProjectData(6);
204
+ }
205
+ if (getProgress() < 7) {
206
+ const hostingResponse = await fetch(`https://firebasehosting.googleapis.com/v1beta1/projects/${projectId}/sites/${projectId}?updateMask=appId`, {
207
+ method: "PATCH",
208
+ headers: {
209
+ Authorization: `Bearer ${token}`,
210
+ "Content-Type": "application/json",
211
+ "X-Goog-User-Project": projectId,
212
+ },
213
+ body: JSON.stringify({
214
+ appId,
215
+ }),
216
+ });
217
+ const hostingResponseJson = await hostingResponse.json();
218
+ console.log(hostingResponseJson);
219
+ if (!hostingResponse.ok) {
220
+ throw new Error("Error updating Firebase Hosting site web app association.");
221
+ }
222
+ await updateProjectData(7);
223
+ }
224
+ if (getProgress() < 8) {
225
+ const hostingConfig = {};
226
+ if (process.env.FB_HOSTING_ENABLE_CLOUD_LOGGING === "true") {
227
+ hostingConfig.cloudLoggingEnabled = true;
228
+ }
229
+ if (process.env.FB_HOSTING_MAX_VERSIONS) {
230
+ hostingConfig.maxVersions = process.env.FB_HOSTING_MAX_VERSIONS;
231
+ }
232
+ if (Object.keys(hostingConfig).length > 0) {
233
+ const hostingConfigResponse = await fetch(`https://firebasehosting.googleapis.com/v1beta1/projects/${projectId}/sites/${projectId}/config?updateMask=cloudLoggingEnabled,maxVersions`, {
234
+ method: "PATCH",
235
+ headers: {
236
+ Authorization: `Bearer ${token}`,
237
+ "Content-Type": "application/json",
238
+ "X-Goog-User-Project": projectId,
239
+ },
240
+ body: JSON.stringify(hostingConfig),
241
+ });
242
+ const hostingConfigResponseJson = await hostingConfigResponse.json();
243
+ console.log(hostingConfigResponseJson);
244
+ if (!hostingConfigResponse.ok) {
245
+ throw new Error("Error updating Firebase Hosting site web app config.");
246
+ }
247
+ }
248
+ await updateProjectData(8);
249
+ }
250
+ if (getProgress() < 9) {
251
+ await new Promise((resolve) => setTimeout(resolve, 10000));
252
+ await retryOperation(async () => {
253
+ const rtdb = await fetch(`https://firebasedatabase.googleapis.com/v1beta/projects/${projectId}/locations/${process.env.FB_DATABASE_REGION}/instances?key=${webAppConfig.apiKey}`, {
254
+ method: "POST",
255
+ headers: {
256
+ Authorization: `Bearer ${token}`,
257
+ Accept: "application/json",
258
+ "Content-Type": "application/json",
259
+ "X-Goog-User-Project": projectId,
260
+ },
261
+ body: JSON.stringify({
262
+ type: "DEFAULT_DATABASE",
263
+ }),
264
+ }).catch(() => {
265
+ throw new Error("Error creating Firebase Realtime Database instance.");
266
+ });
267
+ const rtdbResponse = await rtdb.json();
268
+ console.log(rtdbResponse);
269
+ if (!rtdb.ok) {
270
+ throw new Error("Error creating Firebase Realtime Database instance.");
271
+ }
272
+ else {
273
+ webAppConfig.databaseURL = rtdbResponse.databaseUrl;
274
+ }
275
+ }, [], undefined, 5000);
276
+ await updateProjectData(9);
277
+ }
278
+ if (getProgress() < 10) {
279
+ await new Promise((resolve) => setTimeout(resolve, 10000));
280
+ await retryOperation(async () => {
281
+ await runChildProcess("gcloud", [
282
+ "firestore",
283
+ "databases",
284
+ "create",
285
+ `--location=${process.env.FB_FIRESTORE_REGION}`,
286
+ "--type=firestore-native",
287
+ "--delete-protection",
288
+ options.pitr && process.env.FB_FIRESTORE_ENABLE_PITR !== "false"
289
+ ? "--enable-pitr"
290
+ : "--no-enable-pitr",
291
+ `--project=${projectId}`,
292
+ "--quiet",
293
+ ]).catch(() => {
294
+ throw new Error("Error creating Firestore database.");
295
+ });
296
+ }, [], undefined, 5000);
297
+ await updateProjectData(10);
298
+ }
299
+ if (getProgress() < 11) {
300
+ await new Promise((resolve) => setTimeout(resolve, 10000));
301
+ const backupArguments = [
302
+ "firestore",
303
+ "backups",
304
+ "schedules",
305
+ "create",
306
+ options.backupRecurrence
307
+ ? `--recurrence=${options.backupRecurrence}`
308
+ : process.env.FB_FIRESTORE_BACKUP_RECURRENCE
309
+ ? `--recurrence=${process.env.FB_FIRESTORE_BACKUP_RECURRENCE}`
310
+ : "--recurrence=daily",
311
+ options.backupRetention
312
+ ? `--retention=${options.backupRetention}`
313
+ : process.env.FB_FIRESTORE_BACKUP_RETENTION
314
+ ? `--retention=${process.env.FB_FIRESTORE_BACKUP_RETENTION}`
315
+ : "--retention=7d",
316
+ "--database=(default)",
317
+ `--project=${projectId}`,
318
+ "--quiet",
319
+ ];
320
+ if (process.env.FB_FIRESTORE_BACKUP_RECURRENCE === "weekly" || options.backupRecurrence === "weekly")
321
+ backupArguments.push("--day-of-week=SUN");
322
+ await retryOperation(async () => {
323
+ await runChildProcess("gcloud", backupArguments).catch(() => {
324
+ throw new Error("Error creating Firestore database.");
325
+ });
326
+ }, [], undefined, 5000);
327
+ await updateProjectData(11);
328
+ }
329
+ if (getProgress() < 12) {
330
+ await runChildProcess("gcloud", [
331
+ "storage",
332
+ "buckets",
333
+ "create",
334
+ `gs://${projectId}`,
335
+ "--default-storage-class=standard",
336
+ `--location=${process.env.FB_STORAGE_REGION}`,
337
+ `--project=${projectId}`,
338
+ "--public-access-prevention",
339
+ options.softDelete
340
+ ? `--soft-delete-duration=${options.softDelete}`
341
+ : process.env.FB_STORAGE_SOFT_DELETE_DURATION
342
+ ? `--soft-delete-duration=${process.env.FB_STORAGE_SOFT_DELETE_DURATION}`
343
+ : "--soft-delete-duration=30d",
344
+ "--quiet",
345
+ ]).catch(() => {
346
+ throw new Error("Error creating Cloud Storage Bucket.");
347
+ });
348
+ await updateProjectData(12);
349
+ }
350
+ if (getProgress() < 13) {
351
+ if (options.versioning && process.env.FB_STORAGE_ENABLE_VERSIONING !== "false") {
352
+ await runChildProcess("gcloud", [
353
+ "storage",
354
+ "buckets",
355
+ "update",
356
+ `gs://${projectId}`,
357
+ "--versioning",
358
+ `--project=${projectId}`,
359
+ "--quiet",
360
+ ]).catch(() => {
361
+ throw new Error("Error enabling Cloud Storage versioning.");
362
+ });
363
+ }
364
+ await updateProjectData(13);
365
+ }
366
+ if (getProgress() < 14) {
367
+ await writeFile("cors.json", process.env.FB_STORAGE_CORS
368
+ ? process.env.FB_STORAGE_CORS
369
+ : JSON.stringify([
370
+ {
371
+ origin: [`https://${projectId}.web.app`, `https://${projectId}.firebaseapp.com`],
372
+ method: ["GET"],
373
+ maxAgeSeconds: 3600,
374
+ },
375
+ ]));
376
+ await runChildProcess("gcloud", [
377
+ "storage",
378
+ "buckets",
379
+ "update",
380
+ `gs://${projectId}`,
381
+ "--cors-file",
382
+ options.customCors || "cors.json",
383
+ `--project=${projectId}`,
384
+ "--quiet",
385
+ ]).catch(() => {
386
+ throw new Error("Error enabling Cloud Storage CORS.");
387
+ });
388
+ await unlink(join(process.cwd(), "cors.json"));
389
+ await updateProjectData(14);
390
+ }
391
+ if (getProgress() < 15) {
392
+ await runChildProcess("gcloud", [
393
+ "storage",
394
+ "buckets",
395
+ "create",
396
+ `gs://${projectId}-export`,
397
+ "--default-storage-class=standard",
398
+ `--location=${process.env.FB_STORAGE_REGION}`,
399
+ `--project=${projectId}`,
400
+ "--public-access-prevention",
401
+ options.softDelete
402
+ ? `--soft-delete-duration=${options.softDelete}`
403
+ : process.env.FB_STORAGE_SOFT_DELETE_DURATION
404
+ ? `--soft-delete-duration=${process.env.FB_STORAGE_SOFT_DELETE_DURATION}`
405
+ : "--soft-delete-duration=30d",
406
+ "--quiet",
407
+ ]).catch(() => {
408
+ throw new Error("Error creating Cloud Storage export Bucket.");
409
+ });
410
+ await updateProjectData(15);
411
+ }
412
+ if (getProgress() < 16) {
413
+ const storageResponse = await fetch(`https://firebasestorage.googleapis.com/v1beta/projects/${projectId}/buckets/${projectId}:addFirebase`, {
414
+ method: "POST",
415
+ headers: {
416
+ Authorization: `Bearer ${token}`,
417
+ "Content-Type": "application/json",
418
+ "X-Goog-User-Project": projectId,
419
+ },
420
+ body: "{}",
421
+ });
422
+ const storageResponseJson = await storageResponse.json();
423
+ console.log(storageResponseJson);
424
+ if (!storageResponse.ok) {
425
+ throw new Error("Error adding Firebase to Cloud Storage.");
426
+ }
427
+ await updateProjectData(16);
428
+ }
429
+ if (getProgress() < 17) {
430
+ if (process.platform === "win32") {
431
+ const firebasercPath = join(process.cwd(), ".firebaserc");
432
+ await runChildProcess("attrib", ["-H", firebasercPath]);
433
+ }
434
+ await runChildProcess("firebase", ["target:apply", "storage", "default", projectId, "--project", projectId]);
435
+ await updateProjectData(17);
436
+ }
437
+ if (getProgress() < 18) {
438
+ await retryOperation(async () => {
439
+ await runChildProcess("gcloud", [
440
+ "projects",
441
+ "add-iam-policy-binding",
442
+ projectId,
443
+ "--member",
444
+ `serviceAccount:service-${projectNumber}@gcp-sa-firebasestorage.iam.gserviceaccount.com`,
445
+ "--role",
446
+ "roles/firebaserules.firestoreServiceAgent",
447
+ "--quiet",
448
+ ]).catch(() => {
449
+ throw new Error("Error attaching Firebase Storage Rules permissions.");
450
+ });
451
+ }, [], undefined, 5000);
452
+ await updateProjectData(18);
453
+ }
454
+ if (getProgress() < 19) {
455
+ await retryOperation(async () => {
456
+ await runChildProcess("gcloud", [
457
+ "projects",
458
+ "add-iam-policy-binding",
459
+ projectId,
460
+ "--member",
461
+ `serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`,
462
+ "--role",
463
+ "roles/monitoring.metricWriter",
464
+ "--quiet",
465
+ ]).catch(() => {
466
+ throw new Error("Error attaching GenKit permissions.");
467
+ });
468
+ }, [], undefined, 5000);
469
+ await updateProjectData(19);
470
+ }
471
+ if (getProgress() < 20) {
472
+ await retryOperation(async () => {
473
+ await runChildProcess("gcloud", [
474
+ "projects",
475
+ "add-iam-policy-binding",
476
+ projectId,
477
+ "--member",
478
+ `serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`,
479
+ "--role",
480
+ "roles/cloudtrace.agent",
481
+ "--quiet",
482
+ ]).catch(() => {
483
+ throw new Error("Error attaching GenKit permissions.");
484
+ });
485
+ }, [], undefined, 5000);
486
+ await updateProjectData(20);
487
+ }
488
+ if (getProgress() < 21) {
489
+ await retryOperation(async () => {
490
+ await runChildProcess("gcloud", [
491
+ "projects",
492
+ "add-iam-policy-binding",
493
+ projectId,
494
+ "--member",
495
+ `serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`,
496
+ "--role",
497
+ "roles/logging.logWriter",
498
+ "--quiet",
499
+ ]).catch(() => {
500
+ throw new Error("Error attaching GenKit permissions.");
501
+ });
502
+ }, [], undefined, 5000);
503
+ await updateProjectData(21);
504
+ }
505
+ if (getProgress() < 22) {
506
+ await retryOperation(async () => {
507
+ await runChildProcess("gcloud", [
508
+ "projects",
509
+ "add-iam-policy-binding",
510
+ projectId,
511
+ "--member",
512
+ `serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`,
513
+ "--role",
514
+ "roles/iam.serviceAccountTokenCreator",
515
+ "--quiet",
516
+ ]).catch(() => {
517
+ throw new Error("Error attaching GenKit permissions.");
518
+ });
519
+ }, [], undefined, 5000);
520
+ await updateProjectData(22);
521
+ }
522
+ webAppConfig.storageBucket = `gs://${projectId}`;
523
+ if (getProgress() < 23) {
524
+ const identity = await fetch(`https://identitytoolkit.googleapis.com/v2/projects/${projectId}/identityPlatform:initializeAuth`, {
525
+ method: "POST",
526
+ headers: {
527
+ Authorization: `Bearer ${token}`,
528
+ Accept: "application/json",
529
+ "Content-Type": "application/json",
530
+ "X-Goog-User-Project": projectId,
531
+ },
532
+ });
533
+ const identityResponse = await identity.json();
534
+ console.log(identityResponse);
535
+ if (!identity.ok) {
536
+ throw new Error("Error setting up Firebase Auth.");
537
+ }
538
+ await updateProjectData(23);
539
+ }
540
+ if (getProgress() < 24) {
541
+ const auth = await fetch(`https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config`, {
542
+ method: "PATCH",
543
+ headers: {
544
+ Authorization: `Bearer ${token}`,
545
+ Accept: "application/json",
546
+ "Content-Type": "application/json",
547
+ "X-Goog-User-Project": projectId,
548
+ },
549
+ body: JSON.stringify({
550
+ signIn: {
551
+ email: {
552
+ enabled: true,
553
+ passwordRequired: true,
554
+ },
555
+ allowDuplicateEmails: false,
556
+ },
557
+ authorizedDomains: [`${projectId}.web.app`, `${projectId}.firebaseapp.com`],
558
+ client: {
559
+ permissions: {
560
+ disabledUserSignup: true,
561
+ disabledUserDeletion: true,
562
+ },
563
+ },
564
+ monitoring: {
565
+ requestLogging: {
566
+ enabled: true,
567
+ },
568
+ },
569
+ mfa: {
570
+ providerConfigs: [
571
+ {
572
+ state: "ENABLED",
573
+ totpProviderConfig: {
574
+ adjacentIntervals: 5,
575
+ },
576
+ },
577
+ ],
578
+ },
579
+ emailPrivacyConfig: {
580
+ enableImprovedEmailPrivacy: true,
581
+ },
582
+ passwordPolicyConfig: {
583
+ passwordPolicyEnforcementState: "ENFORCE",
584
+ forceUpgradeOnSignin: JSON.parse(process.env.FB_AUTH_PASSWORD_POLICY_UPGRADE),
585
+ passwordPolicyVersions: [
586
+ {
587
+ customStrengthOptions: JSON.parse(process.env.FB_AUTH_PASSWORD_POLICY),
588
+ },
589
+ ],
590
+ },
591
+ autodeleteAnonymousUsers: true,
592
+ }),
593
+ });
594
+ const authResponse = await auth.json();
595
+ console.log(authResponse);
596
+ if (!auth.ok) {
597
+ throw new Error("Error setting up Firebase Auth.");
598
+ }
599
+ await updateProjectData(24);
600
+ }
601
+ const secretManager = new SecretManagerServiceClient();
602
+ if (getProgress() < 25) {
603
+ if (process.env.ALGOLIA_ADMIN_KEY) {
604
+ const [algoliaSecret] = await secretManager.createSecret({
605
+ parent: `projects/${projectId}`,
606
+ secret: {
607
+ name: "ALGOLIA_ADMIN_KEY",
608
+ replication: {
609
+ automatic: {},
610
+ },
611
+ },
612
+ secretId: "ALGOLIA_ADMIN_KEY",
613
+ });
614
+ const [algoliaSecretVersion] = await secretManager.addSecretVersion({
615
+ parent: algoliaSecret.name,
616
+ payload: {
617
+ data: Buffer.from(process.env.ALGOLIA_ADMIN_KEY, "utf8"),
618
+ },
619
+ });
620
+ console.log(algoliaSecretVersion);
621
+ }
622
+ await updateProjectData(25);
623
+ }
624
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
625
+ let smtpPasswordSecret;
626
+ if (getProgress() < 26) {
627
+ ;
628
+ [smtpPasswordSecret] = await secretManager.createSecret({
629
+ parent: `projects/${projectId}`,
630
+ secret: {
631
+ name: "firestore-send-email-SMTP_PASSWORD",
632
+ replication: {
633
+ automatic: {},
634
+ },
635
+ },
636
+ secretId: "firestore-send-email-SMTP_PASSWORD",
637
+ });
638
+ await updateProjectData(26);
639
+ }
640
+ if (getProgress() < 27) {
641
+ const [smtpPasswordSecretVersion] = await secretManager.addSecretVersion({
642
+ parent: smtpPasswordSecret.name,
643
+ payload: {
644
+ data: Buffer.from(process.env.MAIL_SMTP_PASSWORD, "utf8"),
645
+ },
646
+ });
647
+ console.log(smtpPasswordSecretVersion);
648
+ await updateProjectData(27);
649
+ }
650
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
651
+ let twilioPasswordSecret;
652
+ if (getProgress() < 28) {
653
+ if (process.env.TWILIO_AUTH_TOKEN) {
654
+ ;
655
+ [twilioPasswordSecret] = await secretManager.createSecret({
656
+ parent: `projects/${projectId}`,
657
+ secret: {
658
+ name: "TWILIO_AUTH_TOKEN",
659
+ replication: {
660
+ automatic: {},
661
+ },
662
+ },
663
+ secretId: "TWILIO_AUTH_TOKEN",
664
+ });
665
+ }
666
+ await updateProjectData(28);
667
+ }
668
+ if (getProgress() < 29) {
669
+ if (process.env.TWILIO_AUTH_TOKEN) {
670
+ const [twilioPasswordSecretVersion] = await secretManager.addSecretVersion({
671
+ parent: twilioPasswordSecret.name,
672
+ payload: {
673
+ data: Buffer.from(process.env.TWILIO_AUTH_TOKEN, "utf8"),
674
+ },
675
+ });
676
+ console.log(twilioPasswordSecretVersion);
677
+ }
678
+ await updateProjectData(29);
679
+ }
680
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
681
+ let twilioAccountSidSecret;
682
+ if (getProgress() < 30) {
683
+ if (process.env.TWILIO_ACCOUNT_SID) {
684
+ ;
685
+ [twilioAccountSidSecret] = await secretManager.createSecret({
686
+ parent: `projects/${projectId}`,
687
+ secret: {
688
+ name: "TWILIO_ACCOUNT_SID",
689
+ replication: {
690
+ automatic: {},
691
+ },
692
+ },
693
+ secretId: "TWILIO_ACCOUNT_SID",
694
+ });
695
+ }
696
+ await updateProjectData(30);
697
+ }
698
+ if (getProgress() < 31) {
699
+ if (process.env.TWILIO_ACCOUNT_SID) {
700
+ const [twilioAccountSidSecretVersion] = await secretManager.addSecretVersion({
701
+ parent: twilioAccountSidSecret.name,
702
+ payload: {
703
+ data: Buffer.from(process.env.TWILIO_ACCOUNT_SID, "utf8"),
704
+ },
705
+ });
706
+ console.log(twilioAccountSidSecretVersion);
707
+ }
708
+ await updateProjectData(31);
709
+ }
710
+ if (getProgress() < 32) {
711
+ if (process.env.TWILIO_PHONE_NUMBER) {
712
+ const [twilioPhoneNumberSecret] = await secretManager.createSecret({
713
+ parent: `projects/${projectId}`,
714
+ secret: {
715
+ name: "TWILIO_PHONE_NUMBER",
716
+ replication: {
717
+ automatic: {},
718
+ },
719
+ },
720
+ secretId: "TWILIO_PHONE_NUMBER",
721
+ });
722
+ const [twilioPhoneNumberSecretVersion] = await secretManager.addSecretVersion({
723
+ parent: twilioPhoneNumberSecret.name,
724
+ payload: {
725
+ data: Buffer.from(process.env.TWILIO_PHONE_NUMBER, "utf8"),
726
+ },
727
+ });
728
+ console.log(twilioPhoneNumberSecretVersion);
729
+ }
730
+ await updateProjectData(32);
731
+ }
732
+ if (getProgress() < 33) {
733
+ const externalSecrets = JSON.parse(process.env.EXTERNAL_SECRETS || "{}");
734
+ for (const [secretName, secretValue] of Object.entries(externalSecrets)) {
735
+ const [externalSecret] = await secretManager.createSecret({
736
+ parent: `projects/${projectId}`,
737
+ secret: {
738
+ name: secretName,
739
+ replication: {
740
+ automatic: {},
741
+ },
742
+ },
743
+ secretId: secretName,
744
+ });
745
+ const [secretVersion] = await secretManager.addSecretVersion({
746
+ parent: externalSecret.name,
747
+ payload: {
748
+ data: Buffer.from(secretValue, "utf8"),
749
+ },
750
+ });
751
+ console.log(secretVersion);
752
+ }
753
+ await updateProjectData(33);
754
+ }
755
+ if (getProgress() < 34) {
756
+ const apiKeys = await runChildProcess("gcloud", [
757
+ "services",
758
+ "api-keys",
759
+ "list",
760
+ `--project=${projectId}`,
761
+ "--quiet",
762
+ "--format=json",
763
+ ]);
764
+ const apiKeysJson = JSON.parse(apiKeys);
765
+ const apiKey = apiKeysJson[0];
766
+ if (!apiKey) {
767
+ throw new Error("Error getting Firebase API key.");
768
+ }
769
+ let allowedReferrers = `--allowed-referrers=https://${projectId}.web.app,https://${projectId}.firebaseapp.com`;
770
+ if (options.development) {
771
+ allowedReferrers = `--allowed-referrers=https://${projectId}.web.app,https://${projectId}.firebaseapp.com,http://localhost`;
772
+ }
773
+ const apiKeyUpdateArgs = [
774
+ "services",
775
+ "api-keys",
776
+ "update",
777
+ apiKey.uid,
778
+ "--api-target=service=maps-backend.googleapis.com",
779
+ "--api-target=service=geocoding-backend.googleapis.com",
780
+ allowedReferrers,
781
+ `--project=${projectId}`,
782
+ "--quiet",
783
+ ];
784
+ apiKey.restrictions.apiTargets.forEach((target) => apiKeyUpdateArgs.push(`--api-target=service=${target.service}`));
785
+ await runChildProcess("gcloud", apiKeyUpdateArgs);
786
+ await updateProjectData(34);
787
+ }
788
+ const recaptchaResponse = await fetch(`https://recaptchaenterprise.googleapis.com/v1/projects/${projectId}/keys`, {
789
+ method: "POST",
790
+ headers: {
791
+ Authorization: `Bearer ${token}`,
792
+ "Content-Type": "application/json",
793
+ "X-Goog-User-Project": projectId,
794
+ },
795
+ body: JSON.stringify({
796
+ displayName: "Firebase App Check",
797
+ webSettings: {
798
+ allowedDomains: [`${projectId}.web.app`, `${projectId}.firebaseapp.com`],
799
+ integrationType: "SCORE",
800
+ },
801
+ }),
802
+ });
803
+ const recaptchaResponseJson = await recaptchaResponse.json();
804
+ console.log(recaptchaResponseJson);
805
+ if (!recaptchaResponse.ok) {
806
+ throw new Error("Failed to create Recaptcha key");
807
+ }
808
+ const recaptchaKey = recaptchaResponseJson.name;
809
+ const recaptchaKeyId = recaptchaKey.split("/").pop();
810
+ if (getProgress() < 35) {
811
+ const recaptchaEnterpriseConfig = await fetch(`https://firebaseappcheck.googleapis.com/v1beta/projects/${projectId}/apps/${appId}/recaptchaEnterpriseConfig?updateMask=siteKey,tokenTtl,riskAnalysis`, {
812
+ method: "PATCH",
813
+ headers: {
814
+ Authorization: `Bearer ${token}`,
815
+ "Content-Type": "application/json",
816
+ "X-Goog-User-Project": projectId,
817
+ },
818
+ body: JSON.stringify({
819
+ siteKey: recaptchaKeyId,
820
+ tokenTtl: process.env.FB_APP_CHECK_TOKEN_TTL || "3600s",
821
+ riskAnalysis: {
822
+ minValidScore: process.env.FB_APP_CHECK_MIN_VALID_SCORE
823
+ ? parseFloat(process.env.FB_APP_CHECK_MIN_VALID_SCORE)
824
+ : 0.5,
825
+ },
826
+ }),
827
+ });
828
+ const recaptchaEnterpriseConfigJson = await recaptchaEnterpriseConfig.json();
829
+ console.log(recaptchaEnterpriseConfigJson);
830
+ if (!recaptchaEnterpriseConfig.ok) {
831
+ throw new Error("Failed to create Recaptcha Enterprise config");
832
+ }
833
+ await updateProjectData(35);
834
+ }
835
+ if (getProgress() < 36) {
836
+ if (process.env.FB_ENABLE_APP_CHECK === "true") {
837
+ const servicesResponse = await fetch(`https://firebaseappcheck.googleapis.com/v1beta/projects/${projectId}/services:batchUpdate`, {
838
+ method: "POST",
839
+ headers: {
840
+ Authorization: `Bearer ${token}`,
841
+ "Content-Type": "application/json",
842
+ "X-Goog-User-Project": projectId,
843
+ },
844
+ body: JSON.stringify({
845
+ requests: [
846
+ {
847
+ service: {
848
+ name: `projects/${projectId}/services/identitytoolkit.googleapis.com`,
849
+ enforcementMode: "ENFORCED",
850
+ },
851
+ updateMask: "enforcementMode",
852
+ },
853
+ {
854
+ service: {
855
+ name: `projects/${projectId}/services/firestore.googleapis.com`,
856
+ enforcementMode: "ENFORCED",
857
+ },
858
+ updateMask: "enforcementMode",
859
+ },
860
+ {
861
+ service: {
862
+ name: `projects/${projectId}/services/firebasedatabase.googleapis.com`,
863
+ enforcementMode: "ENFORCED",
864
+ },
865
+ updateMask: "enforcementMode",
866
+ },
867
+ {
868
+ service: {
869
+ name: `projects/${projectId}/services/firebasestorage.googleapis.com`,
870
+ enforcementMode: "ENFORCED",
871
+ },
872
+ updateMask: "enforcementMode",
873
+ },
874
+ {
875
+ service: {
876
+ name: `projects/${projectId}/services/maps-backend.googleapis.com`,
877
+ enforcementMode: "ENFORCED",
878
+ },
879
+ updateMask: "enforcementMode",
880
+ },
881
+ ],
882
+ }),
883
+ });
884
+ const servicesResponseJson = await servicesResponse.json();
885
+ console.log(servicesResponseJson);
886
+ if (!servicesResponse.ok) {
887
+ throw new Error("Failed to update App Check services");
888
+ }
889
+ }
890
+ await updateProjectData(36);
891
+ }
892
+ const firebaseJson = JSON.parse(await readFile(join(process.cwd(), "firebase.json"), "utf8"));
893
+ const authPort = firebaseJson.emulators.auth.port;
894
+ const databasePort = firebaseJson.emulators.database.port;
895
+ const firestorePort = firebaseJson.emulators.firestore.port;
896
+ const storagePort = firebaseJson.emulators.storage.port;
897
+ const functionsPort = firebaseJson.emulators.functions.port;
898
+ const envDir = join(process.cwd(), ".env");
899
+ const envFile = join(envDir, `.env.${projectId}`);
900
+ let envContent = `STOKER_FB_WEB_APP_CONFIG='${JSON.stringify(webAppConfig)}'
901
+ STOKER_FB_ENABLE_APP_CHECK=${process.env.FB_ENABLE_APP_CHECK}
902
+ STOKER_FB_APP_CHECK_KEY="${recaptchaKeyId}"
903
+ STOKER_ALGOLIA_ID="${process.env.ALGOLIA_ID || ""}"
904
+ STOKER_FB_FUNCTIONS_REGION="${process.env.FB_FUNCTIONS_REGION}"
905
+ FB_DATABASE="${projectId}-default-rtdb"
906
+ FB_FIRESTORE_EXPORT_BUCKET="${projectId}-export"`;
907
+ if (process.env.SENTRY_DSN) {
908
+ envContent += `\nSTOKER_SENTRY_DSN="${process.env.SENTRY_DSN}"`;
909
+ }
910
+ if (process.env.TWILIO_ACCOUNT_SID && process.env.TWILIO_AUTH_TOKEN && process.env.TWILIO_PHONE_NUMBER) {
911
+ envContent += `\nSTOKER_SMS_ENABLED=true`;
912
+ }
913
+ if (process.env.FULLCALENDAR_KEY) {
914
+ envContent += `\nSTOKER_FULLCALENDAR_KEY="${process.env.FULLCALENDAR_KEY}"`;
915
+ }
916
+ if (options.development) {
917
+ envContent += `
918
+ STOKER_FB_EMULATOR_AUTH_PORT=${authPort}
919
+ STOKER_FB_EMULATOR_DATABASE_PORT=${databasePort}
920
+ STOKER_FB_EMULATOR_FIRESTORE_PORT=${firestorePort}
921
+ STOKER_FB_EMULATOR_STORAGE_PORT=${storagePort}
922
+ STOKER_FB_EMULATOR_FUNCTIONS_PORT=${functionsPort}`;
923
+ }
924
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
925
+ await writeFile(envFile, envContent);
926
+ dotenv.config({ path: join(process.cwd(), ".env", `.env.${projectId}`), quiet: true });
927
+ if (getProgress() < 37) {
928
+ await runChildProcess("npx", ["stoker", "deploy", "--initial", "--retry"]);
929
+ await updateProjectData(37);
930
+ }
931
+ await runChildProcess("npx", ["stoker", "set-project"]);
932
+ await addTenant();
933
+ await updateProjectData(1000);
934
+ process.exit();
935
+ };