@idevconn/create-icore 0.4.1 → 0.5.0

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 (48) hide show
  1. package/dist/cli.js +221 -0
  2. package/dist/index.cjs +221 -0
  3. package/dist/index.js +221 -0
  4. package/package.json +1 -1
  5. package/templates/apps/api/src/app/app.module.ts +5 -1
  6. package/templates/apps/api/src/main.ts +12 -6
  7. package/templates/apps/microservices/auth/project.json +2 -1
  8. package/templates/apps/microservices/auth/src/app/app.module.ts +47 -23
  9. package/templates/apps/microservices/jobs/project.json +2 -1
  10. package/templates/apps/microservices/notes/project.json +2 -1
  11. package/templates/apps/microservices/notes/src/app/app.module.ts +44 -25
  12. package/templates/apps/microservices/payment/project.json +2 -1
  13. package/templates/apps/microservices/payment/src/app/app.module.ts +35 -12
  14. package/templates/apps/microservices/upload/project.json +2 -1
  15. package/templates/apps/microservices/upload/src/app/app.module.ts +48 -28
  16. package/templates/apps/templates/client-antd/.env.example +7 -0
  17. package/templates/apps/templates/client-antd/vite.config.mts +4 -4
  18. package/templates/apps/templates/client-mui/.env.example +7 -0
  19. package/templates/apps/templates/client-mui/vite.config.mts +4 -4
  20. package/templates/apps/templates/client-shadcn/.env.example +6 -1
  21. package/templates/apps/templates/client-shadcn/vite.config.mts +4 -4
  22. package/templates/libs/auth-client/src/index.ts +1 -0
  23. package/templates/libs/auth-client/src/lib/auth-client.module.ts +1 -1
  24. package/templates/libs/auth-client/src/lib/auth-client.service.ts +1 -1
  25. package/templates/libs/auth-client/src/lib/auth-client.tokens.ts +4 -0
  26. package/templates/libs/jobs-client/src/index.ts +1 -0
  27. package/templates/libs/jobs-client/src/lib/jobs-client.module.ts +1 -1
  28. package/templates/libs/jobs-client/src/lib/jobs-client.service.ts +1 -1
  29. package/templates/libs/jobs-client/src/lib/jobs-client.tokens.ts +4 -0
  30. package/templates/libs/notes-client/src/index.ts +1 -0
  31. package/templates/libs/notes-client/src/lib/notes-client.module.ts +1 -1
  32. package/templates/libs/notes-client/src/lib/notes-client.service.ts +1 -1
  33. package/templates/libs/notes-client/src/lib/notes-client.tokens.ts +4 -0
  34. package/templates/libs/payment-client/src/index.ts +1 -0
  35. package/templates/libs/payment-client/src/lib/payment-client.module.ts +1 -1
  36. package/templates/libs/payment-client/src/lib/payment-client.service.ts +1 -1
  37. package/templates/libs/payment-client/src/lib/payment-client.tokens.ts +4 -0
  38. package/templates/libs/shared/src/env.ts +88 -0
  39. package/templates/libs/shared/src/index.ts +1 -0
  40. package/templates/libs/shared/src/transport.ts +37 -0
  41. package/templates/libs/upload-client/src/index.ts +1 -0
  42. package/templates/libs/upload-client/src/lib/upload-client.module.ts +1 -1
  43. package/templates/libs/upload-client/src/lib/upload-client.service.ts +1 -1
  44. package/templates/libs/upload-client/src/lib/upload-client.tokens.ts +4 -0
  45. package/templates/libs/vite-plugins/src/index.d.mts +6 -0
  46. package/templates/libs/vite-plugins/src/index.mjs +50 -0
  47. package/templates/package.json +1 -0
  48. package/templates/tools/create-icore/_template-shell/package.json +1 -0
package/dist/index.js CHANGED
@@ -38,6 +38,22 @@ async function rewriteRootPackageJson(targetDir, opts) {
38
38
  if (opts.packageManager !== "yarn") {
39
39
  delete pkg.packageManager;
40
40
  }
41
+ if (opts.packageManager === "pnpm") {
42
+ pkg["pnpm"] = {
43
+ onlyBuiltDependencies: [
44
+ "@firebase/util",
45
+ "@nestjs/core",
46
+ "@parcel/watcher",
47
+ "@scarf/scarf",
48
+ "@swc/core",
49
+ "less",
50
+ "msgpackr-extract",
51
+ "nx",
52
+ "protobufjs",
53
+ "unrs-resolver"
54
+ ]
55
+ };
56
+ }
41
57
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
42
58
  }
43
59
  async function writeAuthEnv(targetDir, opts) {
@@ -90,6 +106,14 @@ async function writeRootEnv(targetDir, opts) {
90
106
  ];
91
107
  await writeFile(join(targetDir, ".env"), lines.join("\n"));
92
108
  }
109
+ async function writeClientEnv(targetDir) {
110
+ const envExample = join(targetDir, "apps/client/.env.example");
111
+ try {
112
+ const env = await readFile(envExample, "utf8");
113
+ await writeFile(join(targetDir, "apps/client/.env"), env);
114
+ } catch {
115
+ }
116
+ }
93
117
  async function writePaymentEnv(targetDir, opts) {
94
118
  if (opts.payment === "none") return;
95
119
  const envExample = join(targetDir, "apps/microservices/payment/.env.example");
@@ -466,6 +490,7 @@ async function scaffold(opts, templatesDir) {
466
490
  await writeGatewayEnv(opts.targetDir, opts);
467
491
  await writeRootEnv(opts.targetDir, opts);
468
492
  await selectClientTemplate(opts.targetDir, opts);
493
+ await writeClientEnv(opts.targetDir);
469
494
  if (opts.upload === "none") await removeUploadStack(opts.targetDir);
470
495
  if (opts.payment === "none") await removePaymentStack(opts.targetDir);
471
496
  if (opts.jobs === "none") await removeJobsStack(opts.targetDir);
@@ -476,9 +501,199 @@ async function scaffold(opts, templatesDir) {
476
501
  if (opts.packageManager === "yarn") {
477
502
  await writeFile(join(opts.targetDir, "yarn.lock"), "");
478
503
  }
504
+ await writeAiFiles(opts.targetDir, opts);
479
505
  if (opts.install) runInstall(opts.targetDir, opts.packageManager);
480
506
  if (opts.initGit) gitInit(opts.targetDir, opts.projectName);
481
507
  }
508
+ async function writeAiFiles(targetDir, opts) {
509
+ const pm = opts.packageManager;
510
+ const nx = pm === "npm" ? "npx nx" : `${pm} nx`;
511
+ const devCmd = `${pm} dev`;
512
+ const activeMSes = ["auth (port 4001)"];
513
+ if (opts.upload !== "none") activeMSes.push(`upload (port 4002)`);
514
+ if (opts.payment !== "none") activeMSes.push(`payment (port 4003)`);
515
+ if (opts.example !== "none") activeMSes.push(`notes (port 4004)`);
516
+ if (opts.jobs !== "none") activeMSes.push(`jobs (standalone)`);
517
+ const usesSupabase = opts.authProvider === "supabase" || opts.dbProvider === "supabase" || opts.upload === "supabase";
518
+ const usesFirebase = opts.authProvider === "firebase" || opts.dbProvider === "firebase" || opts.upload === "firebase";
519
+ await writeFile(join(targetDir, "CLAUDE.md"), "@AGENTS.md\n");
520
+ const uiLabel = { shadcn: "shadcn/ui + Tailwind", antd: "Ant Design 6", mui: "MUI 6" }[opts.ui];
521
+ const readme = `# ${opts.projectName}
522
+
523
+ > Scaffolded with [iCore](https://github.com/iDEVconn/create-icore) \u2014 Nx + NestJS + React full-stack template.
524
+
525
+ ## Stack
526
+
527
+ | Layer | Technology |
528
+ |-------|-----------|
529
+ | Monorepo | Nx + ${pm} |
530
+ | Gateway | NestJS 11 + Swagger |
531
+ | Auth | ${opts.authProvider} |
532
+ | Database | ${opts.dbProvider} |
533
+ | Upload | ${opts.upload === "none" ? "\u2014" : opts.upload} |
534
+ | UI | ${uiLabel} + TanStack Router + Query |
535
+ | i18n | i18next (en / ru / he) |
536
+
537
+ ## Quick start
538
+
539
+ \`\`\`bash
540
+ # 1. Fill in provider credentials
541
+ # apps/microservices/auth/.env
542
+ # apps/microservices/upload/.env (if upload is enabled)
543
+ # apps/client/.env (VITE_API_URL \u2014 already defaults to /api)
544
+
545
+ # 2. Start everything
546
+ ${devCmd}
547
+ # \u2192 http://localhost:4200 client
548
+ # \u2192 http://localhost:3001/api/docs Swagger
549
+ \`\`\`
550
+
551
+ ## Commands
552
+
553
+ \`\`\`bash
554
+ ${nx} run <project>:serve # start a single service
555
+ ${nx} test <project> # unit tests
556
+ ${nx} lint <project> # lint
557
+ ${nx} build <project> # production build
558
+ ${pm === "yarn" ? "yarn remove-notes" : pm === "pnpm" ? "pnpm remove-notes" : "npm run remove-notes"} # strip the notes sample feature
559
+ \`\`\`
560
+
561
+ ## Scaffolded by
562
+
563
+ [iCore](https://github.com/iDEVconn/create-icore) \u2014 [@idevconn/create-icore](https://www.npmjs.com/package/@idevconn/create-icore)
564
+
565
+ ## License
566
+
567
+ Apache-2.0
568
+ `;
569
+ await writeFile(join(targetDir, "README.md"), readme);
570
+ const agents = `# ${opts.projectName} \u2014 Agent Instructions
571
+
572
+ ## Stack snapshot
573
+
574
+ | Dimension | Choice |
575
+ |------------|--------|
576
+ | Auth | ${opts.authProvider} |
577
+ | Database | ${opts.dbProvider} |
578
+ | Upload | ${opts.upload} |
579
+ | Payment | ${opts.payment} |
580
+ | Jobs | ${opts.jobs} |
581
+ | UI | ${opts.ui} |
582
+ | Transport | ${opts.transport} |
583
+ | PM | ${pm} |
584
+
585
+ ## \u{1F680} Mandatory Workflow
586
+
587
+ - **Branch strategy**: \`dev\` is default. Cut \`feature/<name>\` or \`bug/<name>\` from dev. PRs only target dev. Never push directly to main.
588
+ - **No code without approval**: Propose changes first, wait for go-ahead.
589
+ - **\u0417\u0410\u041A\u041E\u041D \u2014 no crash on missing .env**: MS factories must catch config errors, print a boxed banner with ALL missing vars, and return a Fake strategy in dev. In prod (\`NODE_ENV=production\`) throw the same banner. The \`formatEnvBanner\` + \`missingEnv\` helpers from \`@icore/shared\` handle this.
590
+ - **Post-coding routine**: \`npx prettier --write <files>\` \u2192 \`${nx} lint <project>\` \u2192 \`${nx} build <project>\` \u2014 all green before committing.
591
+ - **Nx generators only**: never hand-write \`project.json\` / tsconfig stacks. Use \`${nx} g @nx/<plugin>:<schematic>\`.
592
+
593
+ ## Architecture
594
+
595
+ \`\`\`
596
+ apps/
597
+ \u251C\u2500\u2500 api/ NestJS gateway \u2014 all client traffic enters here (:3001)
598
+ \u251C\u2500\u2500 microservices/
599
+ ${activeMSes.map((s) => `\u2502 \u251C\u2500\u2500 ${s.split(" ")[0]}/`).join("\n")}
600
+ \u2514\u2500\u2500 client/ Vite + React 19 + ${opts.ui} (:4200)
601
+ libs/
602
+ \u251C\u2500\u2500 shared/ contracts, CASL, transport helpers, env banner utils
603
+ \u251C\u2500\u2500 auth-strategies/${opts.authProvider}/
604
+ ${opts.upload !== "none" ? `\u251C\u2500\u2500 storage-strategies/${opts.upload}/
605
+ ` : ""}\u251C\u2500\u2500 db-strategies/${opts.dbProvider === "firebase" ? "firestore" : opts.dbProvider}/
606
+ \u251C\u2500\u2500 auth-client/ gateway \u2192 auth MS (TCP/Redis/NATS)
607
+ ${opts.upload !== "none" ? `\u251C\u2500\u2500 upload-client/ gateway \u2192 upload MS
608
+ ` : ""}\u2514\u2500\u2500 template-shared/ browser-safe React foundation (stores, i18n, CASL)
609
+ \`\`\`
610
+
611
+ ## Key patterns
612
+
613
+ **Strategy swap** \u2014 provider is chosen at runtime via env. Never import a concrete strategy in app code; always inject via the factory token (\`AuthStrategy\`, \`StorageStrategy\`, \`DBStrategy\`).
614
+
615
+ **Transport** \u2014 \`buildTransport(prefix)\` reads \`${opts.transport.toUpperCase()}*\` vars. Same helper on gateway client-modules and each MS \`main.ts\`. Supports tcp / redis / nats \u2014 change by flipping \`*_TRANSPORT\` in \`.env\`.
616
+
617
+ **Env layering**:
618
+ 1. Root \`.env\` \u2014 \`DB_PROVIDER\`
619
+ 2. \`apps/api/.env\` \u2014 gateway transport endpoints
620
+ 3. \`apps/microservices/<name>/.env\` \u2014 each MS provider + transport
621
+ 4. \`apps/client/.env\` \u2014 \`VITE_API_URL\`
622
+
623
+ ## Commands
624
+
625
+ \`\`\`bash
626
+ ${devCmd} # start all services
627
+ ${nx} run api:serve # gateway only
628
+ ${nx} run auth:serve # auth MS only
629
+ ${nx} test <project> # run tests
630
+ ${nx} lint <project> # lint
631
+ ${nx} build <project> # build
632
+ ${nx} g @nx/nest:resource # generate NestJS resource
633
+ \`\`\`
634
+
635
+ ## .env files to configure
636
+
637
+ | File | Key vars |
638
+ |------|----------|
639
+ | \`apps/microservices/auth/.env\` | \`AUTH_PROVIDER=${opts.authProvider}\`, ${opts.authProvider === "supabase" ? "`SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`" : "`FB_ADMIN_*`, `FIREBASE_WEB_API_KEY`"} |
640
+ ${opts.upload !== "none" ? `| \`apps/microservices/upload/.env\` | \`STORAGE_PROVIDER=${opts.upload}\`, provider creds |
641
+ ` : ""}| \`apps/microservices/notes/.env\` | \`DB_PROVIDER=${opts.dbProvider}\`, DB creds |
642
+ | \`apps/client/.env\` | \`VITE_API_URL=/api\` (proxied to :3001 in dev) |
643
+
644
+ ## Testing
645
+
646
+ - Unit tests: Vitest, files named \`*.unit.test.ts(x)\` in \`__tests__/\` next to source.
647
+ - Test behaviour, not implementation. Fake strategies from \`@icore/shared\` (FakeAuthStrategy etc.) serve as test doubles.
648
+ - Run: \`${nx} test <project>\`
649
+ `;
650
+ await writeFile(join(targetDir, "AGENTS.md"), agents);
651
+ await mkdir(join(targetDir, ".claude"), { recursive: true });
652
+ const mcpServers = {
653
+ nx: {
654
+ command: "npx",
655
+ args: ["-y", "@nx/mcp@latest", "--directory", "."],
656
+ type: "stdio"
657
+ }
658
+ };
659
+ if (usesSupabase) {
660
+ mcpServers["supabase"] = {
661
+ command: "npx",
662
+ args: [
663
+ "-y",
664
+ "@supabase/mcp-server-supabase@latest",
665
+ "--access-token",
666
+ "<SUPABASE_PERSONAL_ACCESS_TOKEN>"
667
+ ],
668
+ type: "stdio"
669
+ };
670
+ }
671
+ if (usesFirebase) {
672
+ mcpServers["firebase"] = {
673
+ command: "npx",
674
+ args: ["-y", "firebase-tools@latest", "experimental:mcp"],
675
+ type: "stdio"
676
+ };
677
+ }
678
+ const nxCmds = [`Bash(${nx} *)`, `Bash(${devCmd})`];
679
+ if (pm !== "npm") nxCmds.push(`Bash(npx nx *)`);
680
+ const settings = {
681
+ mcpServers,
682
+ permissions: {
683
+ allow: [
684
+ ...nxCmds,
685
+ "Bash(npx prettier *)",
686
+ "Bash(git status)",
687
+ "Bash(git diff *)",
688
+ "Bash(git log *)"
689
+ ]
690
+ }
691
+ };
692
+ await writeFile(
693
+ join(targetDir, ".claude", "settings.json"),
694
+ JSON.stringify(settings, null, 2) + "\n"
695
+ );
696
+ }
482
697
 
483
698
  // src/lib/prompts.ts
484
699
  import * as p from "@clack/prompts";
@@ -674,6 +889,12 @@ Re-run with @latest to refresh:
674
889
  });
675
890
  if (p.isCancel(transport)) throw new Error("cancelled");
676
891
  const packageManager = flags.packageManager ?? detectPackageManager();
892
+ if (packageManager === "yarn") {
893
+ p.note(
894
+ "yarn 4.15+ enforces a 24h publish-age gate (npmMinimalAgeGate=1d), so a\n`yarn create @idevconn/icore@latest` run within 24h of a release resolves an\nolder version. If the banner above shows an unexpectedly old version, either:\n \u2022 wait \u2014 the version auto-unlocks 24h after publish, or\n \u2022 bypass once: yarn config set npmMinimalAgeGate 0 (then re-run), or\n \u2022 use npm/pnpm: npm init @idevconn/icore@latest <name> -- [flags]",
895
+ "\u26A0 yarn 24h age-gate"
896
+ );
897
+ }
677
898
  const initGit = flags.initGit ?? !await p.confirm({ message: "Initialise git repo?", initialValue: true }) === false;
678
899
  const install = flags.install ?? !await p.confirm({
679
900
  message: `Run ${packageManager} install?`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idevconn/create-icore",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
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",
@@ -1,3 +1,4 @@
1
+ import { join } from 'node:path';
1
2
  import { Module } from '@nestjs/common';
2
3
  import { ConfigModule } from '@nestjs/config';
3
4
  import { ThrottlerModule, seconds } from '@nestjs/throttler';
@@ -11,7 +12,10 @@ import { AdminModule } from './admin/admin.module';
11
12
 
12
13
  @Module({
13
14
  imports: [
14
- ConfigModule.forRoot({ isGlobal: true }),
15
+ ConfigModule.forRoot({
16
+ isGlobal: true,
17
+ envFilePath: [join(process.cwd(), 'apps/api/.env'), join(process.cwd(), '.env')],
18
+ }),
15
19
  ThrottlerModule.forRoot([{ name: 'auth-burst', ttl: seconds(60), limit: 10 }]),
16
20
  AuthModule,
17
21
  AbilitiesModule,
@@ -3,9 +3,17 @@ import { NestFactory } from '@nestjs/core';
3
3
  import { NestExpressApplication } from '@nestjs/platform-express';
4
4
  import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
5
5
  import cookieParser from 'cookie-parser';
6
+ import { formatGatewayBanner } from '@icore/shared';
6
7
  import { AppModule } from './app/app.module';
7
8
  import pkg from '@icore/package.json';
8
9
 
10
+ const GATEWAY_SERVICES = [
11
+ { name: 'auth', prefix: 'AUTH' },
12
+ { name: 'upload', prefix: 'UPLOAD' },
13
+ { name: 'notes', prefix: 'NOTES' },
14
+ { name: 'payment', prefix: 'PAYMENT' },
15
+ ];
16
+
9
17
  const DEFAULT_PORT = 3001;
10
18
 
11
19
  async function bootstrap() {
@@ -28,12 +36,10 @@ async function bootstrap() {
28
36
 
29
37
  bootstrap()
30
38
  .then(() => {
31
- const logger = new Logger('API-Bootstrap');
32
- logger.log(
33
- `API Bootstrap completed successfully: ${process.env.API_ORIGIN ?? 'http://localhost'}:${process.env.API_PORT ?? DEFAULT_PORT}/api`,
34
- );
35
- logger.log(
36
- `Swagger UI: ${process.env.API_ORIGIN ?? 'http://localhost'}:${process.env.API_PORT ?? DEFAULT_PORT}/api/docs`,
39
+ const origin = process.env.API_ORIGIN ?? 'http://localhost';
40
+ const port = Number(process.env.API_PORT ?? DEFAULT_PORT);
41
+ new Logger('API-Bootstrap').log(
42
+ formatGatewayBanner({ port, origin, services: GATEWAY_SERVICES }),
37
43
  );
38
44
  })
39
45
  .catch((err) => {
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "auth:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9230
54
55
  },
55
56
  "configurations": {
56
57
  "development": {
@@ -5,15 +5,26 @@ import { createClient } from '@supabase/supabase-js';
5
5
  import * as admin from 'firebase-admin';
6
6
  import { SupabaseAuthStrategy } from '@icore/auth-supabase';
7
7
  import { FirebaseAuthStrategy, HttpIdentityToolkitClient } from '@icore/auth-firebase';
8
- import { FakeAuthStrategy } from '@icore/shared';
8
+ import { FakeAuthStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
9
9
  import type { AuthStrategy } from '@icore/shared';
10
10
  import { Logger } from '@nestjs/common';
11
11
  import { AuthController } from './auth.controller';
12
12
 
13
+ const ENV_PATH = 'apps/microservices/auth/.env';
14
+
15
+ // Env vars each provider needs (besides AUTH_PROVIDER itself).
16
+ const REQUIRED_ENV: Record<string, string[]> = {
17
+ 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
+ ],
24
+ };
25
+
13
26
  function requireEnv(cfg: ConfigService, key: string): string {
14
- const val = cfg.getOrThrow<string>(key);
15
- if (!val) throw new Error(`${key} is not set — check apps/microservices/auth/.env`);
16
- return val;
27
+ return cfg.getOrThrow<string>(key);
17
28
  }
18
29
 
19
30
  function makeFirebaseStrategy(cfg: ConfigService): AuthStrategy {
@@ -49,28 +60,41 @@ function makeFirebaseStrategy(cfg: ConfigService): AuthStrategy {
49
60
  {
50
61
  provide: 'AuthStrategy',
51
62
  useFactory: (cfg: ConfigService): AuthStrategy => {
63
+ const logger = new Logger('AuthStrategy');
64
+ const provider = cfg.get<string>('AUTH_PROVIDER')?.trim();
65
+ const keys = provider ? REQUIRED_ENV[provider] : undefined;
66
+ const missing = keys ? missingEnv((k) => cfg.get<string>(k), keys) : [];
67
+
68
+ // Prod: fail fast — never silently run a fake auth strategy.
69
+ // Dev: warn with a boxed banner + fall back to the in-memory fake.
70
+ const fallback = (reason?: string): AuthStrategy => {
71
+ const banner = formatEnvBanner({
72
+ service: 'auth MS',
73
+ provider,
74
+ missing,
75
+ envPath: ENV_PATH,
76
+ reason,
77
+ });
78
+ if (process.env.NODE_ENV === 'production') throw new Error(banner);
79
+ logger.warn(banner);
80
+ return new FakeAuthStrategy();
81
+ };
82
+
83
+ if (!keys || missing.length > 0) return fallback();
84
+
52
85
  try {
53
- const provider = requireEnv(cfg, 'AUTH_PROVIDER');
54
- switch (provider) {
55
- case 'supabase': {
56
- const client = createClient(
57
- requireEnv(cfg, 'SUPABASE_URL'),
58
- requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
59
- { auth: { autoRefreshToken: false, persistSession: false } },
60
- );
61
- return new SupabaseAuthStrategy({ client });
62
- }
63
- case 'firebase':
64
- return makeFirebaseStrategy(cfg);
65
- default:
66
- throw new Error(`Unsupported AUTH_PROVIDER: ${provider}`);
86
+ if (provider === 'supabase') {
87
+ const client = createClient(
88
+ requireEnv(cfg, 'SUPABASE_URL'),
89
+ requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
90
+ { auth: { autoRefreshToken: false, persistSession: false } },
91
+ );
92
+ return new SupabaseAuthStrategy({ client });
67
93
  }
94
+ return makeFirebaseStrategy(cfg);
68
95
  } catch (err) {
69
- new Logger('AuthStrategy').warn(
70
- `Not configured: ${err instanceof Error ? err.message : String(err)}. ` +
71
- `Requests will fail until apps/microservices/auth/.env is set.`,
72
- );
73
- return new FakeAuthStrategy();
96
+ // Vars present but invalid (e.g. placeholder URL the SDK rejects).
97
+ return fallback(err instanceof Error ? err.message : String(err));
74
98
  }
75
99
  },
76
100
  inject: [ConfigService],
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "jobs:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9234
54
55
  },
55
56
  "configurations": {
56
57
  "development": {
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "notes:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9232
54
55
  },
55
56
  "configurations": {
56
57
  "development": {
@@ -5,15 +5,22 @@ import { createClient } from '@supabase/supabase-js';
5
5
  import * as admin from 'firebase-admin';
6
6
  import { SupabaseDBStrategy } from '@icore/db-supabase';
7
7
  import { FirestoreDBStrategy } from '@icore/db-firestore';
8
- import { FakeDBStrategy } from '@icore/shared';
8
+ import { FakeDBStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
9
9
  import type { DBStrategy } from '@icore/shared';
10
10
  import { Logger } from '@nestjs/common';
11
11
  import { NotesController } from './notes.controller';
12
12
 
13
+ const ENV_PATH = 'apps/microservices/notes/.env';
14
+
15
+ // DB_PROVIDER accepts supabase | firestore | firebase (latter two are Firestore).
16
+ const REQUIRED_ENV: Record<string, string[]> = {
17
+ supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
18
+ firestore: ['FB_ADMIN_PROJECT_ID', 'FB_ADMIN_CLIENT_EMAIL', 'FB_ADMIN_PRIVATE_KEY'],
19
+ firebase: ['FB_ADMIN_PROJECT_ID', 'FB_ADMIN_CLIENT_EMAIL', 'FB_ADMIN_PRIVATE_KEY'],
20
+ };
21
+
13
22
  function requireEnv(cfg: ConfigService, key: string): string {
14
- const val = cfg.getOrThrow<string>(key);
15
- if (!val) throw new Error(`${key} is not set — check apps/microservices/notes/.env`);
16
- return val;
23
+ return cfg.getOrThrow<string>(key);
17
24
  }
18
25
 
19
26
  @Module({
@@ -31,8 +38,27 @@ function requireEnv(cfg: ConfigService, key: string): string {
31
38
  {
32
39
  provide: 'DBStrategy',
33
40
  useFactory: (cfg: ConfigService): DBStrategy => {
41
+ const logger = new Logger('DBStrategy');
42
+ const provider = cfg.get<string>('DB_PROVIDER')?.trim();
43
+ const keys = provider ? REQUIRED_ENV[provider] : undefined;
44
+ const missing = keys ? missingEnv((k) => cfg.get<string>(k), keys) : [];
45
+
46
+ const fallback = (reason?: string): DBStrategy => {
47
+ const banner = formatEnvBanner({
48
+ service: 'notes MS',
49
+ provider,
50
+ missing,
51
+ envPath: ENV_PATH,
52
+ reason,
53
+ });
54
+ if (process.env.NODE_ENV === 'production') throw new Error(banner);
55
+ logger.warn(banner);
56
+ return new FakeDBStrategy();
57
+ };
58
+
59
+ if (!keys || missing.length > 0) return fallback();
60
+
34
61
  try {
35
- const provider = requireEnv(cfg, 'DB_PROVIDER');
36
62
  if (provider === 'supabase') {
37
63
  const client = createClient(
38
64
  requireEnv(cfg, 'SUPABASE_URL'),
@@ -41,29 +67,22 @@ function requireEnv(cfg: ConfigService, key: string): string {
41
67
  );
42
68
  return new SupabaseDBStrategy({ client });
43
69
  }
44
- if (provider === 'firestore' || provider === 'firebase') {
45
- if (admin.apps.length === 0) {
46
- admin.initializeApp({
47
- credential: admin.credential.cert({
48
- projectId: requireEnv(cfg, 'FB_ADMIN_PROJECT_ID'),
49
- clientEmail: requireEnv(cfg, 'FB_ADMIN_CLIENT_EMAIL'),
50
- privateKey: requireEnv(cfg, 'FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
51
- }),
52
- });
53
- }
54
- return new FirestoreDBStrategy({
55
- db: admin.firestore() as unknown as ConstructorParameters<
56
- typeof FirestoreDBStrategy
57
- >[0]['db'],
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
+ }),
58
77
  });
59
78
  }
60
- throw new Error(`Unsupported DB_PROVIDER: ${provider}`);
79
+ return new FirestoreDBStrategy({
80
+ db: admin.firestore() as unknown as ConstructorParameters<
81
+ typeof FirestoreDBStrategy
82
+ >[0]['db'],
83
+ });
61
84
  } catch (err) {
62
- new Logger('DBStrategy').warn(
63
- `Not configured: ${err instanceof Error ? err.message : String(err)}. ` +
64
- `Requests will fail until apps/microservices/notes/.env is set.`,
65
- );
66
- return new FakeDBStrategy();
85
+ return fallback(err instanceof Error ? err.message : String(err));
67
86
  }
68
87
  },
69
88
  inject: [ConfigService],
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "payment:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9233
54
55
  },
55
56
  "configurations": {
56
57
  "development": {
@@ -1,9 +1,16 @@
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 { PaymentRegistry, PaypalStrategy, createPayment } from '@idevconn/payment';
5
+ import { missingEnv, formatEnvBanner } from '@icore/shared';
5
6
  import { PaymentController } from './payment.controller';
6
7
 
8
+ const ENV_PATH = 'apps/microservices/payment/.env';
9
+
10
+ const REQUIRED_ENV: Record<string, string[]> = {
11
+ paypal: ['PAYPAL_CLIENT_ID', 'PAYPAL_CLIENT_SECRET'],
12
+ };
13
+
7
14
  @Module({
8
15
  imports: [
9
16
  ConfigModule.forRoot({
@@ -19,19 +26,35 @@ import { PaymentController } from './payment.controller';
19
26
  {
20
27
  provide: PaymentRegistry,
21
28
  useFactory: (cfg: ConfigService) => {
22
- const provider = cfg.get<string>('PAYMENT_PROVIDER') ?? 'paypal';
23
- if (provider === 'paypal') {
24
- return createPayment({
25
- strategies: {
26
- paypal: new PaypalStrategy({
27
- clientId: cfg.getOrThrow<string>('PAYPAL_CLIENT_ID'),
28
- secret: cfg.getOrThrow<string>('PAYPAL_CLIENT_SECRET'),
29
- environment: cfg.get<'sandbox' | 'live'>('PAYPAL_ENVIRONMENT') ?? 'sandbox',
30
- }),
31
- },
29
+ const logger = new Logger('PaymentRegistry');
30
+ const provider = (cfg.get<string>('PAYMENT_PROVIDER') ?? 'paypal').trim();
31
+ const keys = REQUIRED_ENV[provider];
32
+ if (!keys) throw new Error(`Unsupported PAYMENT_PROVIDER: ${provider}`);
33
+
34
+ const missing = missingEnv((k) => cfg.get<string>(k), keys);
35
+ if (missing.length > 0) {
36
+ const banner = formatEnvBanner({
37
+ service: 'payment MS',
38
+ provider,
39
+ missing,
40
+ envPath: ENV_PATH,
41
+ headline: `⚠ payment MS — ${provider} credentials missing (payments will fail)`,
32
42
  });
43
+ // Prod: fail fast. Dev: warn — PayPal is lazy, so the MS still boots
44
+ // and only payment calls fail until creds are set.
45
+ if (process.env.NODE_ENV === 'production') throw new Error(banner);
46
+ logger.warn(banner);
33
47
  }
34
- throw new Error(`Unsupported PAYMENT_PROVIDER: ${provider}`);
48
+
49
+ return createPayment({
50
+ strategies: {
51
+ paypal: new PaypalStrategy({
52
+ clientId: cfg.get<string>('PAYPAL_CLIENT_ID') ?? '',
53
+ secret: cfg.get<string>('PAYPAL_CLIENT_SECRET') ?? '',
54
+ environment: cfg.get<'sandbox' | 'live'>('PAYPAL_ENVIRONMENT') ?? 'sandbox',
55
+ }),
56
+ },
57
+ });
35
58
  },
36
59
  inject: [ConfigService],
37
60
  },
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "upload:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9231
54
55
  },
55
56
  "configurations": {
56
57
  "development": {