@idevconn/create-icore 0.5.0 → 0.5.2
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/dist/cli.js +84 -25
- package/dist/index.cjs +81 -24
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +80 -24
- package/package.json +3 -1
- package/templates/apps/api/.env.example +14 -0
- package/templates/apps/microservices/auth/package.json +1 -1
- package/templates/apps/microservices/auth/src/app/app.module.ts +17 -30
- package/templates/apps/microservices/auth/src/main.ts +6 -23
- package/templates/apps/microservices/jobs/src/app/redis-connection.ts +35 -0
- package/templates/apps/microservices/jobs/src/app/workers/cleanup.worker.ts +2 -1
- package/templates/apps/microservices/jobs/src/app/workers/email.worker.ts +2 -1
- package/templates/apps/microservices/jobs/src/app/workers/image-process.worker.ts +2 -1
- package/templates/apps/microservices/notes/src/app/app.module.ts +22 -27
- package/templates/apps/microservices/notes/src/main.ts +6 -23
- package/templates/apps/microservices/payment/src/app/app.module.ts +6 -4
- package/templates/apps/microservices/payment/src/main.ts +6 -23
- package/templates/apps/microservices/upload/package.json +1 -1
- package/templates/apps/microservices/upload/src/app/app.module.ts +18 -30
- package/templates/apps/microservices/upload/src/main.ts +6 -23
- package/templates/libs/auth-strategies/firebase/src/lib/__tests__/firebase-auth.contract.unit.test.ts +1 -1
- package/templates/libs/auth-strategies/supabase/src/lib/__tests__/supabase-auth.contract.unit.test.ts +1 -1
- package/templates/libs/db-strategies/firestore/src/lib/__tests__/firestore-db.contract.unit.test.ts +1 -1
- package/templates/libs/db-strategies/supabase/src/lib/__tests__/supabase-db.contract.unit.test.ts +1 -1
- package/templates/libs/firebase-admin/README.md +11 -0
- package/templates/libs/firebase-admin/eslint.config.mjs +24 -0
- package/templates/libs/firebase-admin/package.json +12 -0
- package/templates/libs/firebase-admin/project.json +19 -0
- package/templates/libs/firebase-admin/src/index.ts +1 -0
- package/templates/libs/firebase-admin/src/lib/__tests__/firebase-admin.unit.test.ts +105 -0
- package/templates/libs/firebase-admin/src/lib/firebase-admin.ts +70 -0
- package/templates/libs/firebase-admin/tsconfig.json +24 -0
- package/templates/libs/firebase-admin/tsconfig.lib.json +23 -0
- package/templates/libs/firebase-admin/tsconfig.spec.json +22 -0
- package/templates/libs/firebase-admin/vitest.config.mts +21 -0
- package/templates/libs/jobs-client/src/lib/jobs-client.service.ts +14 -2
- package/templates/libs/jobs-client/tsconfig.json +2 -1
- package/templates/libs/notes-client/tsconfig.json +2 -1
- package/templates/libs/payment-client/tsconfig.json +2 -1
- package/templates/libs/shared/src/__tests__/bootstrap.unit.test.ts +92 -0
- package/templates/libs/shared/src/__tests__/transport.unit.test.ts +14 -2
- package/templates/libs/shared/src/bootstrap.ts +79 -0
- package/templates/libs/shared/src/index.ts +1 -0
- package/templates/libs/shared/src/strategies/__tests__/fake-auth.contract.unit.test.ts +1 -1
- package/templates/libs/shared/src/strategies/__tests__/fake-db.contract.unit.test.ts +1 -1
- package/templates/libs/shared/src/strategies/__tests__/fake-storage.contract.unit.test.ts +1 -1
- package/templates/libs/shared/src/strategies/index.ts +3 -3
- package/templates/libs/shared/src/testing.ts +14 -0
- package/templates/libs/shared/src/transport.ts +25 -3
- package/templates/libs/shared/tsconfig.lib.json +3 -1
- package/templates/libs/shared/vitest.config.mts +11 -1
- package/templates/libs/storage-strategies/cloudinary/src/lib/__tests__/cloudinary-storage.contract.unit.test.ts +1 -1
- package/templates/libs/storage-strategies/firebase/src/lib/__tests__/firebase-storage.contract.unit.test.ts +1 -1
- package/templates/libs/storage-strategies/supabase/src/lib/__tests__/supabase-storage.contract.unit.test.ts +1 -1
- package/templates/tsconfig.base.json +3 -1
- /package/templates/libs/shared/src/strategies/{contract/auth-contract.ts → __tests__/auth.contract.unit.test.ts} +0 -0
- /package/templates/libs/shared/src/strategies/{contract/db-contract.ts → __tests__/db.contract.unit.test.ts} +0 -0
- /package/templates/libs/shared/src/strategies/{contract/storage-contract.ts → __tests__/storage.contract.unit.test.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/lib/options.ts
|
|
2
|
+
function pmRun(pm, script) {
|
|
3
|
+
return pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
// src/lib/scaffold.ts
|
|
2
7
|
import { copyFile, mkdir, readdir, readFile, stat, writeFile, rm } from "fs/promises";
|
|
3
8
|
import { readFileSync } from "fs";
|
|
@@ -35,6 +40,10 @@ async function rewriteRootPackageJson(targetDir, opts) {
|
|
|
35
40
|
pkg["version"] = "0.0.1";
|
|
36
41
|
pkg["private"] = true;
|
|
37
42
|
delete pkg.description;
|
|
43
|
+
if (opts.transport === "nats") {
|
|
44
|
+
const deps = pkg["dependencies"] ??= {};
|
|
45
|
+
deps["nats"] = "^2.29.3";
|
|
46
|
+
}
|
|
38
47
|
if (opts.packageManager !== "yarn") {
|
|
39
48
|
delete pkg.packageManager;
|
|
40
49
|
}
|
|
@@ -91,9 +100,9 @@ async function writeNotesEnv(targetDir, opts) {
|
|
|
91
100
|
async function writeGatewayEnv(targetDir, opts) {
|
|
92
101
|
const envExample = join(targetDir, "apps/api/.env.example");
|
|
93
102
|
const env = await readFile(envExample, "utf8");
|
|
94
|
-
let next = env.replace(/^AUTH_TRANSPORT=.*$/m, `AUTH_TRANSPORT=${opts.transport}`).replace(/^UPLOAD_TRANSPORT=.*$/m, `UPLOAD_TRANSPORT=${opts.transport}`);
|
|
103
|
+
let next = env.replace(/^AUTH_TRANSPORT=.*$/m, `AUTH_TRANSPORT=${opts.transport}`).replace(/^UPLOAD_TRANSPORT=.*$/m, `UPLOAD_TRANSPORT=${opts.transport}`).replace(/^NOTES_TRANSPORT=.*$/m, `NOTES_TRANSPORT=${opts.transport}`).replace(/^PAYMENT_TRANSPORT=.*$/m, `PAYMENT_TRANSPORT=${opts.transport}`);
|
|
95
104
|
if (opts.transport !== "tcp") {
|
|
96
|
-
next = next.replace(/^# (AUTH_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (UPLOAD_(?:REDIS|NATS)_URL=)/m, "$1");
|
|
105
|
+
next = next.replace(/^# (AUTH_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (UPLOAD_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (NOTES_(?:REDIS|NATS)_URL=)/m, "$1").replace(/^# (PAYMENT_(?:REDIS|NATS)_URL=)/m, "$1");
|
|
97
106
|
}
|
|
98
107
|
await writeFile(join(targetDir, "apps/api/.env"), next);
|
|
99
108
|
}
|
|
@@ -106,6 +115,17 @@ async function writeRootEnv(targetDir, opts) {
|
|
|
106
115
|
];
|
|
107
116
|
await writeFile(join(targetDir, ".env"), lines.join("\n"));
|
|
108
117
|
}
|
|
118
|
+
async function stripGatewayTransport(targetDir, prefix) {
|
|
119
|
+
const gatewayEnv = join(targetDir, "apps/api/.env");
|
|
120
|
+
try {
|
|
121
|
+
const env = await readFile(gatewayEnv, "utf8");
|
|
122
|
+
const next = env.split("\n").filter(
|
|
123
|
+
(line) => !line.startsWith(`${prefix}_`) && !line.startsWith(`# ${prefix}_`) && !line.includes(`${prefix} MS transport`)
|
|
124
|
+
).join("\n");
|
|
125
|
+
await writeFile(gatewayEnv, next);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
109
129
|
async function writeClientEnv(targetDir) {
|
|
110
130
|
const envExample = join(targetDir, "apps/client/.env.example");
|
|
111
131
|
try {
|
|
@@ -190,6 +210,7 @@ async function removePaymentStack(targetDir) {
|
|
|
190
210
|
"@icore/payment-client",
|
|
191
211
|
"@idevconn/payment"
|
|
192
212
|
]);
|
|
213
|
+
await stripGatewayTransport(targetDir, "PAYMENT");
|
|
193
214
|
}
|
|
194
215
|
async function removeNotesStack(targetDir) {
|
|
195
216
|
for (const p2 of [
|
|
@@ -211,6 +232,7 @@ async function removeNotesStack(targetDir) {
|
|
|
211
232
|
} catch {
|
|
212
233
|
}
|
|
213
234
|
await stripDeps(join(targetDir, "apps/api/package.json"), ["@icore/notes-client"]);
|
|
235
|
+
await stripGatewayTransport(targetDir, "NOTES");
|
|
214
236
|
const tsconfigPath = join(targetDir, "tsconfig.base.json");
|
|
215
237
|
try {
|
|
216
238
|
const src = await readFile(tsconfigPath, "utf8");
|
|
@@ -267,15 +289,17 @@ async function stripTsconfigPath(targetDir, alias) {
|
|
|
267
289
|
}
|
|
268
290
|
async function removeUnusedAuthStrategies(targetDir, authProvider) {
|
|
269
291
|
const modulePath = join(targetDir, "apps/microservices/auth/src/app/app.module.ts");
|
|
292
|
+
const AUTH_BRANCH = /if \(provider === 'supabase'\) return makeSupabaseAuth\(cfg\);\n\s*return makeFirebaseAuth\(cfg\);/m;
|
|
270
293
|
if (authProvider === "supabase") {
|
|
271
294
|
await rm(join(targetDir, "libs/auth-strategies/firebase"), { recursive: true, force: true });
|
|
272
295
|
await stripDeps(join(targetDir, "apps/microservices/auth/package.json"), [
|
|
273
|
-
"@icore/auth-firebase"
|
|
296
|
+
"@icore/auth-firebase",
|
|
297
|
+
"@icore/firebase-admin"
|
|
274
298
|
]);
|
|
275
299
|
await stripTsconfigPath(targetDir, "@icore/auth-firebase");
|
|
276
300
|
try {
|
|
277
301
|
const src = await readFile(modulePath, "utf8");
|
|
278
|
-
const next = src.replace(/^import
|
|
302
|
+
const next = src.replace(/^import \{[^}]*\} from '@icore\/firebase-admin';\n/m, "").replace(/^import \{[^}]*FirebaseAuthStrategy[^}]*\} from '@icore\/auth-firebase';\n/m, "").replace(/^ {2}firebase: \[[^\]]*\],\n/m, "").replace(/\nfunction makeFirebaseAuth[\s\S]*?\n}\n/m, "").replace(AUTH_BRANCH, "return makeSupabaseAuth(cfg);");
|
|
279
303
|
await writeFile(modulePath, next);
|
|
280
304
|
} catch {
|
|
281
305
|
}
|
|
@@ -288,10 +312,7 @@ async function removeUnusedAuthStrategies(targetDir, authProvider) {
|
|
|
288
312
|
await stripTsconfigPath(targetDir, "@icore/auth-supabase");
|
|
289
313
|
try {
|
|
290
314
|
const src = await readFile(modulePath, "utf8");
|
|
291
|
-
const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseAuthStrategy[^}]*\} from '@icore\/auth-supabase';\n/m, "").replace(
|
|
292
|
-
/\n {10}case 'supabase': \{[\s\S]*?return new SupabaseAuthStrategy\(\{ client \}\);\n {10}\}\n/m,
|
|
293
|
-
""
|
|
294
|
-
);
|
|
315
|
+
const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseAuthStrategy[^}]*\} from '@icore\/auth-supabase';\n/m, "").replace(/\nfunction makeSupabaseAuth[\s\S]*?\n}\n/m, "").replace(AUTH_BRANCH, "return makeFirebaseAuth(cfg);");
|
|
295
316
|
await writeFile(modulePath, next);
|
|
296
317
|
} catch {
|
|
297
318
|
}
|
|
@@ -303,7 +324,8 @@ async function removeUnusedStorageStrategies(targetDir, uploadProvider) {
|
|
|
303
324
|
if (uploadProvider !== "firebase") {
|
|
304
325
|
await rm(join(targetDir, "libs/storage-strategies/firebase"), { recursive: true, force: true });
|
|
305
326
|
await stripDeps(join(targetDir, "apps/microservices/upload/package.json"), [
|
|
306
|
-
"@icore/storage-firebase"
|
|
327
|
+
"@icore/storage-firebase",
|
|
328
|
+
"@icore/firebase-admin"
|
|
307
329
|
]);
|
|
308
330
|
await stripTsconfigPath(targetDir, "@icore/storage-firebase");
|
|
309
331
|
}
|
|
@@ -327,26 +349,26 @@ async function removeUnusedStorageStrategies(targetDir, uploadProvider) {
|
|
|
327
349
|
try {
|
|
328
350
|
let src = await readFile(modulePath, "utf8");
|
|
329
351
|
if (uploadProvider !== "firebase") {
|
|
330
|
-
src = src.replace(/^import
|
|
352
|
+
src = src.replace(/^import \{[^}]*\} from '@icore\/firebase-admin';\n/m, "").replace(
|
|
331
353
|
/^import \{[^}]*FirebaseStorageStrategy[^}]*\} from '@icore\/storage-firebase';\n/m,
|
|
332
354
|
""
|
|
333
|
-
).replace(/^
|
|
355
|
+
).replace(/^ {2}firebase: \[[^\]]*\],\n/m, "").replace(/\nfunction makeFirebaseStorage[\s\S]*?\n}\n/m, "");
|
|
334
356
|
}
|
|
335
357
|
if (uploadProvider !== "cloudinary") {
|
|
336
358
|
src = src.replace(/^import \{ v2 as cloudinary \} from 'cloudinary';\n/m, "").replace(
|
|
337
359
|
/^import \{[^}]*CloudinaryStorageStrategy[^}]*\} from '@icore\/storage-cloudinary';\n/m,
|
|
338
360
|
""
|
|
339
|
-
).replace(
|
|
361
|
+
).replace(/\nfunction makeCloudinaryStorage[\s\S]*?\n}\n/m, "");
|
|
340
362
|
}
|
|
341
363
|
if (uploadProvider !== "supabase") {
|
|
342
364
|
src = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(
|
|
343
365
|
/^import \{[^}]*SupabaseStorageStrategy[^}]*\} from '@icore\/storage-supabase';\n/m,
|
|
344
366
|
""
|
|
345
|
-
).replace(
|
|
346
|
-
/\n {10}case 'supabase': \{[\s\S]*?bucket: requireEnv\(cfg, 'SUPABASE_STORAGE_BUCKET'\),\n {12}\}\);\n {10}\}\n/m,
|
|
347
|
-
""
|
|
348
|
-
);
|
|
367
|
+
).replace(/\nfunction makeSupabaseStorage[\s\S]*?\n}\n/m, "");
|
|
349
368
|
}
|
|
369
|
+
const STORAGE_BRANCH = /if \(provider === 'supabase'\) return makeSupabaseStorage\(cfg\);\n\s*if \(provider === 'firebase'\) return makeFirebaseStorage\(cfg\);\n\s*return makeCloudinaryStorage\(cfg\);/m;
|
|
370
|
+
const chosenReturn = `return make${uploadProvider.charAt(0).toUpperCase() + uploadProvider.slice(1)}Storage(cfg);`;
|
|
371
|
+
src = src.replace(STORAGE_BRANCH, chosenReturn);
|
|
350
372
|
await writeFile(modulePath, src);
|
|
351
373
|
} catch {
|
|
352
374
|
}
|
|
@@ -356,14 +378,15 @@ async function removeUnusedDbStrategies(targetDir, dbProvider) {
|
|
|
356
378
|
if (dbProvider === "supabase") {
|
|
357
379
|
await rm(join(targetDir, "libs/db-strategies/firestore"), { recursive: true, force: true });
|
|
358
380
|
await stripDeps(join(targetDir, "apps/microservices/notes/package.json"), [
|
|
359
|
-
"@icore/db-firestore"
|
|
381
|
+
"@icore/db-firestore",
|
|
382
|
+
"@icore/firebase-admin"
|
|
360
383
|
]);
|
|
361
384
|
await stripTsconfigPath(targetDir, "@icore/db-firestore");
|
|
362
385
|
try {
|
|
363
386
|
const src = await readFile(modulePath, "utf8");
|
|
364
|
-
const next = src.replace(/^import
|
|
365
|
-
|
|
366
|
-
""
|
|
387
|
+
const next = src.replace(/^import \{[^}]*\} from '@icore\/firebase-admin';\n/m, "").replace(/^import \{[^}]*FirestoreDBStrategy[^}]*\} from '@icore\/db-firestore';\n/m, "").replace(/^ {2}firestore: \[[^\]]*\],\n/m, "").replace(/^ {2}firebase: \[[^\]]*\],\n/m, "").replace(/\nfunction makeFirestoreDB[\s\S]*?\n}\n/m, "").replace(
|
|
388
|
+
/if \(provider === 'supabase'\) return makeSupabaseDB\(cfg\);\n\s*return makeFirestoreDB\(cfg\);/m,
|
|
389
|
+
"return makeSupabaseDB(cfg);"
|
|
367
390
|
);
|
|
368
391
|
await writeFile(modulePath, next);
|
|
369
392
|
} catch {
|
|
@@ -377,15 +400,19 @@ async function removeUnusedDbStrategies(targetDir, dbProvider) {
|
|
|
377
400
|
await stripTsconfigPath(targetDir, "@icore/db-supabase");
|
|
378
401
|
try {
|
|
379
402
|
const src = await readFile(modulePath, "utf8");
|
|
380
|
-
const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(
|
|
381
|
-
|
|
382
|
-
""
|
|
403
|
+
const next = src.replace(/^import \{ createClient \} from '@supabase\/supabase-js';\n/m, "").replace(/^import \{[^}]*SupabaseDBStrategy[^}]*\} from '@icore\/db-supabase';\n/m, "").replace(/\nfunction makeSupabaseDB[\s\S]*?\n}\n/m, "").replace(
|
|
404
|
+
/if \(provider === 'supabase'\) return makeSupabaseDB\(cfg\);\n\s*return makeFirestoreDB\(cfg\);/m,
|
|
405
|
+
"return makeFirestoreDB(cfg);"
|
|
383
406
|
);
|
|
384
407
|
await writeFile(modulePath, next);
|
|
385
408
|
} catch {
|
|
386
409
|
}
|
|
387
410
|
}
|
|
388
411
|
}
|
|
412
|
+
async function removeFirebaseAdminLib(targetDir) {
|
|
413
|
+
await rm(join(targetDir, "libs/firebase-admin"), { recursive: true, force: true });
|
|
414
|
+
await stripTsconfigPath(targetDir, "@icore/firebase-admin");
|
|
415
|
+
}
|
|
389
416
|
async function removeUploadStack(targetDir) {
|
|
390
417
|
const paths = [
|
|
391
418
|
"apps/microservices/upload",
|
|
@@ -498,17 +525,45 @@ async function scaffold(opts, templatesDir) {
|
|
|
498
525
|
await removeUnusedAuthStrategies(opts.targetDir, opts.authProvider);
|
|
499
526
|
await removeUnusedStorageStrategies(opts.targetDir, opts.upload);
|
|
500
527
|
await removeUnusedDbStrategies(opts.targetDir, opts.dbProvider);
|
|
528
|
+
const firebaseUsed = opts.authProvider === "firebase" || opts.dbProvider === "firebase" || opts.upload === "firebase";
|
|
529
|
+
if (!firebaseUsed) await removeFirebaseAdminLib(opts.targetDir);
|
|
501
530
|
if (opts.packageManager === "yarn") {
|
|
502
531
|
await writeFile(join(opts.targetDir, "yarn.lock"), "");
|
|
532
|
+
} else {
|
|
533
|
+
await rm(join(opts.targetDir, ".yarn"), { recursive: true, force: true });
|
|
534
|
+
await rm(join(opts.targetDir, ".yarnrc.yml"), { force: true });
|
|
503
535
|
}
|
|
536
|
+
await patchGitignoreForPm(opts.targetDir, opts.packageManager);
|
|
504
537
|
await writeAiFiles(opts.targetDir, opts);
|
|
505
538
|
if (opts.install) runInstall(opts.targetDir, opts.packageManager);
|
|
506
539
|
if (opts.initGit) gitInit(opts.targetDir, opts.projectName);
|
|
507
540
|
}
|
|
541
|
+
async function patchGitignoreForPm(targetDir, pm) {
|
|
542
|
+
const giPath = join(targetDir, ".gitignore");
|
|
543
|
+
try {
|
|
544
|
+
let src = await readFile(giPath, "utf8");
|
|
545
|
+
src = src.replace(/^# Build artifacts.*\ntools\/create-icore\/templates\/\s*\n/m, "");
|
|
546
|
+
if (pm !== "yarn") {
|
|
547
|
+
src = src.replace(/^\.yarn\/\*\s*\n/m, "").replace(/^!\.yarn\/patches\s*\n/m, "").replace(/^!\.yarn\/plugins\s*\n/m, "").replace(/^!\.yarn\/releases\s*\n/m, "").replace(/^!\.yarn\/sdks\s*\n/m, "").replace(/^!\.yarn\/versions\s*\n/m, "").replace(/^\.pnp\.\*\s*\n/m, "");
|
|
548
|
+
}
|
|
549
|
+
if (pm === "pnpm") {
|
|
550
|
+
if (!src.includes(".pnpm-debug.log")) {
|
|
551
|
+
src += "\n# pnpm\n.pnpm-debug.log*\n";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (pm === "npm") {
|
|
555
|
+
if (!src.includes("npm-debug.log")) {
|
|
556
|
+
src += "\n# npm\nnpm-debug.log*\n";
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
await writeFile(giPath, src);
|
|
560
|
+
} catch {
|
|
561
|
+
}
|
|
562
|
+
}
|
|
508
563
|
async function writeAiFiles(targetDir, opts) {
|
|
509
564
|
const pm = opts.packageManager;
|
|
510
565
|
const nx = pm === "npm" ? "npx nx" : `${pm} nx`;
|
|
511
|
-
const devCmd =
|
|
566
|
+
const devCmd = pmRun(pm, "dev");
|
|
512
567
|
const activeMSes = ["auth (port 4001)"];
|
|
513
568
|
if (opts.upload !== "none") activeMSes.push(`upload (port 4002)`);
|
|
514
569
|
if (opts.payment !== "none") activeMSes.push(`payment (port 4003)`);
|
|
@@ -918,5 +973,6 @@ Re-run with @latest to refresh:
|
|
|
918
973
|
}
|
|
919
974
|
export {
|
|
920
975
|
collectOptions,
|
|
976
|
+
pmRun,
|
|
921
977
|
scaffold
|
|
922
978
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idevconn/create-icore",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Bootstrap a new project from the iCore scaffold (Nx + NestJS + React + Vite + shadcn/Tailwind, swappable auth + storage providers).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "iDEVconn",
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
"test:watch": "vitest",
|
|
57
57
|
"lint": "eslint src",
|
|
58
58
|
"typecheck": "tsc --noEmit",
|
|
59
|
+
"smoke": "npm run build && node scripts/snapshot-templates.mjs && node scripts/smoke-scaffold.mjs --mode=link --projects=shared,firebase-admin,auth,upload,notes,api",
|
|
60
|
+
"smoke:run": "npm run build && node scripts/snapshot-templates.mjs && node scripts/smoke-scaffold.mjs --mode=install --run --projects=shared,firebase-admin,auth,upload,notes,api --services=api,auth,upload,notes",
|
|
59
61
|
"prepublishOnly": "node scripts/snapshot-templates.mjs && npm run typecheck && npm run test && npm run build"
|
|
60
62
|
},
|
|
61
63
|
"dependencies": {
|
|
@@ -15,5 +15,19 @@ UPLOAD_PORT=4002
|
|
|
15
15
|
# UPLOAD_REDIS_URL=redis://localhost:6379
|
|
16
16
|
# UPLOAD_NATS_URL=nats://localhost:4222
|
|
17
17
|
|
|
18
|
+
# Notes MS transport — must match apps/microservices/notes/.env
|
|
19
|
+
NOTES_TRANSPORT=tcp
|
|
20
|
+
NOTES_HOST=127.0.0.1
|
|
21
|
+
NOTES_PORT=4004
|
|
22
|
+
# NOTES_REDIS_URL=redis://localhost:6379
|
|
23
|
+
# NOTES_NATS_URL=nats://localhost:4222
|
|
24
|
+
|
|
25
|
+
# Payment MS transport — must match apps/microservices/payment/.env
|
|
26
|
+
PAYMENT_TRANSPORT=tcp
|
|
27
|
+
PAYMENT_HOST=127.0.0.1
|
|
28
|
+
PAYMENT_PORT=4003
|
|
29
|
+
# PAYMENT_REDIS_URL=redis://localhost:6379
|
|
30
|
+
# PAYMENT_NATS_URL=nats://localhost:4222
|
|
31
|
+
|
|
18
32
|
# Per-request multipart file size cap (KB). Default 5120 (5 MB) when unset.
|
|
19
33
|
MAX_FILE_SIZE_KB=5120
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@icore/auth-firebase": "*",
|
|
7
7
|
"@icore/auth-supabase": "*",
|
|
8
|
+
"@icore/firebase-admin": "*",
|
|
8
9
|
"@icore/shared": "*",
|
|
9
|
-
"firebase-admin": "^13.0.0",
|
|
10
10
|
"@nestjs/common": "^11.1.24",
|
|
11
11
|
"@nestjs/config": "^4.0.4",
|
|
12
12
|
"@nestjs/core": "^11.1.24",
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { Module } from '@nestjs/common';
|
|
2
|
+
import { Module, Logger } from '@nestjs/common';
|
|
3
3
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
4
4
|
import { createClient } from '@supabase/supabase-js';
|
|
5
|
-
import * as admin from 'firebase-admin';
|
|
6
5
|
import { SupabaseAuthStrategy } from '@icore/auth-supabase';
|
|
7
6
|
import { FirebaseAuthStrategy, HttpIdentityToolkitClient } from '@icore/auth-firebase';
|
|
7
|
+
import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
|
|
8
8
|
import { FakeAuthStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
|
|
9
9
|
import type { AuthStrategy } from '@icore/shared';
|
|
10
|
-
import { Logger } from '@nestjs/common';
|
|
11
10
|
import { AuthController } from './auth.controller';
|
|
12
11
|
|
|
13
12
|
const ENV_PATH = 'apps/microservices/auth/.env';
|
|
@@ -15,33 +14,28 @@ const ENV_PATH = 'apps/microservices/auth/.env';
|
|
|
15
14
|
// Env vars each provider needs (besides AUTH_PROVIDER itself).
|
|
16
15
|
const REQUIRED_ENV: Record<string, string[]> = {
|
|
17
16
|
supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
|
|
18
|
-
firebase: [
|
|
19
|
-
'FB_ADMIN_PROJECT_ID',
|
|
20
|
-
'FB_ADMIN_CLIENT_EMAIL',
|
|
21
|
-
'FB_ADMIN_PRIVATE_KEY',
|
|
22
|
-
'FIREBASE_WEB_API_KEY',
|
|
23
|
-
],
|
|
17
|
+
firebase: [...FIREBASE_ADMIN_REQUIRED_ENV, 'FIREBASE_WEB_API_KEY'],
|
|
24
18
|
};
|
|
25
19
|
|
|
26
20
|
function requireEnv(cfg: ConfigService, key: string): string {
|
|
27
21
|
return cfg.getOrThrow<string>(key);
|
|
28
22
|
}
|
|
29
23
|
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
function makeSupabaseAuth(cfg: ConfigService): AuthStrategy {
|
|
25
|
+
const client = createClient(
|
|
26
|
+
requireEnv(cfg, 'SUPABASE_URL'),
|
|
27
|
+
requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
|
|
28
|
+
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
29
|
+
);
|
|
30
|
+
return new SupabaseAuthStrategy({ client });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeFirebaseAuth(cfg: ConfigService): AuthStrategy {
|
|
34
|
+
const app = getFirebaseAdmin(cfg);
|
|
41
35
|
const identityToolkit = new HttpIdentityToolkitClient(requireEnv(cfg, 'FIREBASE_WEB_API_KEY'));
|
|
42
36
|
return new FirebaseAuthStrategy({
|
|
43
37
|
identityToolkit,
|
|
44
|
-
adminAuth:
|
|
38
|
+
adminAuth: app.auth(),
|
|
45
39
|
});
|
|
46
40
|
}
|
|
47
41
|
|
|
@@ -83,15 +77,8 @@ function makeFirebaseStrategy(cfg: ConfigService): AuthStrategy {
|
|
|
83
77
|
if (!keys || missing.length > 0) return fallback();
|
|
84
78
|
|
|
85
79
|
try {
|
|
86
|
-
if (provider === 'supabase')
|
|
87
|
-
|
|
88
|
-
requireEnv(cfg, 'SUPABASE_URL'),
|
|
89
|
-
requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
|
|
90
|
-
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
91
|
-
);
|
|
92
|
-
return new SupabaseAuthStrategy({ client });
|
|
93
|
-
}
|
|
94
|
-
return makeFirebaseStrategy(cfg);
|
|
80
|
+
if (provider === 'supabase') return makeSupabaseAuth(cfg);
|
|
81
|
+
return makeFirebaseAuth(cfg);
|
|
95
82
|
} catch (err) {
|
|
96
83
|
// Vars present but invalid (e.g. placeholder URL the SDK rejects).
|
|
97
84
|
return fallback(err instanceof Error ? err.message : String(err));
|
|
@@ -1,28 +1,11 @@
|
|
|
1
1
|
import { Logger } from '@nestjs/common';
|
|
2
2
|
import { NestFactory } from '@nestjs/core';
|
|
3
3
|
import { MicroserviceOptions } from '@nestjs/microservices';
|
|
4
|
-
import { buildTransportMS } from '@icore/shared';
|
|
4
|
+
import { bootstrapMicroservice, buildTransportMS } from '@icore/shared';
|
|
5
5
|
import { AppModule } from './app/app.module';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
await app.listen();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
bootstrap()
|
|
16
|
-
.then(() => {
|
|
17
|
-
const logger = new Logger('Auth-Bootstrap');
|
|
18
|
-
logger.log(
|
|
19
|
-
`Auth MS Bootstrap completed: transport=${process.env.AUTH_TRANSPORT ?? 'tcp'} host=${process.env.AUTH_HOST ?? '127.0.0.1'} port=${process.env.AUTH_PORT ?? '4001'}`,
|
|
20
|
-
);
|
|
21
|
-
})
|
|
22
|
-
.catch((err) => {
|
|
23
|
-
new Logger('Auth-Bootstrap').error(
|
|
24
|
-
'Auth MS bootstrap failed',
|
|
25
|
-
err instanceof Error ? err.stack : err,
|
|
26
|
-
);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
});
|
|
7
|
+
void bootstrapMicroservice(
|
|
8
|
+
'AUTH',
|
|
9
|
+
() => NestFactory.createMicroservice<MicroserviceOptions>(AppModule, buildTransportMS('AUTH')),
|
|
10
|
+
new Logger('Auth-Bootstrap'),
|
|
11
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common';
|
|
2
|
+
import IORedis from 'ioredis';
|
|
3
|
+
import { formatEnvBanner } from '@icore/shared';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates an IORedis connection that NEVER crashes the process when Redis is
|
|
7
|
+
* unreachable. Without an 'error' handler, ioredis emits an unhandled 'error'
|
|
8
|
+
* event → Node exits. Here we log one boxed banner and let ioredis keep
|
|
9
|
+
* retrying in the background, so the jobs MS stays up and connects once Redis
|
|
10
|
+
* is available (honours the "never crash on missing infra" rule).
|
|
11
|
+
*/
|
|
12
|
+
export function createJobsRedis(url: string, logger: Logger): IORedis {
|
|
13
|
+
const connection = new IORedis(url, {
|
|
14
|
+
maxRetriesPerRequest: null,
|
|
15
|
+
retryStrategy: (times) => Math.min(times * 200, 5000),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let warned = false;
|
|
19
|
+
connection.on('error', (err: Error) => {
|
|
20
|
+
if (warned) return;
|
|
21
|
+
warned = true;
|
|
22
|
+
logger.warn(
|
|
23
|
+
formatEnvBanner({
|
|
24
|
+
service: 'jobs MS',
|
|
25
|
+
provider: 'redis',
|
|
26
|
+
missing: [],
|
|
27
|
+
envPath: 'apps/microservices/jobs/.env (JOBS_REDIS_URL)',
|
|
28
|
+
reason: `${err.message} — retrying in the background until Redis is up at ${url}`,
|
|
29
|
+
headline: `⚠ jobs MS — Redis unreachable (workers idle until it's up)`,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return connection;
|
|
35
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from '@ne
|
|
|
2
2
|
import { ConfigService } from '@nestjs/config';
|
|
3
3
|
import { Worker, type Job } from 'bullmq';
|
|
4
4
|
import IORedis from 'ioredis';
|
|
5
|
+
import { createJobsRedis } from '../redis-connection';
|
|
5
6
|
import type { CleanupJob } from '@icore/shared';
|
|
6
7
|
|
|
7
8
|
@Injectable()
|
|
@@ -15,7 +16,7 @@ export class CleanupWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
15
16
|
onModuleInit(): void {
|
|
16
17
|
const url = this.cfg.get<string>('JOBS_REDIS_URL') ?? 'redis://localhost:6379';
|
|
17
18
|
const concurrency = Number(this.cfg.get<string>('JOBS_WORKER_CONCURRENCY') ?? '5');
|
|
18
|
-
this.connection =
|
|
19
|
+
this.connection = createJobsRedis(url, this.logger);
|
|
19
20
|
this.worker = new Worker<CleanupJob>(
|
|
20
21
|
'cleanup',
|
|
21
22
|
async (job: Job<CleanupJob>) => {
|
|
@@ -2,6 +2,7 @@ import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from '@ne
|
|
|
2
2
|
import { ConfigService } from '@nestjs/config';
|
|
3
3
|
import { Worker, type Job } from 'bullmq';
|
|
4
4
|
import IORedis from 'ioredis';
|
|
5
|
+
import { createJobsRedis } from '../redis-connection';
|
|
5
6
|
import type { EmailJob } from '@icore/shared';
|
|
6
7
|
|
|
7
8
|
@Injectable()
|
|
@@ -15,7 +16,7 @@ export class EmailWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
15
16
|
onModuleInit(): void {
|
|
16
17
|
const url = this.cfg.get<string>('JOBS_REDIS_URL') ?? 'redis://localhost:6379';
|
|
17
18
|
const concurrency = Number(this.cfg.get<string>('JOBS_WORKER_CONCURRENCY') ?? '5');
|
|
18
|
-
this.connection =
|
|
19
|
+
this.connection = createJobsRedis(url, this.logger);
|
|
19
20
|
this.worker = new Worker<EmailJob>(
|
|
20
21
|
'email',
|
|
21
22
|
async (job: Job<EmailJob>) => {
|
|
@@ -2,6 +2,7 @@ import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from '@ne
|
|
|
2
2
|
import { ConfigService } from '@nestjs/config';
|
|
3
3
|
import { Worker, type Job } from 'bullmq';
|
|
4
4
|
import IORedis from 'ioredis';
|
|
5
|
+
import { createJobsRedis } from '../redis-connection';
|
|
5
6
|
import type { ImageProcessJob } from '@icore/shared';
|
|
6
7
|
|
|
7
8
|
@Injectable()
|
|
@@ -15,7 +16,7 @@ export class ImageProcessWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
15
16
|
onModuleInit(): void {
|
|
16
17
|
const url = this.cfg.get<string>('JOBS_REDIS_URL') ?? 'redis://localhost:6379';
|
|
17
18
|
const concurrency = Number(this.cfg.get<string>('JOBS_WORKER_CONCURRENCY') ?? '5');
|
|
18
|
-
this.connection =
|
|
19
|
+
this.connection = createJobsRedis(url, this.logger);
|
|
19
20
|
this.worker = new Worker<ImageProcessJob>(
|
|
20
21
|
'image-process',
|
|
21
22
|
async (job: Job<ImageProcessJob>) => {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { Module } from '@nestjs/common';
|
|
2
|
+
import { Module, Logger } from '@nestjs/common';
|
|
3
3
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
4
4
|
import { createClient } from '@supabase/supabase-js';
|
|
5
|
-
import * as admin from 'firebase-admin';
|
|
6
5
|
import { SupabaseDBStrategy } from '@icore/db-supabase';
|
|
7
6
|
import { FirestoreDBStrategy } from '@icore/db-firestore';
|
|
7
|
+
import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
|
|
8
8
|
import { FakeDBStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
|
|
9
9
|
import type { DBStrategy } from '@icore/shared';
|
|
10
|
-
import { Logger } from '@nestjs/common';
|
|
11
10
|
import { NotesController } from './notes.controller';
|
|
12
11
|
|
|
13
12
|
const ENV_PATH = 'apps/microservices/notes/.env';
|
|
@@ -15,14 +14,30 @@ const ENV_PATH = 'apps/microservices/notes/.env';
|
|
|
15
14
|
// DB_PROVIDER accepts supabase | firestore | firebase (latter two are Firestore).
|
|
16
15
|
const REQUIRED_ENV: Record<string, string[]> = {
|
|
17
16
|
supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
|
|
18
|
-
firestore: [
|
|
19
|
-
firebase: [
|
|
17
|
+
firestore: [...FIREBASE_ADMIN_REQUIRED_ENV],
|
|
18
|
+
firebase: [...FIREBASE_ADMIN_REQUIRED_ENV],
|
|
20
19
|
};
|
|
21
20
|
|
|
22
21
|
function requireEnv(cfg: ConfigService, key: string): string {
|
|
23
22
|
return cfg.getOrThrow<string>(key);
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
function makeSupabaseDB(cfg: ConfigService): DBStrategy {
|
|
26
|
+
const client = createClient(
|
|
27
|
+
requireEnv(cfg, 'SUPABASE_URL'),
|
|
28
|
+
requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
|
|
29
|
+
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
30
|
+
);
|
|
31
|
+
return new SupabaseDBStrategy({ client });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeFirestoreDB(cfg: ConfigService): DBStrategy {
|
|
35
|
+
const app = getFirebaseAdmin(cfg);
|
|
36
|
+
return new FirestoreDBStrategy({
|
|
37
|
+
db: app.firestore() as unknown as ConstructorParameters<typeof FirestoreDBStrategy>[0]['db'],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
@Module({
|
|
27
42
|
imports: [
|
|
28
43
|
ConfigModule.forRoot({
|
|
@@ -59,28 +74,8 @@ function requireEnv(cfg: ConfigService, key: string): string {
|
|
|
59
74
|
if (!keys || missing.length > 0) return fallback();
|
|
60
75
|
|
|
61
76
|
try {
|
|
62
|
-
if (provider === 'supabase')
|
|
63
|
-
|
|
64
|
-
requireEnv(cfg, 'SUPABASE_URL'),
|
|
65
|
-
requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
|
|
66
|
-
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
67
|
-
);
|
|
68
|
-
return new SupabaseDBStrategy({ client });
|
|
69
|
-
}
|
|
70
|
-
if (admin.apps.length === 0) {
|
|
71
|
-
admin.initializeApp({
|
|
72
|
-
credential: admin.credential.cert({
|
|
73
|
-
projectId: requireEnv(cfg, 'FB_ADMIN_PROJECT_ID'),
|
|
74
|
-
clientEmail: requireEnv(cfg, 'FB_ADMIN_CLIENT_EMAIL'),
|
|
75
|
-
privateKey: requireEnv(cfg, 'FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
|
|
76
|
-
}),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return new FirestoreDBStrategy({
|
|
80
|
-
db: admin.firestore() as unknown as ConstructorParameters<
|
|
81
|
-
typeof FirestoreDBStrategy
|
|
82
|
-
>[0]['db'],
|
|
83
|
-
});
|
|
77
|
+
if (provider === 'supabase') return makeSupabaseDB(cfg);
|
|
78
|
+
return makeFirestoreDB(cfg);
|
|
84
79
|
} catch (err) {
|
|
85
80
|
return fallback(err instanceof Error ? err.message : String(err));
|
|
86
81
|
}
|
|
@@ -1,28 +1,11 @@
|
|
|
1
1
|
import { Logger } from '@nestjs/common';
|
|
2
2
|
import { NestFactory } from '@nestjs/core';
|
|
3
3
|
import { MicroserviceOptions } from '@nestjs/microservices';
|
|
4
|
-
import { buildTransportMS } from '@icore/shared';
|
|
4
|
+
import { bootstrapMicroservice, buildTransportMS } from '@icore/shared';
|
|
5
5
|
import { AppModule } from './app/app.module';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
await app.listen();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
bootstrap()
|
|
16
|
-
.then(() => {
|
|
17
|
-
const logger = new Logger('Notes-Bootstrap');
|
|
18
|
-
logger.log(
|
|
19
|
-
`Notes MS Bootstrap completed: transport=${process.env.NOTES_TRANSPORT ?? 'tcp'} host=${process.env.NOTES_HOST ?? '127.0.0.1'} port=${process.env.NOTES_PORT ?? '4004'}`,
|
|
20
|
-
);
|
|
21
|
-
})
|
|
22
|
-
.catch((err) => {
|
|
23
|
-
new Logger('Notes-Bootstrap').error(
|
|
24
|
-
'Notes MS bootstrap failed',
|
|
25
|
-
err instanceof Error ? err.stack : err,
|
|
26
|
-
);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
});
|
|
7
|
+
void bootstrapMicroservice(
|
|
8
|
+
'NOTES',
|
|
9
|
+
() => NestFactory.createMicroservice<MicroserviceOptions>(AppModule, buildTransportMS('NOTES')),
|
|
10
|
+
new Logger('Notes-Bootstrap'),
|
|
11
|
+
);
|
|
@@ -40,17 +40,19 @@ const REQUIRED_ENV: Record<string, string[]> = {
|
|
|
40
40
|
envPath: ENV_PATH,
|
|
41
41
|
headline: `⚠ payment MS — ${provider} credentials missing (payments will fail)`,
|
|
42
42
|
});
|
|
43
|
-
// Prod: fail fast. Dev: warn
|
|
44
|
-
//
|
|
43
|
+
// Prod: fail fast. Dev: warn + register an EMPTY strategy map so the
|
|
44
|
+
// MS boots (PaypalStrategy's constructor throws on blank creds, so we
|
|
45
|
+
// must not instantiate it) — payment endpoints fail until creds are set.
|
|
45
46
|
if (process.env.NODE_ENV === 'production') throw new Error(banner);
|
|
46
47
|
logger.warn(banner);
|
|
48
|
+
return createPayment({ strategies: {} });
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
return createPayment({
|
|
50
52
|
strategies: {
|
|
51
53
|
paypal: new PaypalStrategy({
|
|
52
|
-
clientId: cfg.
|
|
53
|
-
secret: cfg.
|
|
54
|
+
clientId: cfg.getOrThrow<string>('PAYPAL_CLIENT_ID'),
|
|
55
|
+
secret: cfg.getOrThrow<string>('PAYPAL_CLIENT_SECRET'),
|
|
54
56
|
environment: cfg.get<'sandbox' | 'live'>('PAYPAL_ENVIRONMENT') ?? 'sandbox',
|
|
55
57
|
}),
|
|
56
58
|
},
|