create-forgeon 0.3.15 → 0.3.17

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 (129) hide show
  1. package/package.json +4 -2
  2. package/src/cli/add-options.test.mjs +5 -2
  3. package/src/cli/options.test.mjs +1 -0
  4. package/src/cli/prompt-select.test.mjs +1 -0
  5. package/src/core/docs.test.mjs +80 -40
  6. package/src/core/scaffold.test.mjs +100 -0
  7. package/src/core/validate.test.mjs +1 -0
  8. package/src/modules/accounts.mjs +416 -0
  9. package/src/modules/db-prisma.mjs +23 -55
  10. package/src/modules/dependencies.test.mjs +71 -29
  11. package/src/modules/executor.mjs +3 -2
  12. package/src/modules/executor.test.mjs +631 -500
  13. package/src/modules/files-access.mjs +36 -105
  14. package/src/modules/files-image.mjs +35 -107
  15. package/src/modules/files-local.mjs +15 -6
  16. package/src/modules/files-quotas.mjs +75 -93
  17. package/src/modules/files-s3.mjs +17 -6
  18. package/src/modules/files.mjs +56 -125
  19. package/src/modules/i18n.mjs +17 -121
  20. package/src/modules/idempotency.test.mjs +180 -0
  21. package/src/modules/logger.mjs +0 -9
  22. package/src/modules/probes.test.mjs +204 -0
  23. package/src/modules/queue.mjs +325 -440
  24. package/src/modules/rate-limit.mjs +36 -76
  25. package/src/modules/rbac.mjs +39 -78
  26. package/src/modules/registry.mjs +22 -35
  27. package/src/modules/scheduler.mjs +51 -171
  28. package/src/modules/shared/files-runtime-wiring.mjs +81 -0
  29. package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
  30. package/src/modules/shared/patch-utils.mjs +29 -1
  31. package/src/modules/shared/probes.mjs +235 -0
  32. package/src/modules/sync-integrations.mjs +109 -396
  33. package/src/modules/sync-integrations.test.mjs +141 -0
  34. package/src/run-add-module.test.mjs +154 -0
  35. package/templates/base/README.md +7 -55
  36. package/templates/base/apps/web/src/App.tsx +70 -42
  37. package/templates/base/apps/web/src/probes.ts +61 -0
  38. package/templates/base/apps/web/src/styles.css +86 -25
  39. package/templates/base/package.json +21 -15
  40. package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -281
  41. package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
  42. package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
  43. package/templates/module-fragments/accounts/20_scope.md +29 -0
  44. package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
  45. package/templates/module-fragments/accounts/90_status_planned.md +7 -0
  46. package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
  47. package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
  48. package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
  49. package/templates/module-fragments/swagger/20_scope.md +2 -1
  50. package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
  51. package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
  52. package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
  53. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
  54. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
  55. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
  56. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
  57. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
  58. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
  59. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
  60. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
  61. package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
  62. package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
  63. package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
  64. package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
  65. package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
  66. package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
  67. package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
  68. package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
  69. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
  70. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
  71. package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
  72. package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
  73. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
  74. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
  75. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
  76. package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
  77. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
  78. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
  79. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
  80. package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
  81. package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
  82. package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
  83. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
  84. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
  85. package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
  86. package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
  87. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
  88. package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
  89. package/templates/module-presets/files/packages/files/package.json +1 -2
  90. package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
  91. package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
  92. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
  93. package/templates/module-presets/files/packages/files/src/index.ts +2 -1
  94. package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
  95. package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
  96. package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
  97. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
  98. package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
  99. package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
  100. package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
  101. package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
  102. package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
  103. package/src/modules/jwt-auth.mjs +0 -390
  104. package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
  105. package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
  106. package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
  107. package/templates/base/docs/AI/PROJECT.md +0 -43
  108. package/templates/base/docs/AI/ROADMAP.md +0 -171
  109. package/templates/base/docs/AI/TASKS.md +0 -60
  110. package/templates/base/docs/AI/VALIDATION.md +0 -31
  111. package/templates/base/docs/README.md +0 -18
  112. package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
  113. package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
  114. package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
  115. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
  116. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
  117. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
  118. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
  119. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
  120. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
  121. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
  122. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
  123. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
  124. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
  125. package/templates/module-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
  126. /package/templates/module-presets/{jwt-auth/packages/auth-api/src/jwt-auth.guard.ts → accounts/packages/accounts-api/src/access-token.guard.ts} +0 -0
  127. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
  128. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
  129. /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
@@ -1,4 +1,4 @@
1
- import { describe, it } from 'node:test';
1
+ import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
@@ -18,6 +18,21 @@ function createMinimalForgeonProject(targetRoot) {
18
18
  fs.writeFileSync(path.join(targetRoot, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n', 'utf8');
19
19
  }
20
20
 
21
+ function readFile(filePath) {
22
+ return fs.readFileSync(filePath, 'utf8');
23
+ }
24
+
25
+ function readWebProbes(projectRoot) {
26
+ return readFile(path.join(projectRoot, 'apps', 'web', 'src', 'probes.ts'));
27
+ }
28
+
29
+ function assertWebProbeShell(projectRoot) {
30
+ const appTsx = readFile(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'));
31
+ assert.match(appTsx, /id="probes"/);
32
+ assert.match(appTsx, /from '\.\/probes'/);
33
+ return appTsx;
34
+ }
35
+
21
36
  function assertDbPrismaWiring(projectRoot) {
22
37
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
23
38
  assert.match(appModule, /dbPrismaConfig/);
@@ -75,10 +90,12 @@ function assertRateLimitWiring(projectRoot) {
75
90
  assert.match(healthController, /@Get\('rate-limit'\)/);
76
91
  assert.match(healthController, /TOO_MANY_REQUESTS/);
77
92
 
78
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
93
+ const appTsx = assertWebProbeShell(projectRoot);
94
+ const probesTs = readWebProbes(projectRoot);
79
95
  assert.match(appTsx, /cache: 'no-store'/);
80
- assert.match(appTsx, /Check rate limit \(click repeatedly\)/);
81
- assert.match(appTsx, /Rate limit probe response/);
96
+ assert.match(probesTs, /"id": "rate-limit"/);
97
+ assert.match(probesTs, /"buttonLabel": "Check rate limit \(click repeatedly\)"/);
98
+ assert.match(probesTs, /"resultTitle": "Rate limit probe response"/);
82
99
 
83
100
  const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
84
101
  assert.match(readme, /## Rate Limit Module/);
@@ -109,9 +126,11 @@ function assertQueueWiring(projectRoot) {
109
126
  assert.match(healthController, /@Get\('queue'\)/);
110
127
  assert.match(healthController, /queueService\.getProbeStatus/);
111
128
 
112
- const webApp = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
113
- assert.match(webApp, /Check queue health/);
114
- assert.match(webApp, /Queue probe response/);
129
+ assertWebProbeShell(projectRoot);
130
+ const probesTs = readWebProbes(projectRoot);
131
+ assert.match(probesTs, /"id": "queue"/);
132
+ assert.match(probesTs, /"buttonLabel": "Check queue health"/);
133
+ assert.match(probesTs, /"resultTitle": "Queue probe response"/);
115
134
 
116
135
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
117
136
  assert.match(apiEnv, /QUEUE_ENABLED=true/);
@@ -156,9 +175,11 @@ function assertSchedulerWiring(projectRoot) {
156
175
  assert.match(healthController, /@Get\('scheduler'\)/);
157
176
  assert.match(healthController, /schedulerService\.getProbeStatus/);
158
177
 
159
- const webApp = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
160
- assert.match(webApp, /Check scheduler health/);
161
- assert.match(webApp, /Scheduler probe response/);
178
+ assertWebProbeShell(projectRoot);
179
+ const probesTs = readWebProbes(projectRoot);
180
+ assert.match(probesTs, /"id": "scheduler"/);
181
+ assert.match(probesTs, /"buttonLabel": "Check scheduler health"/);
182
+ assert.match(probesTs, /"resultTitle": "Scheduler probe response"/);
162
183
 
163
184
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
164
185
  assert.match(apiEnv, /SCHEDULER_ENABLED=true/);
@@ -200,169 +221,254 @@ function assertRbacWiring(projectRoot) {
200
221
  assert.match(healthController, /@Get\('rbac'\)/);
201
222
  assert.match(healthController, /@Permissions\('health\.rbac'\)/);
202
223
 
203
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
204
- assert.match(appTsx, /Check RBAC access/);
205
- assert.match(appTsx, /RBAC probe response/);
206
- assert.match(appTsx, /x-forgeon-permissions/);
224
+ assertWebProbeShell(projectRoot);
225
+ const probesTs = readWebProbes(projectRoot);
226
+ assert.match(probesTs, /"id": "rbac"/);
227
+ assert.match(probesTs, /"buttonLabel": "Check RBAC access"/);
228
+ assert.match(probesTs, /"resultTitle": "RBAC probe response"/);
229
+ assert.match(probesTs, /x-forgeon-permissions/);
207
230
 
208
231
  const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
209
232
  assert.match(readme, /## RBAC \/ Permissions Module/);
210
233
  assert.match(readme, /installs independently/i);
211
- assert.match(readme, /jwt-auth.*optional/i);
212
- }
213
-
214
- function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
215
- const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
216
- assert.match(appModule, /filesConfig/);
217
- assert.match(appModule, /filesEnvSchema/);
218
- assert.match(appModule, /ForgeonFilesModule/);
219
-
220
- const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
221
- assert.match(apiPackage, /@forgeon\/files/);
222
- assert.match(apiPackage, /pnpm --filter @forgeon\/files build/);
223
-
224
- const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
225
- assert.match(apiDockerfile, /COPY packages\/files\/package\.json packages\/files\/package\.json/);
226
- assert.match(apiDockerfile, /COPY packages\/files packages\/files/);
227
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files build/);
228
-
229
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
230
- assert.match(apiEnv, /FILES_ENABLED=true/);
231
- assert.match(apiEnv, new RegExp(`FILES_STORAGE_DRIVER=${expectedStorageDriver}`));
232
- assert.match(apiEnv, /FILES_PUBLIC_BASE_PATH=\/files/);
233
- assert.match(apiEnv, /FILES_MAX_FILE_SIZE_BYTES=10485760/);
234
- assert.match(apiEnv, /FILES_ALLOWED_MIME_PREFIXES=image\/,application\/pdf,text\//);
235
-
236
- const healthController = fs.readFileSync(
237
- path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
238
- 'utf8',
239
- );
240
- assert.match(healthController, /@Post\('files'\)/);
241
- assert.match(healthController, /@Get\('files-variants'\)/);
242
- assert.match(healthController, /filesService\.createProbeRecord/);
243
- assert.match(healthController, /filesService\.getVariantsProbeStatus/);
244
- assert.match(healthController, /filesService\.deleteByPublicId/);
245
-
246
- const filesController = fs.readFileSync(
247
- path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
248
- 'utf8',
249
- );
250
- assert.match(filesController, /@Query\('variant'\) variantQuery\?: string/);
251
- assert.match(filesController, /parseVariant\(variantQuery\)/);
252
- assert.match(filesController, /@Delete\(':publicId'\)/);
253
-
254
- const filesService = fs.readFileSync(
255
- path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
256
- 'utf8',
257
- );
258
- assert.match(filesService, /getOrCreateBlob/);
259
- assert.match(filesService, /cleanupReferencedBlobs/);
260
- assert.match(filesService, /isUniqueConstraintError/);
261
- assert.match(filesService, /fileBlob\.deleteMany/);
262
- assert.match(filesService, /variants:\s*\{[\s\S]*?none:\s*\{[\s\S]*?\}/);
263
- assert.match(filesService, /prisma\.fileBlob/);
264
-
265
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
266
- assert.match(appTsx, /Check files probe \(create metadata\)/);
267
- assert.match(appTsx, /Check files variants capability/);
268
- assert.match(appTsx, /Files probe response/);
269
- assert.match(appTsx, /Files variants probe response/);
270
-
271
- const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
272
- assert.match(schema, /model FileRecord \{/);
273
- assert.match(schema, /variants\s+FileVariant\[\]/);
274
- assert.match(schema, /model FileVariant \{/);
275
- assert.match(schema, /model FileBlob \{/);
276
- assert.match(schema, /blobId\s+String/);
277
- assert.match(schema, /@@unique\(\[hash,\s*size,\s*mimeType,\s*storageDriver\]\)/);
278
- assert.match(schema, /@@unique\(\[fileId,\s*variantKey\]\)/);
279
- assert.match(schema, /publicId\s+String\s+@unique/);
280
- assert.match(schema, /@@index\(\[ownerType,\s*ownerId,\s*createdAt\]\)/);
281
-
282
- const migration = path.join(
283
- projectRoot,
284
- 'apps',
285
- 'api',
286
- 'prisma',
287
- 'migrations',
288
- '20260306_files_file_record',
289
- 'migration.sql',
290
- );
291
- assert.equal(fs.existsSync(migration), true);
292
-
293
- const variantMigration = path.join(
294
- projectRoot,
295
- 'apps',
296
- 'api',
297
- 'prisma',
298
- 'migrations',
299
- '20260306_files_file_variant',
300
- 'migration.sql',
301
- );
302
- assert.equal(fs.existsSync(variantMigration), true);
303
- }
304
-
305
- function assertFilesLocalWiring(projectRoot) {
306
- const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
307
- assert.match(appModule, /filesLocalConfig/);
308
- assert.match(appModule, /filesLocalEnvSchemaZod/);
309
- assert.match(appModule, /FilesLocalConfigModule/);
310
-
311
- const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
312
- assert.match(apiPackage, /@forgeon\/files-local/);
313
- assert.match(apiPackage, /pnpm --filter @forgeon\/files-local build/);
314
-
315
- const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
316
- assert.match(apiDockerfile, /COPY packages\/files-local\/package\.json packages\/files-local\/package\.json/);
317
- assert.match(apiDockerfile, /COPY packages\/files-local packages\/files-local/);
318
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-local build/);
319
-
320
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
321
- assert.match(apiEnv, /FILES_LOCAL_ROOT=storage\/uploads/);
322
-
323
- const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8');
324
- assert.match(gitignore, /storage\//);
325
-
326
- const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
327
- assert.match(compose, /files_data:\/app\/storage/);
328
- assert.match(compose, /^\s{2}files_data:\s*$/m);
329
- }
330
-
331
- function assertFilesS3Wiring(projectRoot) {
332
- const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
333
- assert.match(appModule, /filesS3Config/);
334
- assert.match(appModule, /filesS3EnvSchemaZod/);
335
- assert.match(appModule, /FilesS3ConfigModule/);
336
-
337
- const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
338
- assert.match(apiPackage, /@forgeon\/files-s3/);
339
- assert.match(apiPackage, /pnpm --filter @forgeon\/files-s3 build/);
340
-
341
- const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
342
- assert.match(apiDockerfile, /COPY packages\/files-s3\/package\.json packages\/files-s3\/package\.json/);
343
- assert.match(apiDockerfile, /COPY packages\/files-s3 packages\/files-s3/);
344
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-s3 build/);
345
-
346
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
347
- assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
348
- assert.match(apiEnv, /FILES_S3_PROVIDER_PRESET=minio/);
349
- assert.match(apiEnv, /FILES_S3_BUCKET=forgeon-files/);
350
- assert.match(apiEnv, /FILES_S3_REGION=/);
351
- assert.match(apiEnv, /FILES_S3_ENDPOINT=/);
352
- assert.match(apiEnv, /FILES_S3_FORCE_PATH_STYLE=/);
353
- assert.match(apiEnv, /FILES_S3_MAX_ATTEMPTS=3/);
354
-
355
- const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
356
- assert.match(compose, /FILES_S3_PROVIDER_PRESET: \$\{FILES_S3_PROVIDER_PRESET\}/);
357
- assert.match(compose, /FILES_S3_MAX_ATTEMPTS: \$\{FILES_S3_MAX_ATTEMPTS\}/);
358
-
359
- const filesS3Package = fs.readFileSync(
360
- path.join(projectRoot, 'packages', 'files-s3', 'package.json'),
361
- 'utf8',
362
- );
363
- assert.match(filesS3Package, /@aws-sdk\/client-s3/);
234
+ assert.match(readme, /accounts.*optional/i);
364
235
  }
365
236
 
237
+ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
238
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
239
+ assert.match(appModule, /filesConfig/);
240
+ assert.match(appModule, /filesEnvSchema/);
241
+ assert.match(appModule, /ForgeonFilesModule\.register\(\{/);
242
+ assert.match(appModule, /ForgeonFilesDbPrismaModule/);
243
+ if (expectedStorageDriver === 's3') {
244
+ assert.match(appModule, /ForgeonFilesS3StorageModule/);
245
+ assert.doesNotMatch(appModule, /imports: \[ForgeonFilesDbPrismaModule, ForgeonFilesLocalStorageModule\]/);
246
+ } else {
247
+ assert.match(appModule, /ForgeonFilesLocalStorageModule/);
248
+ }
249
+
250
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
251
+ assert.match(apiPackage, /@forgeon\/files/);
252
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files build/);
253
+
254
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
255
+ assert.match(apiDockerfile, /COPY packages\/files\/package\.json packages\/files\/package\.json/);
256
+ assert.match(apiDockerfile, /COPY packages\/files packages\/files/);
257
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files build/);
258
+
259
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
260
+ assert.match(apiEnv, /FILES_ENABLED=true/);
261
+ assert.match(apiEnv, new RegExp('FILES_STORAGE_DRIVER=' + expectedStorageDriver));
262
+ assert.match(apiEnv, /FILES_PUBLIC_BASE_PATH=\/files/);
263
+ assert.match(apiEnv, /FILES_MAX_FILE_SIZE_BYTES=10485760/);
264
+ assert.match(apiEnv, /FILES_ALLOWED_MIME_PREFIXES=image\/,application\/pdf,text\//);
265
+
266
+ const healthController = fs.readFileSync(
267
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
268
+ 'utf8',
269
+ );
270
+ assert.match(healthController, /@Post\('files'\)/);
271
+ assert.match(healthController, /@Get\('files-variants'\)/);
272
+ assert.match(healthController, /filesService\.createProbeRecord/);
273
+ assert.match(healthController, /filesService\.getVariantsProbeStatus/);
274
+ assert.match(healthController, /filesService\.deleteByPublicId/);
275
+
276
+ const filesController = fs.readFileSync(
277
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
278
+ 'utf8',
279
+ );
280
+ assert.match(filesController, /@Query\('variant'\) variantQuery\?: string/);
281
+ assert.match(filesController, /parseVariant\(variantQuery\)/);
282
+ assert.match(filesController, /@Delete\(':publicId'\)/);
283
+
284
+ const filesService = fs.readFileSync(
285
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
286
+ 'utf8',
287
+ );
288
+ assert.match(filesService, /FILES_PERSISTENCE_PORT/);
289
+ assert.match(filesService, /FILES_STORAGE_ADAPTER/);
290
+ assert.match(filesService, /getOrCreateBlob/);
291
+ assert.match(filesService, /cleanupReferencedBlobs/);
292
+ assert.match(filesService, /isUniqueConstraintError/);
293
+ assert.match(filesService, /storageAdapter\.put/);
294
+ assert.match(filesService, /persistence\.createBlob/);
295
+ assert.match(filesService, /persistence\.deleteBlobIfUnreferenced/);
296
+ assert.doesNotMatch(filesService, /PrismaService/);
297
+ assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
298
+
299
+ const filesPorts = fs.readFileSync(
300
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.ports.ts'),
301
+ 'utf8',
302
+ );
303
+ assert.match(filesPorts, /FILES_PERSISTENCE_PORT/);
304
+ assert.match(filesPorts, /FILES_STORAGE_ADAPTER/);
305
+ assert.match(filesPorts, /interface FilesPersistencePort/);
306
+ assert.match(filesPorts, /interface FilesStorageAdapter/);
307
+
308
+ const filesModule = fs.readFileSync(
309
+ path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
310
+ 'utf8',
311
+ );
312
+ assert.match(filesModule, /ForgeonFilesModuleOptions/);
313
+ assert.match(filesModule, /static register\(options: ForgeonFilesModuleOptions = \{\}\)/);
314
+ assert.match(filesModule, /FILES_PERSISTENCE_PORT/);
315
+ assert.match(filesModule, /FILES_STORAGE_ADAPTER/);
316
+
317
+ const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
318
+ assert.doesNotMatch(filesPackage, /@forgeon\/db-prisma/);
319
+
320
+ const prismaFilesStore = fs.readFileSync(
321
+ path.join(projectRoot, 'apps', 'api', 'src', 'files', 'prisma-files-persistence.store.ts'),
322
+ 'utf8',
323
+ );
324
+ assert.match(prismaFilesStore, /PrismaService/);
325
+ assert.match(prismaFilesStore, /FILES_PERSISTENCE_PORT/);
326
+ assert.match(prismaFilesStore, /fileBlob\.deleteMany/);
327
+
328
+ const prismaFilesModule = fs.readFileSync(
329
+ path.join(projectRoot, 'apps', 'api', 'src', 'files', 'forgeon-files-db-prisma.module.ts'),
330
+ 'utf8',
331
+ );
332
+ assert.match(prismaFilesModule, /ForgeonFilesDbPrismaModule/);
333
+ assert.match(prismaFilesModule, /DbPrismaModule/);
334
+ assert.match(prismaFilesModule, /FILES_PERSISTENCE_PORT/);
335
+
336
+ assertWebProbeShell(projectRoot);
337
+ const probesTs = readWebProbes(projectRoot);
338
+ assert.match(probesTs, /"id": "files"/);
339
+ assert.match(probesTs, /"buttonLabel": "Check files probe \(create metadata\)"/);
340
+ assert.match(probesTs, /"resultTitle": "Files probe response"/);
341
+ assert.match(probesTs, /"id": "files-variants"/);
342
+ assert.match(probesTs, /"buttonLabel": "Check files variants capability"/);
343
+ assert.match(probesTs, /"resultTitle": "Files variants probe response"/);
344
+
345
+ const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
346
+ assert.match(schema, /model FileRecord \{/);
347
+ assert.match(schema, /variants\s+FileVariant\[\]/);
348
+ assert.match(schema, /model FileVariant \{/);
349
+ assert.match(schema, /model FileBlob \{/);
350
+ assert.match(schema, /blobId\s+String/);
351
+ assert.match(schema, /@@unique\(\[hash,\s*size,\s*mimeType,\s*storageDriver\]\)/);
352
+ assert.match(schema, /@@unique\(\[fileId,\s*variantKey\]\)/);
353
+ assert.match(schema, /publicId\s+String\s+@unique/);
354
+ assert.match(schema, /@@index\(\[ownerType,\s*ownerId,\s*createdAt\]\)/);
355
+
356
+ const migration = path.join(
357
+ projectRoot,
358
+ 'apps',
359
+ 'api',
360
+ 'prisma',
361
+ 'migrations',
362
+ '20260306_files_file_record',
363
+ 'migration.sql',
364
+ );
365
+ assert.equal(fs.existsSync(migration), true);
366
+
367
+ const variantMigration = path.join(
368
+ projectRoot,
369
+ 'apps',
370
+ 'api',
371
+ 'prisma',
372
+ 'migrations',
373
+ '20260306_files_file_variant',
374
+ 'migration.sql',
375
+ );
376
+ assert.equal(fs.existsSync(variantMigration), true);
377
+ }
378
+
379
+ function assertFilesLocalWiring(projectRoot) {
380
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
381
+ assert.match(appModule, /filesLocalConfig/);
382
+ assert.match(appModule, /filesLocalEnvSchemaZod/);
383
+ assert.match(appModule, /FilesLocalConfigModule/);
384
+ assert.match(appModule, /ForgeonFilesLocalStorageModule/);
385
+
386
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
387
+ assert.match(apiPackage, /@forgeon\/files-local/);
388
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-local build/);
389
+
390
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
391
+ assert.match(apiDockerfile, /COPY packages\/files-local\/package\.json packages\/files-local\/package\.json/);
392
+ assert.match(apiDockerfile, /COPY packages\/files-local packages\/files-local/);
393
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-local build/);
394
+
395
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
396
+ assert.match(apiEnv, /FILES_LOCAL_ROOT=storage\/uploads/);
397
+
398
+ const localModule = fs.readFileSync(
399
+ path.join(projectRoot, 'packages', 'files-local', 'src', 'forgeon-files-local-storage.module.ts'),
400
+ 'utf8',
401
+ );
402
+ assert.match(localModule, /ForgeonFilesLocalStorageModule/);
403
+ assert.match(localModule, /FORGEON_FILES_STORAGE_ADAPTER/);
404
+
405
+ const localAdapter = fs.readFileSync(
406
+ path.join(projectRoot, 'packages', 'files-local', 'src', 'local-files-storage.adapter.ts'),
407
+ 'utf8',
408
+ );
409
+ assert.match(localAdapter, /readonly driver = 'local'/);
410
+ assert.match(localAdapter, /createReadStream/);
411
+ assert.match(localAdapter, /writeFile/);
412
+
413
+ const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8');
414
+ assert.match(gitignore, /storage\//);
415
+
416
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
417
+ assert.match(compose, /files_data:\/app\/storage/);
418
+ assert.match(compose, /^\s{2}files_data:\s*$/m);
419
+ }
420
+
421
+ function assertFilesS3Wiring(projectRoot) {
422
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
423
+ assert.match(appModule, /filesS3Config/);
424
+ assert.match(appModule, /filesS3EnvSchemaZod/);
425
+ assert.match(appModule, /FilesS3ConfigModule/);
426
+ assert.match(appModule, /ForgeonFilesS3StorageModule/);
427
+
428
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
429
+ assert.match(apiPackage, /@forgeon\/files-s3/);
430
+ assert.match(apiPackage, /pnpm --filter @forgeon\/files-s3 build/);
431
+
432
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
433
+ assert.match(apiDockerfile, /COPY packages\/files-s3\/package\.json packages\/files-s3\/package\.json/);
434
+ assert.match(apiDockerfile, /COPY packages\/files-s3 packages\/files-s3/);
435
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-s3 build/);
436
+
437
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
438
+ assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
439
+ assert.match(apiEnv, /FILES_S3_PROVIDER_PRESET=minio/);
440
+ assert.match(apiEnv, /FILES_S3_BUCKET=forgeon-files/);
441
+ assert.match(apiEnv, /FILES_S3_REGION=/);
442
+ assert.match(apiEnv, /FILES_S3_ENDPOINT=/);
443
+ assert.match(apiEnv, /FILES_S3_FORCE_PATH_STYLE=/);
444
+ assert.match(apiEnv, /FILES_S3_MAX_ATTEMPTS=3/);
445
+
446
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
447
+ assert.match(compose, /FILES_S3_PROVIDER_PRESET: \$\{FILES_S3_PROVIDER_PRESET\}/);
448
+ assert.match(compose, /FILES_S3_MAX_ATTEMPTS: \$\{FILES_S3_MAX_ATTEMPTS\}/);
449
+
450
+ const filesS3Package = fs.readFileSync(
451
+ path.join(projectRoot, 'packages', 'files-s3', 'package.json'),
452
+ 'utf8',
453
+ );
454
+ assert.match(filesS3Package, /@aws-sdk\/client-s3/);
455
+
456
+ const s3Module = fs.readFileSync(
457
+ path.join(projectRoot, 'packages', 'files-s3', 'src', 'forgeon-files-s3-storage.module.ts'),
458
+ 'utf8',
459
+ );
460
+ assert.match(s3Module, /ForgeonFilesS3StorageModule/);
461
+ assert.match(s3Module, /FORGEON_FILES_STORAGE_ADAPTER/);
462
+
463
+ const s3Adapter = fs.readFileSync(
464
+ path.join(projectRoot, 'packages', 'files-s3', 'src', 's3-files-storage.adapter.ts'),
465
+ 'utf8',
466
+ );
467
+ assert.match(s3Adapter, /readonly driver = 's3'/);
468
+ assert.match(s3Adapter, /@aws-sdk\/client-s3/);
469
+ assert.match(s3Adapter, /loadS3Module/);
470
+ }
471
+
366
472
  function assertFilesAccessWiring(projectRoot) {
367
473
  const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
368
474
  assert.match(appModule, /ForgeonFilesAccessModule/);
@@ -410,10 +516,12 @@ function assertFilesAccessWiring(projectRoot) {
410
516
  assert.match(healthController, /extractFilesAccessSubject/);
411
517
  assert.match(healthController, /filesAccessService\.canRead/);
412
518
 
413
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
414
- assert.match(appTsx, /Check files access/);
415
- assert.match(appTsx, /Files access probe response/);
416
- assert.match(appTsx, /x-forgeon-user-id/);
519
+ assertWebProbeShell(projectRoot);
520
+ const probesTs = readWebProbes(projectRoot);
521
+ assert.match(probesTs, /"id": "files-access"/);
522
+ assert.match(probesTs, /"buttonLabel": "Check files access"/);
523
+ assert.match(probesTs, /"resultTitle": "Files access probe response"/);
524
+ assert.match(probesTs, /x-forgeon-user-id/);
417
525
 
418
526
  const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
419
527
  assert.match(readme, /## Files Access Module/);
@@ -436,7 +544,8 @@ function assertFilesQuotasWiring(projectRoot) {
436
544
  );
437
545
 
438
546
  const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
439
- assert.match(filesPackage, /@forgeon\/files-quotas/);
547
+ assert.doesNotMatch(filesPackage, /@forgeon\/files-quotas/);
548
+ assert.match(filesPackage, /@nestjs\/core/);
440
549
 
441
550
  const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
442
551
  assert.match(
@@ -455,7 +564,9 @@ function assertFilesQuotasWiring(projectRoot) {
455
564
  path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
456
565
  'utf8',
457
566
  );
458
- assert.match(filesController, /FilesQuotasService/);
567
+ assert.match(filesController, /ModuleRef/);
568
+ assert.match(filesController, /FORGEON_FILES_UPLOAD_QUOTA_SERVICE/);
569
+ assert.match(filesController, /getFilesUploadQuotaService/);
459
570
  assert.match(filesController, /filesQuotasService\.assertUploadAllowed/);
460
571
 
461
572
  const healthController = fs.readFileSync(
@@ -465,9 +576,11 @@ function assertFilesQuotasWiring(projectRoot) {
465
576
  assert.match(healthController, /@Get\('files-quotas'\)/);
466
577
  assert.match(healthController, /filesQuotasService\.getProbeStatus/);
467
578
 
468
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
469
- assert.match(appTsx, /Check files quotas/);
470
- assert.match(appTsx, /Files quotas probe response/);
579
+ assertWebProbeShell(projectRoot);
580
+ const probesTs = readWebProbes(projectRoot);
581
+ assert.match(probesTs, /"id": "files-quotas"/);
582
+ assert.match(probesTs, /"buttonLabel": "Check files quotas"/);
583
+ assert.match(probesTs, /"resultTitle": "Files quotas probe response"/);
471
584
 
472
585
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
473
586
  assert.match(apiEnv, /FILES_QUOTAS_ENABLED=true/);
@@ -552,9 +665,11 @@ function assertFilesImageWiring(projectRoot) {
552
665
  assert.match(healthController, /@Get\('files-image'\)/);
553
666
  assert.match(healthController, /filesImageService\.getProbeStatus/);
554
667
 
555
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
556
- assert.match(appTsx, /Check files image sanitize/);
557
- assert.match(appTsx, /Files image probe response/);
668
+ assertWebProbeShell(projectRoot);
669
+ const probesTs = readWebProbes(projectRoot);
670
+ assert.match(probesTs, /"id": "files-image"/);
671
+ assert.match(probesTs, /"buttonLabel": "Check files image sanitize"/);
672
+ assert.match(probesTs, /"resultTitle": "Files image probe response"/);
558
673
 
559
674
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
560
675
  assert.match(apiEnv, /FILES_IMAGE_ENABLED=true/);
@@ -591,68 +706,78 @@ function assertFilesImageWiring(projectRoot) {
591
706
  assert.match(readme, /metadata is stripped before storage/i);
592
707
  }
593
708
 
594
- function assertJwtAuthWiring(projectRoot, withPrismaStore) {
595
- const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
596
- assert.match(apiPackage, /@forgeon\/auth-api/);
597
- assert.match(apiPackage, /@forgeon\/auth-contracts/);
598
- assert.match(apiPackage, /pnpm --filter @forgeon\/auth-contracts build/);
599
- assert.match(apiPackage, /pnpm --filter @forgeon\/auth-api build/);
600
-
601
- const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
602
- assert.match(appModule, /authConfig/);
603
- assert.match(appModule, /authEnvSchema/);
604
- assert.match(appModule, /ForgeonAuthModule\.register\(/);
605
- if (withPrismaStore) {
606
- assert.match(appModule, /AUTH_REFRESH_TOKEN_STORE/);
607
- assert.match(appModule, /PrismaAuthRefreshTokenStore/);
608
- } else {
609
- assert.doesNotMatch(appModule, /PrismaAuthRefreshTokenStore/);
610
- }
611
-
612
- const healthController = fs.readFileSync(
613
- path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
614
- 'utf8',
615
- );
616
- assert.match(healthController, /@Get\('auth'\)/);
617
- assert.match(healthController, /authService\.getProbeStatus/);
618
- assert.doesNotMatch(healthController, /,\s*,/);
619
-
620
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
621
- assert.match(appTsx, /Check JWT auth probe/);
622
- assert.match(appTsx, /Auth probe response/);
623
-
624
- const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
625
- assert.match(
626
- apiDockerfile,
627
- /COPY packages\/auth-contracts\/package\.json packages\/auth-contracts\/package\.json/,
628
- );
629
- assert.match(apiDockerfile, /COPY packages\/auth-api\/package\.json packages\/auth-api\/package\.json/);
630
- assert.match(apiDockerfile, /COPY packages\/auth-contracts packages\/auth-contracts/);
631
- assert.match(apiDockerfile, /COPY packages\/auth-api packages\/auth-api/);
632
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/auth-contracts build/);
633
- assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/auth-api build/);
634
-
635
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
636
- assert.match(apiEnv, /JWT_ACCESS_SECRET=/);
637
- assert.match(apiEnv, /JWT_REFRESH_SECRET=/);
638
- assert.match(apiEnv, /AUTH_DEMO_EMAIL=/);
639
- assert.match(apiEnv, /AUTH_DEMO_PASSWORD=/);
640
-
641
- const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
642
- assert.match(compose, /JWT_ACCESS_SECRET: \$\{JWT_ACCESS_SECRET\}/);
643
- assert.match(compose, /JWT_REFRESH_SECRET: \$\{JWT_REFRESH_SECRET\}/);
644
-
645
- const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
646
- assert.match(readme, /## JWT Auth Module/);
647
-
648
- const authServiceSource = fs.readFileSync(
649
- path.join(projectRoot, 'packages', 'auth-api', 'src', 'auth.service.ts'),
650
- 'utf8',
651
- );
652
- assert.match(authServiceSource, /import type \{/);
653
- assert.doesNotMatch(authServiceSource, /import\s*\{\s*AUTH_ERROR_CODES/);
654
- }
655
-
709
+ function assertAccountsWiring(projectRoot) {
710
+ const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
711
+ assert.match(apiPackage, /@forgeon\/accounts-api/);
712
+ assert.match(apiPackage, /@forgeon\/accounts-contracts/);
713
+ assert.match(apiPackage, /pnpm --filter @forgeon\/accounts-contracts build/);
714
+ assert.match(apiPackage, /pnpm --filter @forgeon\/accounts-api build/);
715
+
716
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
717
+ assert.match(appModule, /authConfig/);
718
+ assert.match(appModule, /authEnvSchema/);
719
+ assert.match(appModule, /ForgeonAccountsDbPrismaModule/);
720
+ assert.match(appModule, /ForgeonAccountsModule\.register\(\{/);
721
+ assert.match(appModule, /UsersModule\.register\(\{\}\)/);
722
+ assert.doesNotMatch(appModule, /AUTH_REFRESH_TOKEN_STORE/);
723
+
724
+ const healthController = fs.readFileSync(
725
+ path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
726
+ 'utf8',
727
+ );
728
+ assert.match(healthController, /@Get\('auth'\)/);
729
+ assert.match(healthController, /authService\.getProbeStatus/);
730
+ assert.doesNotMatch(healthController, /,\s*,/);
731
+
732
+ assertWebProbeShell(projectRoot);
733
+ const probesTs = readWebProbes(projectRoot);
734
+ assert.match(probesTs, /"id": "auth"/);
735
+ assert.match(probesTs, /"buttonLabel": "Check accounts probe"/);
736
+ assert.match(probesTs, /"resultTitle": "Accounts probe response"/);
737
+
738
+ const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
739
+ assert.match(
740
+ apiDockerfile,
741
+ /COPY packages\/accounts-contracts\/package\.json packages\/accounts-contracts\/package\.json/,
742
+ );
743
+ assert.match(apiDockerfile, /COPY packages\/accounts-api\/package\.json packages\/accounts-api\/package\.json/);
744
+ assert.match(apiDockerfile, /COPY packages\/accounts-contracts packages\/accounts-contracts/);
745
+ assert.match(apiDockerfile, /COPY packages\/accounts-api packages\/accounts-api/);
746
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/accounts-contracts build/);
747
+ assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/accounts-api build/);
748
+
749
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
750
+ assert.match(apiEnv, /JWT_ACCESS_SECRET=/);
751
+ assert.match(apiEnv, /JWT_REFRESH_SECRET=/);
752
+ assert.match(apiEnv, /AUTH_ARGON2_MEMORY_COST=/);
753
+ assert.match(apiEnv, /AUTH_ARGON2_PARALLELISM=/);
754
+
755
+ const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
756
+ assert.match(compose, /JWT_ACCESS_SECRET: \$\{JWT_ACCESS_SECRET\}/);
757
+ assert.match(compose, /JWT_REFRESH_SECRET: \$\{JWT_REFRESH_SECRET\}/);
758
+ assert.match(compose, /AUTH_ARGON2_MEMORY_COST: \$\{AUTH_ARGON2_MEMORY_COST\}/);
759
+
760
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
761
+ assert.match(readme, /## Accounts Module/);
762
+ assert.match(readme, /owner-scoped user routes/);
763
+ assert.match(readme, /AccountsEmailPort/);
764
+
765
+ const authServiceSource = fs.readFileSync(
766
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.service.ts'),
767
+ 'utf8',
768
+ );
769
+ assert.match(authServiceSource, /import type \{ RegisterRequest \} from '@forgeon\/accounts-contracts';/);
770
+
771
+ const prismaStorePath = path.join(
772
+ projectRoot,
773
+ 'apps',
774
+ 'api',
775
+ 'src',
776
+ 'accounts',
777
+ 'prisma-accounts-persistence.store.ts',
778
+ );
779
+ assert.equal(fs.existsSync(prismaStorePath), true);
780
+ }
656
781
  function stripDbPrismaArtifacts(projectRoot) {
657
782
  const dbPackageDir = path.join(projectRoot, 'packages', 'db-prisma');
658
783
  if (fs.existsSync(dbPackageDir)) {
@@ -1188,6 +1313,16 @@ describe('addModule', () => {
1188
1313
  assert.match(loggerModule, /ForgeonHttpLoggingMiddleware/);
1189
1314
  assert.match(loggerModule, /consumer\.apply\(RequestIdMiddleware, ForgeonHttpLoggingMiddleware\)\.forRoutes\('\*'\);/);
1190
1315
 
1316
+ const loggerIndex = fs.readFileSync(
1317
+ path.join(projectRoot, 'packages', 'logger', 'src', 'index.ts'),
1318
+ 'utf8',
1319
+ );
1320
+ assert.doesNotMatch(loggerIndex, /http-logging\.interceptor/);
1321
+ assert.equal(
1322
+ fs.existsSync(path.join(projectRoot, 'packages', 'logger', 'src', 'http-logging.interceptor.ts')),
1323
+ false,
1324
+ );
1325
+
1191
1326
  const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
1192
1327
  assert.match(apiEnv, /LOGGER_LEVEL=log/);
1193
1328
  assert.match(apiEnv, /LOGGER_HTTP_ENABLED=true/);
@@ -1405,17 +1540,21 @@ describe('addModule', () => {
1405
1540
  packageRoot,
1406
1541
  });
1407
1542
 
1408
- const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
1409
- assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
1410
-
1411
- const filesService = fs.readFileSync(
1412
- path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
1413
- 'utf8',
1414
- );
1415
- assert.match(filesService, /storeS3/);
1416
- assert.match(filesService, /openS3/);
1417
- assert.match(filesService, /deleteS3/);
1418
- assert.match(filesService, /@aws-sdk\/client-s3/);
1543
+ const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
1544
+ assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
1545
+
1546
+ const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
1547
+ assert.match(appModule, /ForgeonFilesS3StorageModule/);
1548
+ assert.doesNotMatch(appModule, /imports: \[ForgeonFilesDbPrismaModule, ForgeonFilesLocalStorageModule\]/);
1549
+
1550
+ const filesService = fs.readFileSync(
1551
+ path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
1552
+ 'utf8',
1553
+ );
1554
+ assert.doesNotMatch(filesService, /storeS3/);
1555
+ assert.doesNotMatch(filesService, /openS3/);
1556
+ assert.doesNotMatch(filesService, /deleteS3/);
1557
+ assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
1419
1558
  } finally {
1420
1559
  fs.rmSync(targetRoot, { recursive: true, force: true });
1421
1560
  }
@@ -1604,8 +1743,9 @@ describe('addModule', () => {
1604
1743
  assert.match(healthController, /@Get\('files-access'\)/);
1605
1744
  assert.match(healthController, /@Get\('files-quotas'\)/);
1606
1745
 
1607
- const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
1608
- const filesChecks = appTsx.match(/Check files /g) ?? [];
1746
+ assertWebProbeShell(projectRoot);
1747
+ const probesTs = readWebProbes(projectRoot);
1748
+ const filesChecks = probesTs.match(/"buttonLabel": "Check files /g) ?? [];
1609
1749
  assert.equal(filesChecks.length, 5);
1610
1750
  } finally {
1611
1751
  fs.rmSync(targetRoot, { recursive: true, force: true });
@@ -1925,6 +2065,51 @@ describe('addModule', () => {
1925
2065
  }
1926
2066
  });
1927
2067
 
2068
+
2069
+ it('applies i18n after probe modules and preserves managed probe registry entries', () => {
2070
+ const targetRoot = mkTmp('forgeon-module-i18n-probes-');
2071
+ const projectRoot = path.join(targetRoot, 'demo-i18n-probes');
2072
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
2073
+
2074
+ try {
2075
+ scaffoldProject({
2076
+ templateRoot,
2077
+ packageRoot,
2078
+ targetRoot: projectRoot,
2079
+ projectName: 'demo-i18n-probes',
2080
+ frontend: 'react',
2081
+ db: 'prisma',
2082
+ dbPrismaEnabled: true,
2083
+ i18nEnabled: false,
2084
+ proxy: 'caddy',
2085
+ });
2086
+
2087
+ addModule({ moduleId: 'rate-limit', targetRoot: projectRoot, packageRoot });
2088
+ addModule({ moduleId: 'rbac', targetRoot: projectRoot, packageRoot });
2089
+
2090
+ const probesBeforeI18n = readWebProbes(projectRoot);
2091
+ assert.match(probesBeforeI18n, /"id": "rate-limit"/);
2092
+ assert.match(probesBeforeI18n, /"id": "rbac"/);
2093
+
2094
+ const i18nResult = addModule({
2095
+ moduleId: 'i18n',
2096
+ targetRoot: projectRoot,
2097
+ packageRoot,
2098
+ });
2099
+ assert.equal(i18nResult.applied, true);
2100
+
2101
+ const probesAfterI18n = readWebProbes(projectRoot);
2102
+ assert.match(probesAfterI18n, /"id": "rate-limit"/);
2103
+ assert.match(probesAfterI18n, /"id": "rbac"/);
2104
+
2105
+ const rateLimitIndex = probesAfterI18n.indexOf('"id": "rate-limit"');
2106
+ const rbacIndex = probesAfterI18n.indexOf('"id": "rbac"');
2107
+ assert.equal(rbacIndex >= 0 && rateLimitIndex > rbacIndex, true);
2108
+ } finally {
2109
+ fs.rmSync(targetRoot, { recursive: true, force: true });
2110
+ }
2111
+ });
2112
+
1928
2113
  it('applies swagger -> logger -> i18n and keeps all module wiring', () => {
1929
2114
  const targetRoot = mkTmp('forgeon-module-mixed-order-');
1930
2115
  const projectRoot = path.join(targetRoot, 'demo-mixed-order');
@@ -1979,239 +2164,175 @@ describe('addModule', () => {
1979
2164
  }
1980
2165
  });
1981
2166
 
1982
- it('applies jwt-auth with db-prisma as stateless first, then wires persistence via explicit sync', () => {
1983
- const targetRoot = mkTmp('forgeon-module-jwt-db-');
1984
- const projectRoot = path.join(targetRoot, 'demo-jwt-db');
1985
- const templateRoot = path.join(packageRoot, 'templates', 'base');
1986
-
1987
- try {
1988
- scaffoldProject({
1989
- templateRoot,
1990
- packageRoot,
1991
- targetRoot: projectRoot,
1992
- projectName: 'demo-jwt-db',
1993
- frontend: 'react',
1994
- db: 'prisma',
1995
- dbPrismaEnabled: true,
1996
- i18nEnabled: true,
1997
- proxy: 'caddy',
1998
- });
1999
-
2000
- const result = addModule({
2001
- moduleId: 'jwt-auth',
2002
- targetRoot: projectRoot,
2003
- packageRoot,
2004
- });
2005
-
2006
- assert.equal(result.applied, true);
2007
- assertJwtAuthWiring(projectRoot, false);
2008
-
2009
- const syncResult = syncIntegrations({ targetRoot: projectRoot, packageRoot });
2010
- const dbPair = syncResult.summary.find((item) => item.id === 'auth-persistence');
2011
- assert.ok(dbPair);
2012
- assert.equal(dbPair.result.applied, true);
2013
- assert.equal(syncResult.changedFiles.length > 0, true);
2014
-
2015
- assertJwtAuthWiring(projectRoot, true);
2016
-
2017
- const storeFile = path.join(
2018
- projectRoot,
2019
- 'apps',
2020
- 'api',
2021
- 'src',
2022
- 'auth',
2023
- 'prisma-auth-refresh-token.store.ts',
2024
- );
2025
- assert.equal(fs.existsSync(storeFile), true);
2026
-
2027
- const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
2028
- assert.match(schema, /refreshTokenHash/);
2029
-
2030
- const migrationPath = path.join(
2031
- projectRoot,
2032
- 'apps',
2033
- 'api',
2034
- 'prisma',
2035
- 'migrations',
2036
- '0002_auth_refresh_token_hash',
2037
- 'migration.sql',
2038
- );
2039
- assert.equal(fs.existsSync(migrationPath), true);
2040
-
2041
- const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
2042
- assert.match(readme, /refresh token persistence: enabled/);
2043
- assert.match(readme, /db-adapter/);
2044
- assert.match(readme, /current provider: `db-prisma`/);
2045
- assert.match(readme, /0002_auth_refresh_token_hash/);
2046
-
2047
- const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
2048
- assert.match(moduleDoc, /Status: implemented/);
2049
- assert.match(moduleDoc, /db-adapter/);
2050
- } finally {
2051
- fs.rmSync(targetRoot, { recursive: true, force: true });
2052
- }
2053
- });
2054
-
2055
- it('applies jwt-auth without db and keeps stateless fallback until pair sync is available', () => {
2056
- const targetRoot = mkTmp('forgeon-module-jwt-nodb-');
2057
- const projectRoot = path.join(targetRoot, 'demo-jwt-nodb');
2058
- const templateRoot = path.join(packageRoot, 'templates', 'base');
2059
-
2060
- try {
2061
- scaffoldProject({
2062
- templateRoot,
2063
- packageRoot,
2064
- targetRoot: projectRoot,
2065
- projectName: 'demo-jwt-nodb',
2066
- frontend: 'react',
2067
- db: 'prisma',
2068
- dbPrismaEnabled: true,
2069
- i18nEnabled: false,
2070
- proxy: 'caddy',
2071
- });
2072
-
2073
- stripDbPrismaArtifacts(projectRoot);
2074
-
2075
- const result = addModule({
2076
- moduleId: 'jwt-auth',
2077
- targetRoot: projectRoot,
2078
- packageRoot,
2079
- });
2080
-
2081
- assert.equal(result.applied, true);
2082
- assertJwtAuthWiring(projectRoot, false);
2083
- assert.equal(
2084
- fs.existsSync(path.join(projectRoot, 'apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts')),
2085
- false,
2086
- );
2087
-
2088
- const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
2089
- assert.match(readme, /refresh token persistence: disabled/);
2090
- assert.match(readme, /db-adapter/);
2091
- assert.match(readme, /create-forgeon add db-prisma/);
2092
-
2093
- } finally {
2094
- fs.rmSync(targetRoot, { recursive: true, force: true });
2095
- }
2096
- });
2097
-
2098
- it('detects and applies jwt-auth + rbac claims integration explicitly', () => {
2099
- const targetRoot = mkTmp('forgeon-module-jwt-rbac-');
2100
- const projectRoot = path.join(targetRoot, 'demo-jwt-rbac');
2101
- const templateRoot = path.join(packageRoot, 'templates', 'base');
2102
-
2103
- try {
2104
- scaffoldProject({
2105
- templateRoot,
2106
- packageRoot,
2107
- targetRoot: projectRoot,
2108
- projectName: 'demo-jwt-rbac',
2109
- frontend: 'react',
2110
- db: 'prisma',
2111
- dbPrismaEnabled: false,
2112
- i18nEnabled: false,
2113
- proxy: 'caddy',
2114
- });
2115
-
2116
- addModule({
2117
- moduleId: 'rbac',
2118
- targetRoot: projectRoot,
2119
- packageRoot,
2120
- });
2121
- addModule({
2122
- moduleId: 'jwt-auth',
2123
- targetRoot: projectRoot,
2124
- packageRoot,
2125
- });
2126
-
2127
- const scan = scanIntegrations({
2128
- targetRoot: projectRoot,
2129
- relatedModuleId: 'jwt-auth',
2130
- });
2131
- assert.equal(scan.groups.some((group) => group.id === 'auth-rbac-claims'), true);
2132
-
2133
- const syncResult = syncIntegrations({
2134
- targetRoot: projectRoot,
2135
- packageRoot,
2136
- groupIds: ['auth-rbac-claims'],
2137
- });
2138
- const claimsPair = syncResult.summary.find((item) => item.id === 'auth-rbac-claims');
2139
- assert.ok(claimsPair);
2140
- assert.equal(claimsPair.result.applied, true);
2141
-
2142
- const authContracts = fs.readFileSync(
2143
- path.join(projectRoot, 'packages', 'auth-contracts', 'src', 'index.ts'),
2144
- 'utf8',
2145
- );
2146
- assert.match(authContracts, /permissions\?: string\[\];/);
2147
-
2148
- const authService = fs.readFileSync(
2149
- path.join(projectRoot, 'packages', 'auth-api', 'src', 'auth.service.ts'),
2150
- 'utf8',
2151
- );
2152
- assert.match(authService, /permissions: \['health\.rbac'\]/);
2153
- assert.match(authService, /permissions: user\.permissions,/);
2154
- assert.match(
2155
- authService,
2156
- /permissions: Array\.isArray\(payload\.permissions\) \? payload\.permissions : \[\],/,
2157
- );
2158
-
2159
- const authController = fs.readFileSync(
2160
- path.join(projectRoot, 'packages', 'auth-api', 'src', 'auth.controller.ts'),
2161
- 'utf8',
2162
- );
2163
- assert.match(
2164
- authController,
2165
- /permissions: Array\.isArray\(payload\.permissions\) \? payload\.permissions : \[\],/,
2166
- );
2167
- } finally {
2168
- fs.rmSync(targetRoot, { recursive: true, force: true });
2169
- }
2170
- });
2171
-
2172
- it('scans auth persistence as db-adapter participant while remaining triggerable from db-prisma install order', () => {
2173
- const targetRoot = mkTmp('forgeon-module-jwt-db-scan-');
2174
- const projectRoot = path.join(targetRoot, 'demo-jwt-db-scan');
2175
- const templateRoot = path.join(packageRoot, 'templates', 'base');
2176
-
2177
- try {
2178
- scaffoldProject({
2179
- templateRoot,
2180
- packageRoot,
2181
- targetRoot: projectRoot,
2182
- projectName: 'demo-jwt-db-scan',
2183
- frontend: 'react',
2184
- db: 'prisma',
2185
- dbPrismaEnabled: false,
2186
- i18nEnabled: false,
2187
- proxy: 'caddy',
2188
- });
2189
-
2190
- addModule({
2191
- moduleId: 'jwt-auth',
2192
- targetRoot: projectRoot,
2193
- packageRoot,
2194
- });
2195
- addModule({
2196
- moduleId: 'db-prisma',
2197
- targetRoot: projectRoot,
2198
- packageRoot,
2199
- });
2200
-
2201
- const scan = scanIntegrations({
2202
- targetRoot: projectRoot,
2203
- relatedModuleId: 'db-prisma',
2204
- });
2205
- const persistenceGroup = scan.groups.find((group) => group.id === 'auth-persistence');
2206
-
2207
- assert.ok(persistenceGroup);
2208
- assert.deepEqual(persistenceGroup.modules, ['jwt-auth', 'db-adapter']);
2209
- } finally {
2210
- fs.rmSync(targetRoot, { recursive: true, force: true });
2211
- }
2212
- });
2213
-
2214
- it('applies logger then jwt-auth on db/i18n-disabled scaffold without breaking health controller syntax', () => {
2167
+ it('applies accounts with db-prisma and wires the DB-backed runtime immediately', () => {
2168
+ const targetRoot = mkTmp('forgeon-module-accounts-db-');
2169
+ const projectRoot = path.join(targetRoot, 'demo-accounts-db');
2170
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
2171
+
2172
+ try {
2173
+ scaffoldProject({
2174
+ templateRoot,
2175
+ packageRoot,
2176
+ targetRoot: projectRoot,
2177
+ projectName: 'demo-accounts-db',
2178
+ frontend: 'react',
2179
+ db: 'prisma',
2180
+ dbPrismaEnabled: true,
2181
+ i18nEnabled: true,
2182
+ proxy: 'caddy',
2183
+ });
2184
+
2185
+ const result = addModule({
2186
+ moduleId: 'accounts',
2187
+ targetRoot: projectRoot,
2188
+ packageRoot,
2189
+ });
2190
+
2191
+ assert.equal(result.applied, true);
2192
+ assertAccountsWiring(projectRoot);
2193
+
2194
+ const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
2195
+ assert.match(schema, /model UserProfile/);
2196
+ assert.match(schema, /model UserSettings/);
2197
+ assert.match(schema, /model AuthIdentity/);
2198
+ assert.match(schema, /model AuthCredential/);
2199
+ assert.match(schema, /model AuthRefreshToken/);
2200
+ assert.doesNotMatch(schema, /roles\s+/i);
2201
+ assert.doesNotMatch(schema, /permissions\s+/i);
2202
+
2203
+ const migrationPath = path.join(
2204
+ projectRoot,
2205
+ 'apps',
2206
+ 'api',
2207
+ 'prisma',
2208
+ 'migrations',
2209
+ '0002_accounts_core',
2210
+ 'migration.sql',
2211
+ );
2212
+ assert.equal(fs.existsSync(migrationPath), true);
2213
+
2214
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
2215
+ assert.match(readme, /POST \/api\/auth\/register/);
2216
+ assert.match(readme, /POST \/api\/auth\/password-reset\/request/);
2217
+ assert.match(readme, /\/api\/users\/:id\/settings/);
2218
+
2219
+ const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
2220
+ assert.match(moduleDoc, /Status: implemented/);
2221
+ assert.match(moduleDoc, /db-adapter/);
2222
+ assert.match(moduleDoc, /owner-scoped/);
2223
+ } finally {
2224
+ fs.rmSync(targetRoot, { recursive: true, force: true });
2225
+ }
2226
+ });
2227
+
2228
+ it('detects and applies accounts-rbac compatibility sync explicitly', () => {
2229
+ const targetRoot = mkTmp('forgeon-module-accounts-rbac-');
2230
+ const projectRoot = path.join(targetRoot, 'demo-accounts-rbac');
2231
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
2232
+
2233
+ try {
2234
+ scaffoldProject({
2235
+ templateRoot,
2236
+ packageRoot,
2237
+ targetRoot: projectRoot,
2238
+ projectName: 'demo-accounts-rbac',
2239
+ frontend: 'react',
2240
+ db: 'prisma',
2241
+ dbPrismaEnabled: true,
2242
+ i18nEnabled: false,
2243
+ proxy: 'caddy',
2244
+ });
2245
+
2246
+ addModule({
2247
+ moduleId: 'rbac',
2248
+ targetRoot: projectRoot,
2249
+ packageRoot,
2250
+ });
2251
+ addModule({
2252
+ moduleId: 'accounts',
2253
+ targetRoot: projectRoot,
2254
+ packageRoot,
2255
+ });
2256
+
2257
+ const scan = scanIntegrations({
2258
+ targetRoot: projectRoot,
2259
+ relatedModuleId: 'accounts',
2260
+ });
2261
+ assert.equal(scan.groups.some((group) => group.id === 'accounts-rbac'), true);
2262
+
2263
+ const syncResult = syncIntegrations({
2264
+ targetRoot: projectRoot,
2265
+ packageRoot,
2266
+ groupIds: ['accounts-rbac'],
2267
+ });
2268
+ const claimsPair = syncResult.summary.find((item) => item.id === 'accounts-rbac');
2269
+ assert.ok(claimsPair);
2270
+ assert.equal(claimsPair.result.applied, true);
2271
+
2272
+ const contracts = fs.readFileSync(
2273
+ path.join(projectRoot, 'packages', 'accounts-contracts', 'src', 'index.ts'),
2274
+ 'utf8',
2275
+ );
2276
+ assert.match(contracts, /roles\?: string\[\];/);
2277
+ assert.match(contracts, /permissions\?: string\[\];/);
2278
+
2279
+ const authTypes = fs.readFileSync(
2280
+ path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.types.ts'),
2281
+ 'utf8',
2282
+ );
2283
+ assert.match(authTypes, /roles\?: string\[\];/);
2284
+ assert.match(authTypes, /permissions\?: string\[\];/);
2285
+
2286
+ const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
2287
+ assert.match(readme, /forgeon:accounts:rbac:start/);
2288
+ assert.match(readme, /base accounts schema remains free of roles and permissions/);
2289
+ } finally {
2290
+ fs.rmSync(targetRoot, { recursive: true, force: true });
2291
+ }
2292
+ });
2293
+
2294
+ it('scans accounts-rbac compatibility when accounts and rbac are both installed', () => {
2295
+ const targetRoot = mkTmp('forgeon-module-accounts-rbac-scan-');
2296
+ const projectRoot = path.join(targetRoot, 'demo-accounts-rbac-scan');
2297
+ const templateRoot = path.join(packageRoot, 'templates', 'base');
2298
+
2299
+ try {
2300
+ scaffoldProject({
2301
+ templateRoot,
2302
+ packageRoot,
2303
+ targetRoot: projectRoot,
2304
+ projectName: 'demo-accounts-rbac-scan',
2305
+ frontend: 'react',
2306
+ db: 'prisma',
2307
+ dbPrismaEnabled: true,
2308
+ i18nEnabled: false,
2309
+ proxy: 'caddy',
2310
+ });
2311
+
2312
+ addModule({
2313
+ moduleId: 'accounts',
2314
+ targetRoot: projectRoot,
2315
+ packageRoot,
2316
+ });
2317
+ addModule({
2318
+ moduleId: 'rbac',
2319
+ targetRoot: projectRoot,
2320
+ packageRoot,
2321
+ });
2322
+
2323
+ const scan = scanIntegrations({
2324
+ targetRoot: projectRoot,
2325
+ relatedModuleId: 'rbac',
2326
+ });
2327
+ const compatibilityGroup = scan.groups.find((group) => group.id === 'accounts-rbac');
2328
+
2329
+ assert.ok(compatibilityGroup);
2330
+ assert.deepEqual(compatibilityGroup.modules, ['accounts', 'rbac']);
2331
+ } finally {
2332
+ fs.rmSync(targetRoot, { recursive: true, force: true });
2333
+ }
2334
+ });
2335
+ it('applies logger then accounts on db/i18n-disabled scaffold without breaking health controller syntax', () => {
2215
2336
  const targetRoot = mkTmp('forgeon-module-jwt-nodb-noi18n-');
2216
2337
  const projectRoot = path.join(targetRoot, 'demo-jwt-nodb-noi18n');
2217
2338
  const templateRoot = path.join(packageRoot, 'templates', 'base');
@@ -2235,7 +2356,7 @@ describe('addModule', () => {
2235
2356
  packageRoot,
2236
2357
  });
2237
2358
  addModule({
2238
- moduleId: 'jwt-auth',
2359
+ moduleId: 'accounts',
2239
2360
  targetRoot: projectRoot,
2240
2361
  packageRoot,
2241
2362
  });
@@ -2259,7 +2380,7 @@ describe('addModule', () => {
2259
2380
  }
2260
2381
  });
2261
2382
 
2262
- it('keeps health controller valid for add sequence jwt-auth -> logger -> swagger -> i18n -> db-prisma on db/i18n-disabled scaffold', () => {
2383
+ it('keeps health controller valid for add sequence accounts -> logger -> swagger -> i18n -> db-prisma on db/i18n-disabled scaffold', () => {
2263
2384
  const targetRoot = mkTmp('forgeon-module-seq-health-valid-');
2264
2385
  const projectRoot = path.join(targetRoot, 'demo-seq-health-valid');
2265
2386
  const templateRoot = path.join(packageRoot, 'templates', 'base');
@@ -2277,7 +2398,7 @@ describe('addModule', () => {
2277
2398
  proxy: 'caddy',
2278
2399
  });
2279
2400
 
2280
- for (const moduleId of ['jwt-auth', 'logger', 'swagger', 'i18n', 'db-prisma']) {
2401
+ for (const moduleId of ['accounts', 'logger', 'swagger', 'i18n', 'db-prisma']) {
2281
2402
  addModule({ moduleId, targetRoot: projectRoot, packageRoot });
2282
2403
  }
2283
2404
 
@@ -2312,9 +2433,9 @@ describe('addModule', () => {
2312
2433
  }
2313
2434
  });
2314
2435
 
2315
- it('applies swagger then jwt-auth without forcing swagger dependency in auth-api', () => {
2316
- const targetRoot = mkTmp('forgeon-module-jwt-swagger-');
2317
- const projectRoot = path.join(targetRoot, 'demo-jwt-swagger');
2436
+ it('applies swagger then accounts without forcing swagger dependency in accounts-api', () => {
2437
+ const targetRoot = mkTmp('forgeon-module-accounts-swagger-');
2438
+ const projectRoot = path.join(targetRoot, 'demo-accounts-swagger');
2318
2439
  const templateRoot = path.join(packageRoot, 'templates', 'base');
2319
2440
 
2320
2441
  try {
@@ -2322,7 +2443,7 @@ describe('addModule', () => {
2322
2443
  templateRoot,
2323
2444
  packageRoot,
2324
2445
  targetRoot: projectRoot,
2325
- projectName: 'demo-jwt-swagger',
2446
+ projectName: 'demo-accounts-swagger',
2326
2447
  frontend: 'react',
2327
2448
  db: 'prisma',
2328
2449
  dbPrismaEnabled: false,
@@ -2336,15 +2457,15 @@ describe('addModule', () => {
2336
2457
  packageRoot,
2337
2458
  });
2338
2459
  addModule({
2339
- moduleId: 'jwt-auth',
2460
+ moduleId: 'accounts',
2340
2461
  targetRoot: projectRoot,
2341
2462
  packageRoot,
2342
2463
  });
2343
2464
 
2344
- const authApiPackage = JSON.parse(
2345
- fs.readFileSync(path.join(projectRoot, 'packages', 'auth-api', 'package.json'), 'utf8'),
2465
+ const accountsApiPackage = JSON.parse(
2466
+ fs.readFileSync(path.join(projectRoot, 'packages', 'accounts-api', 'package.json'), 'utf8'),
2346
2467
  );
2347
- assert.equal(Object.hasOwn(authApiPackage.dependencies ?? {}, '@nestjs/swagger'), false);
2468
+ assert.equal(Object.hasOwn(accountsApiPackage.dependencies ?? {}, '@nestjs/swagger'), false);
2348
2469
  } finally {
2349
2470
  fs.rmSync(targetRoot, { recursive: true, force: true });
2350
2471
  }
@@ -2368,7 +2489,7 @@ describe('addModule', () => {
2368
2489
  proxy: 'caddy',
2369
2490
  });
2370
2491
 
2371
- for (const moduleId of ['jwt-auth', 'logger', 'swagger', 'rate-limit', 'i18n', 'db-prisma']) {
2492
+ for (const moduleId of ['accounts', 'logger', 'swagger', 'rate-limit', 'i18n', 'db-prisma']) {
2372
2493
  addModule({ moduleId, targetRoot: projectRoot, packageRoot });
2373
2494
  }
2374
2495
 
@@ -2405,7 +2526,7 @@ describe('addModule', () => {
2405
2526
  proxy: 'caddy',
2406
2527
  });
2407
2528
 
2408
- for (const moduleId of ['jwt-auth', 'logger', 'rate-limit', 'rbac', 'swagger', 'i18n', 'db-prisma']) {
2529
+ for (const moduleId of ['accounts', 'logger', 'rate-limit', 'rbac', 'swagger', 'i18n', 'db-prisma']) {
2409
2530
  addModule({ moduleId, targetRoot: projectRoot, packageRoot });
2410
2531
  }
2411
2532
 
@@ -2503,3 +2624,13 @@ describe('addModule', () => {
2503
2624
  });
2504
2625
 
2505
2626
 
2627
+
2628
+
2629
+
2630
+
2631
+
2632
+
2633
+
2634
+
2635
+
2636
+