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
@@ -2,347 +2,105 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
- const PRISMA_AUTH_STORE_CONTENT = `import {
6
- AuthRefreshTokenStore,
7
- } from '@forgeon/auth-api';
8
- import { PrismaService } from '@forgeon/db-prisma';
9
- import { Injectable } from '@nestjs/common';
10
-
11
- @Injectable()
12
- export class PrismaAuthRefreshTokenStore implements AuthRefreshTokenStore {
13
- readonly kind = 'prisma';
5
+ const ACCOUNTS_RBAC_MARKERS = {
6
+ start: '<!-- forgeon:accounts:rbac:start -->',
7
+ end: '<!-- forgeon:accounts:rbac:end -->',
8
+ };
14
9
 
15
- constructor(private readonly prisma: PrismaService) {}
10
+ const ACCOUNTS_RBAC_ENABLED_BLOCK =
11
+ '- RBAC compatibility sync: contracts and JWT payload surfaces are prepared for optional RBAC claims, while the base accounts schema remains free of roles and permissions.';
16
12
 
17
- async saveRefreshTokenHash(subject: string, hash: string): Promise<void> {
18
- await this.prisma.user.upsert({
19
- where: { email: subject },
20
- create: { email: subject, refreshTokenHash: hash },
21
- update: { refreshTokenHash: hash },
22
- select: { id: true },
23
- });
24
- }
25
-
26
- async getRefreshTokenHash(subject: string): Promise<string | null> {
27
- const user = await this.prisma.user.findUnique({
28
- where: { email: subject },
29
- select: { refreshTokenHash: true },
30
- });
31
- return user?.refreshTokenHash ?? null;
32
- }
13
+ function escapeRegExp(value) {
14
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
+ }
33
16
 
34
- async removeRefreshTokenHash(subject: string): Promise<void> {
35
- await this.prisma.user.updateMany({
36
- where: { email: subject },
37
- data: { refreshTokenHash: null },
38
- });
17
+ function replaceReadmeManagedBlock(content, startMarker, endMarker, nextBody) {
18
+ const pattern = new RegExp(`${escapeRegExp(startMarker)}\\n[\\s\\S]*?\\n${escapeRegExp(endMarker)}`);
19
+ if (!pattern.test(content)) {
20
+ return content;
39
21
  }
22
+ return content.replace(pattern, `${startMarker}\n${nextBody}\n${endMarker}`);
40
23
  }
41
- `;
42
-
43
- const PRISMA_AUTH_MIGRATION_CONTENT = `-- AlterTable
44
- ALTER TABLE "User"
45
- ADD COLUMN "refreshTokenHash" TEXT;
46
- `;
47
-
48
- const AUTH_PERSISTENCE_STRATEGIES = [
49
- {
50
- id: 'db-prisma',
51
- providerLabel: 'db-prisma',
52
- isDetected: (detected) => detected.dbPrisma,
53
- apply: syncJwtDbPrisma,
54
- },
55
- ];
56
24
 
57
25
  function detectModules(rootDir) {
58
26
  const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
59
27
  const appModuleText = fs.existsSync(appModulePath) ? fs.readFileSync(appModulePath, 'utf8') : '';
60
28
 
61
29
  return {
62
- jwtAuth:
63
- fs.existsSync(path.join(rootDir, 'packages', 'auth-api', 'package.json')) ||
64
- appModuleText.includes("from '@forgeon/auth-api'"),
30
+ accounts:
31
+ fs.existsSync(path.join(rootDir, 'packages', 'accounts-api', 'package.json')) ||
32
+ appModuleText.includes("from '@forgeon/accounts-api'"),
65
33
  rbac:
66
34
  fs.existsSync(path.join(rootDir, 'packages', 'rbac', 'package.json')) ||
67
35
  appModuleText.includes("from '@forgeon/rbac'"),
68
- dbPrisma:
69
- fs.existsSync(path.join(rootDir, 'packages', 'db-prisma', 'package.json')) ||
70
- appModuleText.includes("from '@forgeon/db-prisma'"),
71
36
  };
72
37
  }
73
38
 
74
- function ensureLineAfter(content, anchorLine, lineToInsert) {
75
- if (content.includes(lineToInsert)) {
76
- return content;
77
- }
78
- const index = content.indexOf(anchorLine);
79
- if (index < 0) {
80
- return `${content.trimEnd()}\n${lineToInsert}\n`;
81
- }
82
- const insertAt = index + anchorLine.length;
83
- return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
84
- }
85
-
86
- const JWT_AUTH_PERSISTENCE_MARKERS = {
87
- start: '<!-- forgeon:jwt-auth:persistence:start -->',
88
- end: '<!-- forgeon:jwt-auth:persistence:end -->',
89
- };
90
-
91
- const JWT_AUTH_RBAC_MARKERS = {
92
- start: '<!-- forgeon:jwt-auth:rbac:start -->',
93
- end: '<!-- forgeon:jwt-auth:rbac:end -->',
94
- };
95
-
96
- const JWT_AUTH_DB_PRISMA_PERSISTENCE_BLOCK = [
97
- '- refresh token persistence: enabled through the `db-adapter` capability (current provider: `db-prisma`)',
98
- '- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
99
- ].join('\n');
100
-
101
- const JWT_AUTH_RBAC_ENABLED_BLOCK = '- RBAC integration: demo auth tokens include `health.rbac` permission';
102
-
103
- function escapeRegExp(value) {
104
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
105
- }
106
-
107
- function replaceReadmeManagedBlock(content, startMarker, endMarker, nextBody) {
108
- const pattern = new RegExp(`${escapeRegExp(startMarker)}\\n[\\s\\S]*?\\n${escapeRegExp(endMarker)}`);
109
- if (!pattern.test(content)) {
110
- return content;
111
- }
112
- return content.replace(pattern, `${startMarker}\n${nextBody}\n${endMarker}`);
113
- }
114
- function resolveAuthPersistenceStrategy(detected) {
115
- const matched = AUTH_PERSISTENCE_STRATEGIES.filter((strategy) => strategy.isDetected(detected));
116
- if (matched.length === 0) {
117
- return { kind: 'none' };
118
- }
119
- if (matched.length > 1) {
120
- return { kind: 'conflict', strategies: matched };
121
- }
122
- return { kind: 'single', strategy: matched[0] };
123
- }
124
-
125
- function syncJwtDbPrisma({ rootDir, changedFiles }) {
126
- const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
127
- const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
128
- const storePath = path.join(rootDir, 'apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts');
129
- const migrationPath = path.join(
130
- rootDir,
131
- 'apps',
132
- 'api',
133
- 'prisma',
134
- 'migrations',
135
- '0002_auth_refresh_token_hash',
136
- 'migration.sql',
137
- );
39
+ function syncAccountsRbac(rootDir, changedFiles) {
40
+ const contractsPath = path.join(rootDir, 'packages', 'accounts-contracts', 'src', 'index.ts');
41
+ const authTypesPath = path.join(rootDir, 'packages', 'accounts-api', 'src', 'auth.types.ts');
138
42
  const readmePath = path.join(rootDir, 'README.md');
139
43
 
140
- if (!fs.existsSync(appModulePath) || !fs.existsSync(schemaPath)) {
141
- return { applied: false, reason: 'app module or prisma schema is missing' };
44
+ if (!fs.existsSync(contractsPath) || !fs.existsSync(authTypesPath) || !fs.existsSync(readmePath)) {
45
+ return { applied: false, reason: 'accounts package files are missing' };
142
46
  }
143
47
 
144
48
  let touched = false;
145
49
 
146
- if (!fs.existsSync(storePath)) {
147
- fs.mkdirSync(path.dirname(storePath), { recursive: true });
148
- fs.writeFileSync(storePath, PRISMA_AUTH_STORE_CONTENT, 'utf8');
149
- changedFiles.add(storePath);
150
- touched = true;
151
- }
152
-
153
- let appModule = fs.readFileSync(appModulePath, 'utf8').replace(/\r\n/g, '\n');
154
- const originalAppModule = appModule;
155
-
156
- if (!appModule.includes("AUTH_REFRESH_TOKEN_STORE, authConfig")) {
157
- appModule = appModule.replace(
158
- /import\s*\{\s*authConfig,\s*authEnvSchema,\s*ForgeonAuthModule\s*\}\s*from '@forgeon\/auth-api';/m,
159
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
50
+ let contracts = fs.readFileSync(contractsPath, 'utf8').replace(/\r\n/g, '\n');
51
+ const originalContracts = contracts;
52
+ if (!contracts.includes('roles?: string[];')) {
53
+ contracts = contracts.replace(
54
+ " type: 'access';",
55
+ " type: 'access';\n roles?: string[];\n permissions?: string[];",
160
56
  );
161
57
  }
162
-
163
- const storeImportLine = "import { PrismaAuthRefreshTokenStore } from './auth/prisma-auth-refresh-token.store';";
164
- if (!appModule.includes(storeImportLine)) {
165
- appModule = ensureLineAfter(
166
- appModule,
167
- "import { HealthController } from './health/health.controller';",
168
- storeImportLine,
58
+ if (!contracts.includes("jti: string;\n type: 'refresh';\n roles?: string[];")) {
59
+ contracts = contracts.replace(
60
+ " jti: string;\n type: 'refresh';",
61
+ " jti: string;\n type: 'refresh';\n roles?: string[];\n permissions?: string[];",
169
62
  );
170
63
  }
171
-
172
- if (!appModule.includes('refreshTokenStoreProvider')) {
173
- appModule = appModule.replace(
174
- /ForgeonAuthModule\.register\(\),/m,
175
- `ForgeonAuthModule.register({
176
- imports: [DbPrismaModule],
177
- refreshTokenStoreProvider: {
178
- provide: AUTH_REFRESH_TOKEN_STORE,
179
- useClass: PrismaAuthRefreshTokenStore,
180
- },
181
- }),`,
182
- );
183
- }
184
-
185
- if (appModule !== originalAppModule) {
186
- fs.writeFileSync(appModulePath, `${appModule.trimEnd()}\n`, 'utf8');
187
- changedFiles.add(appModulePath);
188
- touched = true;
189
- }
190
-
191
- let schema = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
192
- const originalSchema = schema;
193
- if (!schema.includes('refreshTokenHash')) {
194
- schema = schema.replace(/email\s+String\s+@unique/g, 'email String @unique\n refreshTokenHash String?');
195
- }
196
- if (schema !== originalSchema) {
197
- fs.writeFileSync(schemaPath, `${schema.trimEnd()}\n`, 'utf8');
198
- changedFiles.add(schemaPath);
64
+ if (contracts !== originalContracts) {
65
+ fs.writeFileSync(contractsPath, `${contracts.trimEnd()}\n`, 'utf8');
66
+ changedFiles.add(contractsPath);
199
67
  touched = true;
200
68
  }
201
69
 
202
- if (!fs.existsSync(migrationPath)) {
203
- fs.mkdirSync(path.dirname(migrationPath), { recursive: true });
204
- fs.writeFileSync(migrationPath, PRISMA_AUTH_MIGRATION_CONTENT, 'utf8');
205
- changedFiles.add(migrationPath);
206
- touched = true;
207
- }
208
-
209
- if (fs.existsSync(readmePath)) {
210
- let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
211
- const originalReadme = readme;
212
- const managedReadme = replaceReadmeManagedBlock(
213
- readme,
214
- JWT_AUTH_PERSISTENCE_MARKERS.start,
215
- JWT_AUTH_PERSISTENCE_MARKERS.end,
216
- JWT_AUTH_DB_PRISMA_PERSISTENCE_BLOCK,
70
+ let authTypes = fs.readFileSync(authTypesPath, 'utf8').replace(/\r\n/g, '\n');
71
+ const originalAuthTypes = authTypes;
72
+ if (!authTypes.includes('roles?: string[];')) {
73
+ authTypes = authTypes.replace(
74
+ " exp?: number;",
75
+ " exp?: number;\n roles?: string[];\n permissions?: string[];",
217
76
  );
218
- if (managedReadme !== readme) {
219
- readme = managedReadme;
220
- } else {
221
- readme = readme.replace(
222
- '- refresh token persistence: disabled by default (stateless mode; enable it later through a `db-adapter` provider + integration sync)',
223
- '- refresh token persistence: enabled through the `db-adapter` capability (current provider: `db-prisma`)',
224
- );
225
- readme = readme.replace(
226
- /- to enable persistence later:[\s\S]*?2\. run `pnpm forgeon:sync-integrations` to wire auth persistence to the active DB adapter implementation\./m,
227
- '- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
228
- );
229
- }
230
- if (readme !== originalReadme) {
231
- fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
232
- changedFiles.add(readmePath);
233
- touched = true;
234
- }
235
77
  }
236
-
237
- if (!touched) {
238
- return { applied: false, reason: 'already synced' };
239
- }
240
- return { applied: true };
241
- }
242
-
243
- function syncJwtRbacClaims({ rootDir, changedFiles }) {
244
- const authContractsPath = path.join(rootDir, 'packages', 'auth-contracts', 'src', 'index.ts');
245
- const authServicePath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.service.ts');
246
- const authControllerPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.controller.ts');
247
- const readmePath = path.join(rootDir, 'README.md');
248
-
249
- if (!fs.existsSync(authContractsPath) || !fs.existsSync(authServicePath) || !fs.existsSync(authControllerPath)) {
250
- return { applied: false, reason: 'auth package files are missing' };
251
- }
252
-
253
- let touched = false;
254
-
255
- let authContracts = fs.readFileSync(authContractsPath, 'utf8').replace(/\r\n/g, '\n');
256
- const originalAuthContracts = authContracts;
257
- if (!authContracts.includes('permissions?: string[];')) {
258
- authContracts = authContracts.replace(
259
- ' roles: string[];',
260
- ` roles: string[];
261
- permissions?: string[];`,
78
+ if (authTypes.includes('export interface AuthRefreshTokenPayload extends AuthRefreshClaims {') && !authTypes.includes("AuthRefreshTokenPayload extends AuthRefreshClaims {\n iat?: number;\n exp?: number;\n roles?: string[];")) {
79
+ authTypes = authTypes.replace(
80
+ "export interface AuthRefreshTokenPayload extends AuthRefreshClaims {\n iat?: number;\n exp?: number;\n}",
81
+ "export interface AuthRefreshTokenPayload extends AuthRefreshClaims {\n iat?: number;\n exp?: number;\n roles?: string[];\n permissions?: string[];\n}",
262
82
  );
263
83
  }
264
- if (authContracts !== originalAuthContracts) {
265
- fs.writeFileSync(authContractsPath, `${authContracts.trimEnd()}\n`, 'utf8');
266
- changedFiles.add(authContractsPath);
84
+ if (authTypes !== originalAuthTypes) {
85
+ fs.writeFileSync(authTypesPath, `${authTypes.trimEnd()}\n`, 'utf8');
86
+ changedFiles.add(authTypesPath);
267
87
  touched = true;
268
88
  }
269
89
 
270
- let authService = fs.readFileSync(authServicePath, 'utf8').replace(/\r\n/g, '\n');
271
- const originalAuthService = authService;
272
- authService = authService.replace(
273
- /roles: \['user'\],/g,
274
- `roles: ['admin'],
275
- permissions: ['health.rbac'],`,
90
+ let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
91
+ const originalReadme = readme;
92
+ readme = replaceReadmeManagedBlock(
93
+ readme,
94
+ ACCOUNTS_RBAC_MARKERS.start,
95
+ ACCOUNTS_RBAC_MARKERS.end,
96
+ ACCOUNTS_RBAC_ENABLED_BLOCK,
276
97
  );
277
- if (!authService.includes('permissions: user.permissions,')) {
278
- authService = authService.replace(
279
- ' roles: user.roles,',
280
- ` roles: user.roles,
281
- permissions: user.permissions,`,
282
- );
283
- }
284
- if (!authService.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],')) {
285
- authService = authService.replace(
286
- " roles: Array.isArray(payload.roles) ? payload.roles : ['user'],",
287
- ` roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
288
- permissions: Array.isArray(payload.permissions) ? payload.permissions : [],`,
289
- );
290
- }
291
- if (!authService.includes('demoPermissions: [')) {
292
- authService = authService.replace(
293
- " demoEmail: this.configService.demoEmail,",
294
- ` demoEmail: this.configService.demoEmail,
295
- demoPermissions: ['health.rbac'],`,
296
- );
297
- }
298
- if (authService !== originalAuthService) {
299
- fs.writeFileSync(authServicePath, `${authService.trimEnd()}\n`, 'utf8');
300
- changedFiles.add(authServicePath);
98
+ if (readme !== originalReadme) {
99
+ fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
100
+ changedFiles.add(readmePath);
301
101
  touched = true;
302
102
  }
303
103
 
304
- let authController = fs.readFileSync(authControllerPath, 'utf8').replace(/\r\n/g, '\n');
305
- const originalAuthController = authController;
306
- if (!authController.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],')) {
307
- authController = authController.replace(
308
- " roles: Array.isArray(payload.roles) ? payload.roles : ['user'],",
309
- ` roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
310
- permissions: Array.isArray(payload.permissions) ? payload.permissions : [],`,
311
- );
312
- }
313
- if (authController !== originalAuthController) {
314
- fs.writeFileSync(authControllerPath, `${authController.trimEnd()}\n`, 'utf8');
315
- changedFiles.add(authControllerPath);
316
- touched = true;
317
- }
318
-
319
- if (fs.existsSync(readmePath)) {
320
- let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
321
- const originalReadme = readme;
322
- const managedReadme = replaceReadmeManagedBlock(
323
- readme,
324
- JWT_AUTH_RBAC_MARKERS.start,
325
- JWT_AUTH_RBAC_MARKERS.end,
326
- JWT_AUTH_RBAC_ENABLED_BLOCK,
327
- );
328
- if (managedReadme !== readme) {
329
- readme = managedReadme;
330
- } else if (!readme.includes('- RBAC integration: demo auth tokens include `health.rbac` permission')) {
331
- const marker = 'Default demo credentials:';
332
- if (readme.includes(marker)) {
333
- readme = readme.replace(
334
- marker,
335
- '- RBAC integration: demo auth tokens include `health.rbac` permission\n\nDefault demo credentials:',
336
- );
337
- }
338
- }
339
- if (readme !== originalReadme) {
340
- fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
341
- changedFiles.add(readmePath);
342
- touched = true;
343
- }
344
- }
345
-
346
104
  if (!touched) {
347
105
  return { applied: false, reason: 'already synced' };
348
106
  }
@@ -351,35 +109,18 @@ function syncJwtRbacClaims({ rootDir, changedFiles }) {
351
109
 
352
110
  function run() {
353
111
  const rootDir = process.cwd();
354
- const changedFiles = new Set();
355
112
  const detected = detectModules(rootDir);
113
+ const changedFiles = new Set();
356
114
  const summary = [];
357
- const authPersistence = resolveAuthPersistenceStrategy(detected);
358
-
359
- if (detected.jwtAuth && authPersistence.kind === 'single') {
360
- summary.push({
361
- feature: `jwt-auth + db-adapter (current provider: ${authPersistence.strategy.providerLabel})`,
362
- result: authPersistence.strategy.apply({ rootDir, changedFiles }),
363
- });
364
- } else {
365
- const reason =
366
- authPersistence.kind === 'conflict'
367
- ? 'multiple db-adapter providers detected'
368
- : 'required components are not both available';
369
- summary.push({
370
- feature: 'jwt-auth + db-adapter (current provider: db-prisma)',
371
- result: { applied: false, reason },
372
- });
373
- }
374
115
 
375
- if (detected.jwtAuth && detected.rbac) {
116
+ if (detected.accounts && detected.rbac) {
376
117
  summary.push({
377
- feature: 'jwt-auth + rbac',
378
- result: syncJwtRbacClaims({ rootDir, changedFiles }),
118
+ feature: 'accounts + rbac',
119
+ result: syncAccountsRbac(rootDir, changedFiles),
379
120
  });
380
121
  } else {
381
122
  summary.push({
382
- feature: 'jwt-auth + rbac',
123
+ feature: 'accounts + rbac',
383
124
  result: { applied: false, reason: 'required components are not both available' },
384
125
  });
385
126
  }
@@ -396,10 +137,9 @@ function run() {
396
137
  if (changedFiles.size > 0) {
397
138
  console.log('- changed files:');
398
139
  for (const filePath of [...changedFiles].sort()) {
399
- const relative = path.relative(rootDir, filePath);
400
- console.log(` - ${relative}`);
140
+ console.log(` - ${path.relative(rootDir, filePath)}`);
401
141
  }
402
142
  }
403
143
  }
404
144
 
405
- run();
145
+ run();
@@ -1 +1,2 @@
1
- # MODULE: {{MODULE_LABEL}}
1
+ # MODULE: {{MODULE_LABEL}}
2
+
@@ -1,6 +1,6 @@
1
- ## Overview
2
-
3
- - Id: `{{MODULE_ID}}`
4
- - Category: `{{MODULE_CATEGORY}}`
5
- - Status: {{MODULE_STATUS}}
1
+ ## Overview
2
+
3
+ - Id: `{{MODULE_ID}}`
4
+ - Category: `{{MODULE_CATEGORY}}`
5
+ - Status: {{MODULE_STATUS}}
6
6
  - Description: {{MODULE_DESCRIPTION}}
@@ -0,0 +1,29 @@
1
+ ## Scope
2
+
3
+ Implemented scope:
4
+
5
+ 1. Public installer surface:
6
+ - single umbrella add-module: `accounts`
7
+ - requires `db-adapter`
8
+ 2. Internal runtime split:
9
+ - `@forgeon/accounts-contracts`
10
+ - `@forgeon/accounts-api`
11
+ - users core, auth core, auth-jwt, auth-password, email stub port/adapter
12
+ 3. API runtime:
13
+ - `POST /api/auth/register`
14
+ - `POST /api/auth/login`
15
+ - `POST /api/auth/refresh`
16
+ - `POST /api/auth/logout`
17
+ - `GET /api/auth/me`
18
+ - `POST /api/auth/change-password`
19
+ - stub endpoints for verify-email and password reset
20
+ 4. Users surface:
21
+ - owner-scoped routes under `/api/users/:id`, `/api/users/:id/profile`, `/api/users/:id/settings`
22
+ - `/users/me` is resolved through the same owner-scoped route surface
23
+ 5. Persistence and security:
24
+ - DB-backed `User`, `UserProfile`, `UserSettings`, `AuthIdentity`, `AuthCredential`, `AuthRefreshToken`
25
+ - argon2 for password and refresh-token hashing
26
+ - refresh token rotation + revoke with per-token storage rows
27
+ 6. Module checks:
28
+ - API probe endpoint: `GET /api/health/auth`
29
+ - default web probe button + result block
@@ -0,0 +1,8 @@
1
+ ## Current State
2
+
3
+ Status: implemented.
4
+
5
+ Notes:
6
+ - `accounts` is a hard consumer of the `db-adapter` capability.
7
+ - The base accounts schema does not store RBAC roles or permissions.
8
+ - Email verification and password reset are routed through an internal email stub boundary until the public `emails` module is implemented.
@@ -0,0 +1,7 @@
1
+ ## Current State
2
+
3
+ Status: planned.
4
+
5
+ Notes:
6
+ - public provider add-modules and DB-backed RBAC authz storage are intentionally deferred
7
+ - the future `emails` module is expected to plug into the existing `AccountsEmailPort`
@@ -12,5 +12,6 @@ This module is backend-first. It does not include frontend route guards. If fron
12
12
 
13
13
  Optional integration:
14
14
 
15
- - `jwt-auth` can extend demo JWT claims with RBAC permissions through `pnpm forgeon:sync-integrations`
16
- - this module does not require `jwt-auth` to install or work for header-based/manual probe checks
15
+ - `accounts` can extend demo JWT claims with RBAC permissions through `pnpm forgeon:sync-integrations`
16
+ - this module does not require `accounts` to install or work for header-based/manual probe checks
17
+
@@ -21,5 +21,6 @@ Failure path:
21
21
 
22
22
  Integration note:
23
23
 
24
- - if `jwt-auth` is also installed, the optional `auth-rbac-claims` integration can expose demo permissions inside JWT payloads
24
+ - if `accounts` is also installed, the optional `accounts-rbac` integration can expose demo permissions inside JWT payloads
25
25
  - that integration is explicit and is applied through `pnpm forgeon:sync-integrations`
26
+
@@ -20,5 +20,6 @@ Manual forbidden-path check:
20
20
 
21
21
  Optional follow-up:
22
22
 
23
- 1. install `jwt-auth` if you want RBAC claims in demo JWT payloads
23
+ 1. install `accounts` if you want RBAC claims in demo JWT payloads
24
24
  2. run `pnpm forgeon:sync-integrations`
25
+
@@ -14,4 +14,5 @@
14
14
  Not included:
15
15
 
16
16
  - no auto-patching of Swagger decorators into feature modules
17
- - no implicit integration with `jwt-auth` or other add-modules
17
+ - no implicit integration with `accounts` or other add-modules
18
+
@@ -0,0 +1,97 @@
1
+ -- CreateEnum
2
+ -- no enum is required in v1
3
+
4
+ -- AlterTable
5
+ ALTER TABLE "User"
6
+ DROP COLUMN IF EXISTS "email",
7
+ ADD COLUMN IF NOT EXISTS "status" TEXT NOT NULL DEFAULT 'active',
8
+ ADD COLUMN IF NOT EXISTS "data" JSONB,
9
+ ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3),
10
+ ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
11
+
12
+ -- CreateTable
13
+ CREATE TABLE IF NOT EXISTS "UserProfile" (
14
+ "userId" TEXT NOT NULL,
15
+ "name" TEXT,
16
+ "avatar" TEXT,
17
+ "data" JSONB,
18
+ CONSTRAINT "UserProfile_pkey" PRIMARY KEY ("userId")
19
+ );
20
+
21
+ -- CreateTable
22
+ CREATE TABLE IF NOT EXISTS "UserSettings" (
23
+ "userId" TEXT NOT NULL,
24
+ "theme" TEXT,
25
+ "locale" TEXT,
26
+ "data" JSONB,
27
+ CONSTRAINT "UserSettings_pkey" PRIMARY KEY ("userId")
28
+ );
29
+
30
+ -- CreateTable
31
+ CREATE TABLE IF NOT EXISTS "AuthIdentity" (
32
+ "id" TEXT NOT NULL,
33
+ "userId" TEXT NOT NULL,
34
+ "provider" TEXT NOT NULL,
35
+ "providerId" TEXT NOT NULL,
36
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
37
+ CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("id")
38
+ );
39
+
40
+ -- CreateTable
41
+ CREATE TABLE IF NOT EXISTS "AuthCredential" (
42
+ "id" TEXT NOT NULL,
43
+ "userId" TEXT NOT NULL,
44
+ "passwordHash" TEXT NOT NULL,
45
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
46
+ CONSTRAINT "AuthCredential_pkey" PRIMARY KEY ("id")
47
+ );
48
+
49
+ -- CreateTable
50
+ CREATE TABLE IF NOT EXISTS "AuthRefreshToken" (
51
+ "id" TEXT NOT NULL,
52
+ "userId" TEXT NOT NULL,
53
+ "tokenHash" TEXT NOT NULL,
54
+ "expiresAt" TIMESTAMP(3) NOT NULL,
55
+ "revokedAt" TIMESTAMP(3),
56
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
57
+ CONSTRAINT "AuthRefreshToken_pkey" PRIMARY KEY ("id")
58
+ );
59
+
60
+ -- Indexes
61
+ CREATE UNIQUE INDEX IF NOT EXISTS "AuthIdentity_provider_providerId_key" ON "AuthIdentity"("provider", "providerId");
62
+ CREATE UNIQUE INDEX IF NOT EXISTS "AuthCredential_userId_key" ON "AuthCredential"("userId");
63
+ CREATE INDEX IF NOT EXISTS "AuthRefreshToken_userId_createdAt_idx" ON "AuthRefreshToken"("userId", "createdAt");
64
+
65
+ -- Foreign keys
66
+ DO $$
67
+ BEGIN
68
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'UserProfile_userId_fkey') THEN
69
+ ALTER TABLE "UserProfile"
70
+ ADD CONSTRAINT "UserProfile_userId_fkey"
71
+ FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
72
+ END IF;
73
+
74
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'UserSettings_userId_fkey') THEN
75
+ ALTER TABLE "UserSettings"
76
+ ADD CONSTRAINT "UserSettings_userId_fkey"
77
+ FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
78
+ END IF;
79
+
80
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'AuthIdentity_userId_fkey') THEN
81
+ ALTER TABLE "AuthIdentity"
82
+ ADD CONSTRAINT "AuthIdentity_userId_fkey"
83
+ FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
84
+ END IF;
85
+
86
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'AuthCredential_userId_fkey') THEN
87
+ ALTER TABLE "AuthCredential"
88
+ ADD CONSTRAINT "AuthCredential_userId_fkey"
89
+ FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
90
+ END IF;
91
+
92
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'AuthRefreshToken_userId_fkey') THEN
93
+ ALTER TABLE "AuthRefreshToken"
94
+ ADD CONSTRAINT "AuthRefreshToken_userId_fkey"
95
+ FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
96
+ END IF;
97
+ END $$;