create-forgeon 0.3.16 → 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 (107) hide show
  1. package/package.json +1 -1
  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 +1 -0
  6. package/src/core/scaffold.test.mjs +1 -0
  7. package/src/core/validate.test.mjs +1 -0
  8. package/src/modules/accounts.mjs +416 -0
  9. package/src/modules/dependencies.test.mjs +71 -29
  10. package/src/modules/executor.mjs +3 -2
  11. package/src/modules/executor.test.mjs +512 -477
  12. package/src/modules/files-access.mjs +9 -7
  13. package/src/modules/files-image.mjs +9 -7
  14. package/src/modules/files-local.mjs +15 -6
  15. package/src/modules/files-quotas.mjs +8 -6
  16. package/src/modules/files-s3.mjs +17 -6
  17. package/src/modules/files.mjs +21 -21
  18. package/src/modules/idempotency.test.mjs +13 -7
  19. package/src/modules/probes.test.mjs +4 -2
  20. package/src/modules/queue.mjs +9 -6
  21. package/src/modules/rate-limit.mjs +14 -10
  22. package/src/modules/rbac.mjs +12 -11
  23. package/src/modules/registry.mjs +22 -35
  24. package/src/modules/scheduler.mjs +9 -6
  25. package/src/modules/shared/files-runtime-wiring.mjs +81 -0
  26. package/src/modules/shared/patch-utils.mjs +29 -1
  27. package/src/modules/sync-integrations.mjs +102 -422
  28. package/src/modules/sync-integrations.test.mjs +32 -111
  29. package/src/run-add-module.test.mjs +1 -0
  30. package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -325
  31. package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
  32. package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
  33. package/templates/module-fragments/accounts/20_scope.md +29 -0
  34. package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
  35. package/templates/module-fragments/accounts/90_status_planned.md +7 -0
  36. package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
  37. package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
  38. package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
  39. package/templates/module-fragments/swagger/20_scope.md +2 -1
  40. package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
  41. package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
  42. package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
  43. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
  44. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
  45. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
  46. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
  47. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
  48. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
  49. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
  50. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
  51. package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
  52. package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
  53. package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
  54. package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
  55. package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
  56. package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
  57. package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
  58. package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
  59. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
  60. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
  61. package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
  62. package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
  63. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
  64. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
  65. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
  66. package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
  67. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
  68. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
  69. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
  70. package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
  71. package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
  72. package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
  73. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
  74. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
  75. package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
  76. package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
  77. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
  78. package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
  79. package/templates/module-presets/files/packages/files/package.json +1 -2
  80. package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
  81. package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
  82. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
  83. package/templates/module-presets/files/packages/files/src/index.ts +2 -1
  84. package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
  85. package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
  86. package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
  87. package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
  88. package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
  89. package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
  90. package/src/modules/jwt-auth.mjs +0 -271
  91. package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
  92. package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
  93. package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
  94. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
  95. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
  96. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
  97. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
  98. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
  99. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
  100. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
  101. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
  102. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
  103. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
  104. /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
  105. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
  106. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
  107. /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-forgeon",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Forgeon project generator CLI",
5
5
  "license": "MIT",
6
6
  "author": "Forgeon",
@@ -4,8 +4,8 @@ import { parseAddCliArgs } from './add-options.mjs';
4
4
 
5
5
  describe('parseAddCliArgs', () => {
6
6
  it('parses module id and explicit project', () => {
7
- const options = parseAddCliArgs(['jwt-auth', '--project', './demo']);
8
- assert.equal(options.moduleId, 'jwt-auth');
7
+ const options = parseAddCliArgs(['accounts', '--project', './demo']);
8
+ assert.equal(options.moduleId, 'accounts');
9
9
  assert.equal(options.project, './demo');
10
10
  assert.equal(options.list, false);
11
11
  });
@@ -40,3 +40,6 @@ describe('parseAddCliArgs', () => {
40
40
  });
41
41
  });
42
42
  });
43
+
44
+
45
+
@@ -42,3 +42,4 @@ describe('parseCliArgs', () => {
42
42
  );
43
43
  });
44
44
  });
45
+
@@ -146,3 +146,4 @@ describe('promptSelect', () => {
146
146
  assert.equal(inputStream.isPaused(), true);
147
147
  });
148
148
  });
149
+
@@ -122,3 +122,4 @@ describe('generateDocs', () => {
122
122
  }
123
123
  });
124
124
  });
125
+
@@ -97,3 +97,4 @@ describe('scaffoldProject', () => {
97
97
  }
98
98
  });
99
99
  });
100
+
@@ -71,3 +71,4 @@ describe('validatePresetSupport', () => {
71
71
  );
72
72
  });
73
73
  });
74
+
@@ -0,0 +1,416 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
+ import {
5
+ ensureBuildSteps,
6
+ ensureDependency,
7
+ ensureImportLine,
8
+ ensureLineAfter,
9
+ ensureLineBefore,
10
+ ensureLoadItem,
11
+ ensureValidatorSchema,
12
+ upsertEnvLines,
13
+ } from './shared/patch-utils.mjs';
14
+ import { patchHealthControllerServiceProbe } from './shared/nest-runtime-wiring.mjs';
15
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
16
+
17
+ const ACCOUNTS_RBAC_MARKERS = {
18
+ start: '<!-- forgeon:accounts:rbac:start -->',
19
+ end: '<!-- forgeon:accounts:rbac:end -->',
20
+ };
21
+
22
+ const ACCOUNTS_DEFAULT_RBAC_BLOCK =
23
+ '- RBAC compatibility sync: not enabled by default (add `rbac` and run `pnpm forgeon:sync-integrations` to prepare claims compatibility without changing the base accounts schema).';
24
+
25
+ function copyFromPreset(packageRoot, targetRoot, relativePath) {
26
+ const source = path.join(packageRoot, 'templates', 'module-presets', 'accounts', relativePath);
27
+ if (!fs.existsSync(source)) {
28
+ throw new Error(`Missing accounts preset template: ${source}`);
29
+ }
30
+ const destination = path.join(targetRoot, relativePath);
31
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
32
+ copyRecursive(source, destination);
33
+ }
34
+
35
+ function patchApiPackage(targetRoot) {
36
+ const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
37
+ if (!fs.existsSync(packagePath)) {
38
+ return;
39
+ }
40
+
41
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
42
+ ensureDependency(packageJson, '@forgeon/accounts-api', 'workspace:*');
43
+ ensureDependency(packageJson, '@forgeon/accounts-contracts', 'workspace:*');
44
+
45
+ ensureBuildSteps(packageJson, 'predev', [
46
+ 'pnpm --filter @forgeon/accounts-contracts build',
47
+ 'pnpm --filter @forgeon/accounts-api build',
48
+ ]);
49
+
50
+ writeJson(packagePath, packageJson);
51
+ }
52
+
53
+ function patchPrismaSchema(targetRoot) {
54
+ const schemaPath = path.join(targetRoot, 'apps', 'api', 'prisma', 'schema.prisma');
55
+ if (!fs.existsSync(schemaPath)) {
56
+ return;
57
+ }
58
+
59
+ let content = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
60
+ const userModel = `model User {
61
+ id String @id @default(cuid())
62
+ status String @default("active")
63
+ data Json?
64
+ createdAt DateTime @default(now())
65
+ updatedAt DateTime @updatedAt
66
+ deletedAt DateTime?
67
+ profile UserProfile?
68
+ settings UserSettings?
69
+ authIdentities AuthIdentity[]
70
+ authCredential AuthCredential?
71
+ authRefreshTokens AuthRefreshToken[]
72
+ }`;
73
+
74
+ if (/model User \{[\s\S]*?\n\}/m.test(content)) {
75
+ content = content.replace(/model User \{[\s\S]*?\n\}/m, userModel);
76
+ } else {
77
+ content = `${content.trimEnd()}\n\n${userModel}\n`;
78
+ }
79
+
80
+ const extraModels = [
81
+ `model UserProfile {
82
+ userId String @id
83
+ name String?
84
+ avatar String?
85
+ data Json?
86
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
87
+ }`,
88
+ `model UserSettings {
89
+ userId String @id
90
+ theme String?
91
+ locale String?
92
+ data Json?
93
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
94
+ }`,
95
+ `model AuthIdentity {
96
+ id String @id @default(cuid())
97
+ userId String
98
+ provider String
99
+ providerId String
100
+ createdAt DateTime @default(now())
101
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
102
+
103
+ @@unique([provider, providerId])
104
+ }`,
105
+ `model AuthCredential {
106
+ id String @id @default(cuid())
107
+ userId String @unique
108
+ passwordHash String
109
+ createdAt DateTime @default(now())
110
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
111
+ }`,
112
+ `model AuthRefreshToken {
113
+ id String @id @default(cuid())
114
+ userId String
115
+ tokenHash String
116
+ expiresAt DateTime
117
+ revokedAt DateTime?
118
+ createdAt DateTime @default(now())
119
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
120
+
121
+ @@index([userId, createdAt])
122
+ }`,
123
+ ];
124
+
125
+ for (const model of extraModels) {
126
+ const name = model.match(/^model\s+(\w+)/m)?.[1];
127
+ if (!name || content.includes(`model ${name} {`)) {
128
+ continue;
129
+ }
130
+ content = `${content.trimEnd()}\n\n${model}\n`;
131
+ }
132
+
133
+ fs.writeFileSync(schemaPath, `${content.trimEnd()}\n`, 'utf8');
134
+ }
135
+
136
+ function copyMigrationFolder(packageRoot, targetRoot, migrationName) {
137
+ const migrationDir = path.join(targetRoot, 'apps', 'api', 'prisma', 'migrations', migrationName);
138
+ const migrationFile = path.join(migrationDir, 'migration.sql');
139
+ if (fs.existsSync(migrationFile)) {
140
+ return;
141
+ }
142
+
143
+ const sourceDir = path.join(
144
+ packageRoot,
145
+ 'templates',
146
+ 'module-presets',
147
+ 'accounts',
148
+ 'apps',
149
+ 'api',
150
+ 'prisma',
151
+ 'migrations',
152
+ migrationName,
153
+ );
154
+ if (!fs.existsSync(sourceDir)) {
155
+ return;
156
+ }
157
+
158
+ fs.mkdirSync(path.dirname(migrationDir), { recursive: true });
159
+ copyRecursive(sourceDir, migrationDir);
160
+ }
161
+
162
+ function patchPrismaMigration(packageRoot, targetRoot) {
163
+ copyMigrationFolder(packageRoot, targetRoot, '0002_accounts_core');
164
+ }
165
+
166
+ function patchAppModule(targetRoot) {
167
+ const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
168
+ if (!fs.existsSync(filePath)) {
169
+ return;
170
+ }
171
+
172
+ let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
173
+ content = content.replace(
174
+ "import { authConfig, authEnvSchema, ForgeonAccountsModule } from '@forgeon/accounts-api';",
175
+ "import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
176
+ );
177
+ content = ensureImportLine(
178
+ content,
179
+ "import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
180
+ );
181
+ content = ensureImportLine(
182
+ content,
183
+ "import { ForgeonAccountsDbPrismaModule } from './accounts/forgeon-accounts-db-prisma.module';",
184
+ );
185
+ content = ensureLoadItem(content, 'authConfig');
186
+ content = ensureValidatorSchema(content, 'authEnvSchema');
187
+
188
+ if (!content.includes(' ForgeonAccountsDbPrismaModule,')) {
189
+ if (content.includes(' DbPrismaModule,')) {
190
+ content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonAccountsDbPrismaModule,');
191
+ } else {
192
+ content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonAccountsDbPrismaModule,');
193
+ }
194
+ }
195
+
196
+ const accountsModuleLine = ` ForgeonAccountsModule.register({
197
+ users: UsersModule.register({}),
198
+ }),`;
199
+ if (!content.includes('ForgeonAccountsModule.register({')) {
200
+ if (content.includes(' ForgeonI18nModule.register({')) {
201
+ content = ensureLineBefore(content, ' ForgeonI18nModule.register({', accountsModuleLine);
202
+ } else if (content.includes(' ForgeonAccountsDbPrismaModule,')) {
203
+ content = ensureLineAfter(content, ' ForgeonAccountsDbPrismaModule,', accountsModuleLine);
204
+ } else {
205
+ content = ensureLineAfter(content, ' CoreErrorsModule,', accountsModuleLine);
206
+ }
207
+ }
208
+
209
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
210
+ }
211
+
212
+ function patchHealthController(targetRoot, probeTargets) {
213
+ patchHealthControllerServiceProbe(targetRoot, probeTargets, {
214
+ importLine: "import { AuthService } from '@forgeon/accounts-api';",
215
+ constructorMember: 'private readonly authService: AuthService',
216
+ routePath: 'auth',
217
+ methodName: 'getAuthProbe',
218
+ serviceCall: 'this.authService.getProbeStatus()',
219
+ beforeNeedles: ["@Post('db')"],
220
+ beforeNeedle: 'private translate(',
221
+ });
222
+ }
223
+
224
+ function registerWebProbe(targetRoot, probeTargets) {
225
+ ensureWebProbeDefinition({
226
+ targetRoot,
227
+ probeTargets,
228
+ definition: {
229
+ id: 'auth',
230
+ title: 'Accounts',
231
+ buttonLabel: 'Check accounts probe',
232
+ resultTitle: 'Accounts probe response',
233
+ path: '/health/auth',
234
+ },
235
+ });
236
+ }
237
+
238
+ function patchApiDockerfile(targetRoot) {
239
+ const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
240
+ if (!fs.existsSync(dockerfilePath)) {
241
+ return;
242
+ }
243
+
244
+ let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
245
+ const packageAnchors = [
246
+ 'COPY packages/swagger/package.json packages/swagger/package.json',
247
+ 'COPY packages/logger/package.json packages/logger/package.json',
248
+ 'COPY packages/i18n/package.json packages/i18n/package.json',
249
+ 'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
250
+ 'COPY packages/core/package.json packages/core/package.json',
251
+ ];
252
+ const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
253
+ content = ensureLineAfter(
254
+ content,
255
+ packageAnchor,
256
+ 'COPY packages/accounts-contracts/package.json packages/accounts-contracts/package.json',
257
+ );
258
+ content = ensureLineAfter(
259
+ content,
260
+ 'COPY packages/accounts-contracts/package.json packages/accounts-contracts/package.json',
261
+ 'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
262
+ );
263
+
264
+ const sourceAnchors = [
265
+ 'COPY packages/swagger packages/swagger',
266
+ 'COPY packages/logger packages/logger',
267
+ 'COPY packages/i18n packages/i18n',
268
+ 'COPY packages/db-prisma packages/db-prisma',
269
+ 'COPY packages/core packages/core',
270
+ ];
271
+ const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
272
+ content = ensureLineAfter(content, sourceAnchor, 'COPY packages/accounts-contracts packages/accounts-contracts');
273
+ content = ensureLineAfter(
274
+ content,
275
+ 'COPY packages/accounts-contracts packages/accounts-contracts',
276
+ 'COPY packages/accounts-api packages/accounts-api',
277
+ );
278
+
279
+ content = content
280
+ .replace(/^RUN pnpm --filter @forgeon\/auth-contracts build\r?\n?/gm, '')
281
+ .replace(/^RUN pnpm --filter @forgeon\/auth-api build\r?\n?/gm, '');
282
+
283
+ const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
284
+ ? 'RUN pnpm --filter @forgeon/api prisma:generate'
285
+ : 'RUN pnpm --filter @forgeon/api build';
286
+ content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/accounts-contracts build');
287
+ content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/accounts-api build');
288
+
289
+ fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
290
+ }
291
+
292
+ function patchCompose(targetRoot) {
293
+ const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
294
+ if (!fs.existsSync(composePath)) {
295
+ return;
296
+ }
297
+
298
+ let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
299
+ if (!content.includes('JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET}')) {
300
+ content = content.replace(
301
+ /^(\s+API_PREFIX:.*)$/m,
302
+ `$1
303
+ JWT_ACCESS_SECRET: \${JWT_ACCESS_SECRET}
304
+ JWT_ACCESS_EXPIRES_IN: \${JWT_ACCESS_EXPIRES_IN}
305
+ JWT_REFRESH_SECRET: \${JWT_REFRESH_SECRET}
306
+ JWT_REFRESH_EXPIRES_IN: \${JWT_REFRESH_EXPIRES_IN}
307
+ AUTH_ARGON2_MEMORY_COST: \${AUTH_ARGON2_MEMORY_COST}
308
+ AUTH_ARGON2_TIME_COST: \${AUTH_ARGON2_TIME_COST}
309
+ AUTH_ARGON2_PARALLELISM: \${AUTH_ARGON2_PARALLELISM}`,
310
+ );
311
+ }
312
+
313
+ fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
314
+ }
315
+
316
+ function patchReadme(targetRoot) {
317
+ const readmePath = path.join(targetRoot, 'README.md');
318
+ if (!fs.existsSync(readmePath)) {
319
+ return;
320
+ }
321
+
322
+ const section = [
323
+ '## Accounts Module',
324
+ '',
325
+ 'The accounts add-module provides a DB-backed accounts/authentication surface with owner-scoped user routes.',
326
+ '',
327
+ 'What it adds:',
328
+ '- `@forgeon/accounts-contracts` shared contracts for auth and self-service users routes',
329
+ '- `@forgeon/accounts-api` Nest accounts runtime with JWT auth, argon2 passwords, and hashed refresh-token rotation',
330
+ '- owner-scoped routes under `/api/users/:id`, `/api/users/:id/profile`, and `/api/users/:id/settings` (`/users/me` resolves through the same surface)',
331
+ '- auth probe endpoint: `GET /api/health/auth`',
332
+ '',
333
+ 'Current boundaries:',
334
+ '- `UsersModule.register({ user, profile, settings })` controls runtime defaults for JSON-backed extension fields',
335
+ '- email verification and password reset use an internal email stub through `AccountsEmailPort`',
336
+ '- base accounts schema does not include RBAC storage',
337
+ ACCOUNTS_RBAC_MARKERS.start,
338
+ ACCOUNTS_DEFAULT_RBAC_BLOCK,
339
+ ACCOUNTS_RBAC_MARKERS.end,
340
+ '',
341
+ 'Default routes:',
342
+ '- `POST /api/auth/register`',
343
+ '- `POST /api/auth/login`',
344
+ '- `POST /api/auth/refresh`',
345
+ '- `POST /api/auth/logout`',
346
+ '- `GET /api/auth/me`',
347
+ '- `POST /api/auth/change-password`',
348
+ '- `POST /api/auth/verify-email` (stub)',
349
+ '- `POST /api/auth/password-reset/request` (stub)',
350
+ '- `POST /api/auth/password-reset/confirm` (stub)',
351
+ ].join('\n');
352
+
353
+ let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
354
+ const sectionHeading = '## Accounts Module';
355
+ if (content.includes(sectionHeading)) {
356
+ const start = content.indexOf(sectionHeading);
357
+ const tail = content.slice(start + sectionHeading.length);
358
+ const nextHeadingMatch = tail.match(/\n##\s+/);
359
+ const end =
360
+ nextHeadingMatch && nextHeadingMatch.index !== undefined
361
+ ? start + sectionHeading.length + nextHeadingMatch.index + 1
362
+ : content.length;
363
+ content = `${content.slice(0, start)}${section}\n\n${content.slice(end).replace(/^\n+/, '')}`;
364
+ } else if (content.includes('## Prisma In Docker Start')) {
365
+ content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
366
+ } else {
367
+ content = `${content.trimEnd()}\n\n${section}\n`;
368
+ }
369
+
370
+ fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
371
+ }
372
+
373
+ export function applyAccountsModule({ packageRoot, targetRoot }) {
374
+ copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-contracts'));
375
+ copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-api'));
376
+ copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'accounts'));
377
+ copyFromPreset(
378
+ packageRoot,
379
+ targetRoot,
380
+ path.join('apps', 'api', 'prisma', 'migrations', '0002_accounts_core'),
381
+ );
382
+
383
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'accounts' });
384
+
385
+ patchApiPackage(targetRoot);
386
+ patchPrismaSchema(targetRoot);
387
+ patchPrismaMigration(packageRoot, targetRoot);
388
+ patchAppModule(targetRoot);
389
+ patchHealthController(targetRoot, probeTargets);
390
+ registerWebProbe(targetRoot, probeTargets);
391
+ patchApiDockerfile(targetRoot);
392
+ patchCompose(targetRoot);
393
+ patchReadme(targetRoot);
394
+
395
+ upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
396
+ 'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',
397
+ 'JWT_ACCESS_EXPIRES_IN=15m',
398
+ 'JWT_REFRESH_SECRET=forgeon-refresh-secret-change-me',
399
+ 'JWT_REFRESH_EXPIRES_IN=7d',
400
+ 'AUTH_ARGON2_MEMORY_COST=19456',
401
+ 'AUTH_ARGON2_TIME_COST=2',
402
+ 'AUTH_ARGON2_PARALLELISM=1',
403
+ ]);
404
+
405
+ upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
406
+ 'JWT_ACCESS_SECRET=forgeon-access-secret-change-me',
407
+ 'JWT_ACCESS_EXPIRES_IN=15m',
408
+ 'JWT_REFRESH_SECRET=forgeon-refresh-secret-change-me',
409
+ 'JWT_REFRESH_EXPIRES_IN=7d',
410
+ 'AUTH_ARGON2_MEMORY_COST=19456',
411
+ 'AUTH_ARGON2_TIME_COST=2',
412
+ 'AUTH_ARGON2_PARALLELISM=1',
413
+ ]);
414
+ }
415
+
416
+
@@ -25,6 +25,48 @@ const TEST_PRESETS = [
25
25
  requires: [],
26
26
  optionalIntegrations: [],
27
27
  },
28
+ {
29
+ id: 'accounts',
30
+ label: 'Accounts',
31
+ implemented: true,
32
+ detectionPaths: ['packages/accounts-api/package.json'],
33
+ provides: ['accounts-runtime'],
34
+ requires: [{ type: 'capability', id: 'db-adapter' }],
35
+ optionalIntegrations: [
36
+ {
37
+ id: 'accounts-rbac',
38
+ title: 'Accounts RBAC Compatibility Sync',
39
+ modules: ['accounts', 'rbac'],
40
+ requires: [{ type: 'module', id: 'rbac' }],
41
+ description: ['Prepare accounts auth claims compatibility'],
42
+ followUpCommands: [
43
+ 'npx create-forgeon@latest add rbac',
44
+ 'pnpm forgeon:sync-integrations',
45
+ ],
46
+ },
47
+ ],
48
+ },
49
+ {
50
+ id: 'rbac',
51
+ label: 'RBAC',
52
+ implemented: true,
53
+ detectionPaths: ['packages/rbac/package.json'],
54
+ provides: ['rbac-runtime'],
55
+ requires: [],
56
+ optionalIntegrations: [
57
+ {
58
+ id: 'accounts-rbac',
59
+ title: 'Accounts RBAC Compatibility Sync',
60
+ modules: ['accounts', 'rbac'],
61
+ requires: [{ type: 'module', id: 'accounts' }],
62
+ description: ['Prepare accounts auth claims compatibility'],
63
+ followUpCommands: [
64
+ 'npx create-forgeon@latest add accounts',
65
+ 'pnpm forgeon:sync-integrations',
66
+ ],
67
+ },
68
+ ],
69
+ },
28
70
  {
29
71
  id: 'files',
30
72
  label: 'Files',
@@ -88,27 +130,6 @@ const TEST_PRESETS = [
88
130
  requires: [{ type: 'capability', id: 'files-runtime' }],
89
131
  optionalIntegrations: [],
90
132
  },
91
- {
92
- id: 'jwt-auth',
93
- label: 'JWT Auth',
94
- implemented: true,
95
- detectionPaths: ['packages/auth-api/package.json'],
96
- provides: ['auth-runtime'],
97
- requires: [],
98
- optionalIntegrations: [
99
- {
100
- id: 'auth-persistence',
101
- title: 'Auth Persistence Integration',
102
- modules: ['jwt-auth', 'db-adapter'],
103
- requires: [{ type: 'capability', id: 'db-adapter' }],
104
- description: ['Persist refresh-token state'],
105
- followUpCommands: [
106
- 'npx create-forgeon@latest add db-prisma',
107
- 'pnpm forgeon:sync-integrations',
108
- ],
109
- },
110
- ],
111
- },
112
133
  {
113
134
  id: 'queue',
114
135
  label: 'Queue',
@@ -169,6 +190,28 @@ describe('module dependency helpers', () => {
169
190
  }
170
191
  });
171
192
 
193
+ it('builds a concrete install plan for accounts with required db-adapter', async () => {
194
+ const targetRoot = mkTmp('forgeon-deps-accounts-');
195
+
196
+ try {
197
+ const result = await resolveModuleInstallPlan({
198
+ moduleId: 'accounts',
199
+ targetRoot,
200
+ presets: TEST_PRESETS,
201
+ withRequired: true,
202
+ isInteractive: false,
203
+ });
204
+
205
+ assert.equal(result.cancelled, false);
206
+ assert.deepEqual(result.moduleSequence, ['db-prisma', 'accounts']);
207
+ assert.deepEqual(result.selectedProviders, {
208
+ 'db-adapter': 'db-prisma',
209
+ });
210
+ } finally {
211
+ fs.rmSync(targetRoot, { recursive: true, force: true });
212
+ }
213
+ });
214
+
172
215
  it('fails in non-interactive mode with --with-required when capability provider mapping is ambiguous', async () => {
173
216
  const targetRoot = mkTmp('forgeon-deps-provider-required-');
174
217
 
@@ -318,21 +361,21 @@ describe('module dependency helpers', () => {
318
361
  }
319
362
  });
320
363
 
321
- it('reports missing optional integrations for the installed module', () => {
364
+ it('reports pending optional integrations for accounts when rbac is missing', () => {
322
365
  const targetRoot = mkTmp('forgeon-deps-optional-');
323
366
  try {
324
- fs.mkdirSync(path.join(targetRoot, 'packages', 'auth-api'), { recursive: true });
325
- fs.writeFileSync(path.join(targetRoot, 'packages', 'auth-api', 'package.json'), '{}\n', 'utf8');
367
+ fs.mkdirSync(path.join(targetRoot, 'packages', 'accounts-api'), { recursive: true });
368
+ fs.writeFileSync(path.join(targetRoot, 'packages', 'accounts-api', 'package.json'), '{}\n', 'utf8');
326
369
 
327
370
  const pending = getPendingOptionalIntegrations({
328
- moduleId: 'jwt-auth',
371
+ moduleId: 'accounts',
329
372
  targetRoot,
330
373
  presets: TEST_PRESETS,
331
374
  });
332
375
 
333
376
  assert.equal(pending.length, 1);
334
- assert.equal(pending[0].id, 'auth-persistence');
335
- assert.equal(pending[0].missing[0].id, 'db-adapter');
377
+ assert.equal(pending[0].id, 'accounts-rbac');
378
+ assert.equal(pending[0].missing[0].id, 'rbac');
336
379
  } finally {
337
380
  fs.rmSync(targetRoot, { recursive: true, force: true });
338
381
  }
@@ -397,5 +440,4 @@ describe('module dependency helpers', () => {
397
440
  fs.rmSync(targetRoot, { recursive: true, force: true });
398
441
  }
399
442
  });
400
- });
401
-
443
+ });
@@ -10,7 +10,7 @@ import { applyFilesQuotasModule } from './files-quotas.mjs';
10
10
  import { applyFilesLocalModule } from './files-local.mjs';
11
11
  import { applyFilesS3Module } from './files-s3.mjs';
12
12
  import { applyI18nModule } from './i18n.mjs';
13
- import { applyJwtAuthModule } from './jwt-auth.mjs';
13
+ import { applyAccountsModule } from './accounts.mjs';
14
14
  import { applyLoggerModule } from './logger.mjs';
15
15
  import { applyRateLimitModule } from './rate-limit.mjs';
16
16
  import { applyRbacModule } from './rbac.mjs';
@@ -43,7 +43,7 @@ const MODULE_APPLIERS = {
43
43
  'files-local': applyFilesLocalModule,
44
44
  'files-s3': applyFilesS3Module,
45
45
  i18n: applyI18nModule,
46
- 'jwt-auth': applyJwtAuthModule,
46
+ accounts: applyAccountsModule,
47
47
  logger: applyLoggerModule,
48
48
  queue: applyQueueModule,
49
49
  'rate-limit': applyRateLimitModule,
@@ -85,3 +85,4 @@ export function addModule({ moduleId, targetRoot, packageRoot, writeDocs = true
85
85
  };
86
86
  }
87
87
 
88
+