create-forgeon 0.3.16 → 0.3.18

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 (109) 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/db-prisma.mjs +5 -9
  10. package/src/modules/dependencies.test.mjs +71 -29
  11. package/src/modules/executor.mjs +3 -2
  12. package/src/modules/executor.test.mjs +521 -477
  13. package/src/modules/files-access.mjs +9 -7
  14. package/src/modules/files-image.mjs +9 -7
  15. package/src/modules/files-local.mjs +15 -6
  16. package/src/modules/files-quotas.mjs +8 -6
  17. package/src/modules/files-s3.mjs +17 -6
  18. package/src/modules/files.mjs +21 -21
  19. package/src/modules/idempotency.test.mjs +13 -7
  20. package/src/modules/probes.test.mjs +4 -2
  21. package/src/modules/queue.mjs +9 -6
  22. package/src/modules/rate-limit.mjs +14 -10
  23. package/src/modules/rbac.mjs +12 -11
  24. package/src/modules/registry.mjs +22 -35
  25. package/src/modules/scheduler.mjs +9 -6
  26. package/src/modules/shared/files-runtime-wiring.mjs +81 -0
  27. package/src/modules/shared/patch-utils.mjs +29 -1
  28. package/src/modules/sync-integrations.mjs +102 -422
  29. package/src/modules/sync-integrations.test.mjs +32 -111
  30. package/src/run-add-module.test.mjs +1 -0
  31. package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -325
  32. package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
  33. package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
  34. package/templates/module-fragments/accounts/20_scope.md +29 -0
  35. package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
  36. package/templates/module-fragments/accounts/90_status_planned.md +7 -0
  37. package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
  38. package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
  39. package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
  40. package/templates/module-fragments/swagger/20_scope.md +2 -1
  41. package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
  42. package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
  43. package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
  44. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
  45. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
  46. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
  47. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
  48. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
  49. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
  50. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
  51. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
  52. package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
  53. package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
  54. package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
  55. package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
  56. package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
  57. package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
  58. package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
  59. package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
  60. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
  61. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
  62. package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
  63. package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
  64. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
  65. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
  66. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
  67. package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
  68. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
  69. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
  70. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
  71. package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
  72. package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
  73. package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
  74. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
  75. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
  76. package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
  77. package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
  78. package/templates/module-presets/db-prisma/apps/api/prisma/seed.ts +40 -19
  79. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
  80. package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
  81. package/templates/module-presets/files/packages/files/package.json +1 -2
  82. package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
  83. package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
  84. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
  85. package/templates/module-presets/files/packages/files/src/index.ts +2 -1
  86. package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
  87. package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
  88. package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
  89. package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
  90. package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
  91. package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
  92. package/src/modules/jwt-auth.mjs +0 -271
  93. package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
  94. package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
  95. package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
  96. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
  97. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
  98. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
  99. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
  100. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
  101. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
  102. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
  103. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
  104. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
  105. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
  106. /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
  107. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
  108. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
  109. /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
@@ -1,66 +1,31 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { copyRecursive } from '../utils/fs.mjs';
4
- import { ensureLineAfter } from './shared/patch-utils.mjs';
5
3
 
6
- const PRISMA_AUTH_STORE_TEMPLATE = path.join(
7
- 'templates',
8
- 'module-presets',
9
- 'jwt-auth',
10
- 'apps',
11
- 'api',
12
- 'src',
13
- 'auth',
14
- 'prisma-auth-refresh-token.store.ts',
15
- );
4
+ const ACCOUNTS_RBAC_MARKERS = {
5
+ start: '<!-- forgeon:accounts:rbac:start -->',
6
+ end: '<!-- forgeon:accounts:rbac:end -->',
7
+ };
16
8
 
17
- const PRISMA_AUTH_MIGRATION_TEMPLATE = path.join(
18
- 'templates',
19
- 'module-presets',
20
- 'jwt-auth',
21
- 'apps',
22
- 'api',
23
- 'prisma',
24
- 'migrations',
25
- '0002_auth_refresh_token_hash',
26
- );
9
+ const ACCOUNTS_RBAC_ENABLED_BLOCK =
10
+ '- 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.';
27
11
 
28
- const AUTH_PERSISTENCE_STRATEGIES = [
12
+ const INTEGRATION_GROUPS = [
29
13
  {
30
- id: 'db-prisma',
31
- capability: 'db-adapter',
32
- providerLabel: 'db-prisma',
33
- participants: ['jwt-auth', 'db-adapter'],
34
- relatedModules: ['jwt-auth', 'db-prisma'],
14
+ id: 'accounts-rbac',
15
+ title: 'Accounts RBAC Compatibility Sync',
16
+ participants: ['accounts', 'rbac'],
17
+ relatedModules: ['accounts', 'rbac'],
35
18
  description: [
36
- 'Patch AppModule to wire AUTH_REFRESH_TOKEN_STORE to the current db-adapter implementation (today: PrismaAuthRefreshTokenStore)',
37
- 'Add apps/api/src/auth/prisma-auth-refresh-token.store.ts',
38
- 'Extend Prisma User model with refreshTokenHash and add migration 0002_auth_refresh_token_hash',
39
- 'Update JWT auth README note to reflect db-adapter-backed refresh-token persistence',
19
+ 'Add optional roles and permissions fields to accounts auth claims types',
20
+ 'Keep the base accounts schema unchanged while exposing a compatibility seam for future RBAC claims providers',
21
+ 'Update the generated README managed note for accounts + rbac compatibility',
40
22
  ],
41
- isDetected: (detected) => detected.dbPrisma,
42
- isPending: isAuthPersistencePending,
43
- apply: syncJwtDbPrisma,
23
+ isAvailable: (detected) => detected.accounts && detected.rbac,
24
+ isPending: (rootDir) => isAccountsRbacPending(rootDir),
25
+ apply: syncAccountsRbacCompatibility,
44
26
  },
45
27
  ];
46
28
 
47
- const JWT_AUTH_PERSISTENCE_MARKERS = {
48
- start: '<!-- forgeon:jwt-auth:persistence:start -->',
49
- end: '<!-- forgeon:jwt-auth:persistence:end -->',
50
- };
51
-
52
- const JWT_AUTH_RBAC_MARKERS = {
53
- start: '<!-- forgeon:jwt-auth:rbac:start -->',
54
- end: '<!-- forgeon:jwt-auth:rbac:end -->',
55
- };
56
-
57
- const JWT_AUTH_DB_PRISMA_PERSISTENCE_BLOCK = [
58
- '- refresh token persistence: enabled through the `db-adapter` capability (current provider: `db-prisma`)',
59
- '- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
60
- ].join('\n');
61
-
62
- const JWT_AUTH_RBAC_ENABLED_BLOCK = '- RBAC integration: demo auth tokens include `health.rbac` permission';
63
-
64
29
  function escapeRegExp(value) {
65
30
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
66
31
  }
@@ -72,429 +37,143 @@ function replaceReadmeManagedBlock(content, startMarker, endMarker, nextBody) {
72
37
  }
73
38
  return content.replace(pattern, `${startMarker}\n${nextBody}\n${endMarker}`);
74
39
  }
75
- function isAuthPersistencePending(rootDir) {
76
- const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
77
- const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
78
- const storePath = path.join(rootDir, 'apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts');
79
- const migrationPath = path.join(
80
- rootDir,
81
- 'apps',
82
- 'api',
83
- 'prisma',
84
- 'migrations',
85
- '0002_auth_refresh_token_hash',
86
- 'migration.sql',
87
- );
88
-
89
- if (!fs.existsSync(appModulePath) || !fs.existsSync(schemaPath)) {
90
- return false;
91
- }
92
-
93
- const appModule = fs.readFileSync(appModulePath, 'utf8');
94
- const schema = fs.readFileSync(schemaPath, 'utf8');
95
-
96
- const hasModuleWiring =
97
- appModule.includes('refreshTokenStoreProvider') &&
98
- appModule.includes('PrismaAuthRefreshTokenStore') &&
99
- appModule.includes('AUTH_REFRESH_TOKEN_STORE');
100
- const hasSchema = schema.includes('refreshTokenHash');
101
- const hasStoreFile = fs.existsSync(storePath);
102
- const hasMigration = fs.existsSync(migrationPath);
103
-
104
- return !(hasModuleWiring && hasSchema && hasStoreFile && hasMigration);
105
- }
106
-
107
- function isAuthRbacPending(rootDir) {
108
- const authContractsPath = path.join(rootDir, 'packages', 'auth-contracts', 'src', 'index.ts');
109
- const authServicePath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.service.ts');
110
- const authControllerPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.controller.ts');
111
-
112
- if (!fs.existsSync(authContractsPath) || !fs.existsSync(authServicePath) || !fs.existsSync(authControllerPath)) {
113
- return false;
114
- }
115
-
116
- const authContracts = fs.readFileSync(authContractsPath, 'utf8');
117
- const authService = fs.readFileSync(authServicePath, 'utf8');
118
- const authController = fs.readFileSync(authControllerPath, 'utf8');
119
-
120
- const hasContracts = authContracts.includes('permissions?: string[];');
121
- const hasDemoClaims = authService.includes("permissions: ['health.rbac']");
122
- const hasPayloadClaims = authService.includes('permissions: user.permissions,');
123
- const hasRefreshClaims = authService.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],');
124
- const hasControllerClaims =
125
- authController.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],');
126
-
127
- return !(hasContracts && hasDemoClaims && hasPayloadClaims && hasRefreshClaims && hasControllerClaims);
128
- }
129
-
130
- const INTEGRATION_GROUPS = [
131
- {
132
- id: 'auth-persistence',
133
- title: 'Auth Persistence Integration',
134
- participants: ['jwt-auth', 'db-adapter'],
135
- relatedModules: ['jwt-auth', 'db-prisma'],
136
- description: (detected) => getAuthPersistenceDescription(detected),
137
- isAvailable: (detected) => detected.jwtAuth && hasSingleAuthPersistenceStrategy(detected),
138
- isPending: (rootDir, detected) => isAuthPersistencePendingForDetected(rootDir, detected),
139
- apply: applyAuthPersistenceSync,
140
- },
141
- {
142
- id: 'auth-rbac-claims',
143
- title: 'Auth Claims Integration',
144
- participants: ['jwt-auth', 'rbac'],
145
- relatedModules: ['jwt-auth', 'rbac'],
146
- description: [
147
- 'Extend AuthUser with optional permissions in @forgeon/auth-contracts',
148
- 'Add demo RBAC claims to jwt-auth login and token payloads',
149
- 'Expose permissions in auth refresh and /me responses',
150
- 'Update JWT auth README note about RBAC demo claims',
151
- ],
152
- isAvailable: (detected) => detected.jwtAuth && detected.rbac,
153
- isPending: (rootDir) => isAuthRbacPending(rootDir),
154
- apply: syncJwtRbacClaims,
155
- },
156
- ];
157
40
 
158
41
  function detectModules(rootDir) {
159
42
  const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
160
43
  const appModuleText = fs.existsSync(appModulePath) ? fs.readFileSync(appModulePath, 'utf8') : '';
161
44
 
162
45
  return {
163
- jwtAuth:
164
- fs.existsSync(path.join(rootDir, 'packages', 'auth-api', 'package.json')) ||
165
- appModuleText.includes("from '@forgeon/auth-api'"),
46
+ accounts:
47
+ fs.existsSync(path.join(rootDir, 'packages', 'accounts-api', 'package.json')) ||
48
+ appModuleText.includes("from '@forgeon/accounts-api'"),
166
49
  rbac:
167
50
  fs.existsSync(path.join(rootDir, 'packages', 'rbac', 'package.json')) ||
168
51
  appModuleText.includes("from '@forgeon/rbac'"),
169
- dbPrisma:
170
- fs.existsSync(path.join(rootDir, 'packages', 'db-prisma', 'package.json')) ||
171
- appModuleText.includes("from '@forgeon/db-prisma'"),
172
52
  };
173
53
  }
174
54
 
175
- function resolveAuthPersistenceStrategy(detected) {
176
- const matched = AUTH_PERSISTENCE_STRATEGIES.filter((strategy) => strategy.isDetected(detected));
177
- if (matched.length === 0) {
178
- return { kind: 'none' };
179
- }
180
- if (matched.length > 1) {
181
- return { kind: 'conflict', strategies: matched };
182
- }
183
- return { kind: 'single', strategy: matched[0] };
184
- }
185
-
186
- function hasSingleAuthPersistenceStrategy(detected) {
187
- return resolveAuthPersistenceStrategy(detected).kind === 'single';
188
- }
189
-
190
- function getAuthPersistenceDescription(detected) {
191
- const resolved = resolveAuthPersistenceStrategy(detected);
192
- if (resolved.kind === 'single') {
193
- return [...resolved.strategy.description];
194
- }
195
- return [
196
- 'Use the current db-adapter provider strategy to wire refresh-token persistence.',
197
- 'A supported db-adapter provider must be installed before this integration can apply.',
198
- ];
199
- }
55
+ function isAccountsRbacPending(rootDir) {
56
+ const contractsPath = path.join(rootDir, 'packages', 'accounts-contracts', 'src', 'index.ts');
57
+ const authTypesPath = path.join(rootDir, 'packages', 'accounts-api', 'src', 'auth.types.ts');
58
+ const readmePath = path.join(rootDir, 'README.md');
200
59
 
201
- function isAuthPersistencePendingForDetected(rootDir, detected) {
202
- const resolved = resolveAuthPersistenceStrategy(detected);
203
- if (resolved.kind !== 'single') {
60
+ if (!fs.existsSync(contractsPath) || !fs.existsSync(authTypesPath) || !fs.existsSync(readmePath)) {
204
61
  return false;
205
62
  }
206
- return resolved.strategy.isPending(rootDir);
207
- }
208
-
209
- function applyAuthPersistenceSync({ rootDir, packageRoot, changedFiles }) {
210
- const detected = detectModules(rootDir);
211
- const resolved = resolveAuthPersistenceStrategy(detected);
212
- if (resolved.kind === 'none') {
213
- return { applied: false, reason: 'no supported db-adapter provider detected' };
214
- }
215
- if (resolved.kind === 'conflict') {
216
- return { applied: false, reason: 'multiple db-adapter providers detected' };
217
- }
218
- return resolved.strategy.apply({ rootDir, packageRoot, changedFiles });
219
- }
220
63
 
221
- function getGroupParticipants(group) {
222
- return Array.isArray(group.participants) && group.participants.length > 0
223
- ? group.participants
224
- : Array.isArray(group.modules)
225
- ? group.modules
226
- : [];
227
- }
64
+ const contracts = fs.readFileSync(contractsPath, 'utf8');
65
+ const authTypes = fs.readFileSync(authTypesPath, 'utf8');
66
+ const readme = fs.readFileSync(readmePath, 'utf8');
228
67
 
229
- function getGroupRelatedModules(group) {
230
- return Array.isArray(group.relatedModules) && group.relatedModules.length > 0
231
- ? group.relatedModules
232
- : getGroupParticipants(group);
233
- }
68
+ const contractsReady =
69
+ contracts.includes('roles?: string[];') &&
70
+ contracts.includes('permissions?: string[];');
71
+ const authTypesReady =
72
+ authTypes.includes('roles?: string[];') &&
73
+ authTypes.includes('permissions?: string[];');
74
+ const readmeReady = readme.includes(ACCOUNTS_RBAC_ENABLED_BLOCK);
234
75
 
235
- function getGroupDescription(group, detected) {
236
- if (typeof group.description === 'function') {
237
- return group.description(detected);
238
- }
239
- return Array.isArray(group.description) ? group.description : [];
76
+ return !(contractsReady && authTypesReady && readmeReady);
240
77
  }
241
78
 
242
- function syncJwtDbPrisma({ rootDir, packageRoot, changedFiles }) {
243
- const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
244
- const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
79
+ function syncAccountsRbacCompatibility({ rootDir, changedFiles }) {
80
+ const contractsPath = path.join(rootDir, 'packages', 'accounts-contracts', 'src', 'index.ts');
81
+ const authTypesPath = path.join(rootDir, 'packages', 'accounts-api', 'src', 'auth.types.ts');
245
82
  const readmePath = path.join(rootDir, 'README.md');
246
- const storePath = path.join(rootDir, 'apps', 'api', 'src', 'auth', 'prisma-auth-refresh-token.store.ts');
247
- const migrationPath = path.join(
248
- rootDir,
249
- 'apps',
250
- 'api',
251
- 'prisma',
252
- 'migrations',
253
- '0002_auth_refresh_token_hash',
254
- 'migration.sql',
255
- );
256
83
 
257
- if (!fs.existsSync(appModulePath) || !fs.existsSync(schemaPath)) {
258
- return { applied: false, reason: 'app module or prisma schema is missing' };
84
+ if (!fs.existsSync(contractsPath) || !fs.existsSync(authTypesPath) || !fs.existsSync(readmePath)) {
85
+ return { applied: false, reason: 'accounts package files are missing' };
259
86
  }
260
87
 
261
88
  let touched = false;
262
89
 
263
- if (!fs.existsSync(storePath)) {
264
- const storeSource = path.join(packageRoot, PRISMA_AUTH_STORE_TEMPLATE);
265
- if (!fs.existsSync(storeSource)) {
266
- return { applied: false, reason: 'jwt-auth prisma store template is missing' };
267
- }
268
- fs.mkdirSync(path.dirname(storePath), { recursive: true });
269
- copyRecursive(storeSource, storePath);
270
- changedFiles.add(storePath);
271
- touched = true;
272
- }
273
-
274
- let appModule = fs.readFileSync(appModulePath, 'utf8').replace(/\r\n/g, '\n');
275
- const originalAppModule = appModule;
276
-
277
- if (!appModule.includes("AUTH_REFRESH_TOKEN_STORE, authConfig")) {
278
- appModule = appModule.replace(
279
- /import\s*\{\s*authConfig,\s*authEnvSchema,\s*ForgeonAuthModule\s*\}\s*from '@forgeon\/auth-api';/m,
280
- "import { AUTH_REFRESH_TOKEN_STORE, authConfig, authEnvSchema, ForgeonAuthModule } from '@forgeon/auth-api';",
281
- );
282
- }
283
-
284
- const storeImportLine = "import { PrismaAuthRefreshTokenStore } from './auth/prisma-auth-refresh-token.store';";
285
- if (!appModule.includes(storeImportLine)) {
286
- appModule = ensureLineAfter(
287
- appModule,
288
- "import { HealthController } from './health/health.controller';",
289
- storeImportLine,
90
+ let contracts = fs.readFileSync(contractsPath, 'utf8').replace(/\r\n/g, '\n');
91
+ const originalContracts = contracts;
92
+ if (!contracts.includes('roles?: string[];')) {
93
+ contracts = contracts.replace(
94
+ " type: 'access';",
95
+ " type: 'access';\n roles?: string[];\n permissions?: string[];",
290
96
  );
291
97
  }
292
-
293
- if (!appModule.includes('refreshTokenStoreProvider')) {
294
- appModule = appModule.replace(
295
- /ForgeonAuthModule\.register\(\),/m,
296
- `ForgeonAuthModule.register({
297
- imports: [DbPrismaModule],
298
- refreshTokenStoreProvider: {
299
- provide: AUTH_REFRESH_TOKEN_STORE,
300
- useClass: PrismaAuthRefreshTokenStore,
301
- },
302
- }),`,
98
+ if (!contracts.includes("jti: string;\n type: 'refresh';\n roles?: string[];")) {
99
+ contracts = contracts.replace(
100
+ " jti: string;\n type: 'refresh';",
101
+ " jti: string;\n type: 'refresh';\n roles?: string[];\n permissions?: string[];",
303
102
  );
304
103
  }
305
-
306
- if (appModule !== originalAppModule) {
307
- fs.writeFileSync(appModulePath, `${appModule.trimEnd()}\n`, 'utf8');
308
- changedFiles.add(appModulePath);
309
- touched = true;
310
- }
311
-
312
- let schema = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
313
- const originalSchema = schema;
314
- if (!schema.includes('refreshTokenHash')) {
315
- schema = schema.replace(/email\s+String\s+@unique/g, 'email String @unique\n refreshTokenHash String?');
316
- }
317
- if (schema !== originalSchema) {
318
- fs.writeFileSync(schemaPath, `${schema.trimEnd()}\n`, 'utf8');
319
- changedFiles.add(schemaPath);
320
- touched = true;
321
- }
322
-
323
- if (!fs.existsSync(migrationPath)) {
324
- const migrationSource = path.join(packageRoot, PRISMA_AUTH_MIGRATION_TEMPLATE);
325
- if (!fs.existsSync(migrationSource)) {
326
- return { applied: false, reason: 'jwt-auth migration template is missing' };
327
- }
328
- const migrationDir = path.dirname(migrationPath);
329
- fs.mkdirSync(path.dirname(migrationDir), { recursive: true });
330
- copyRecursive(migrationSource, migrationDir);
331
- changedFiles.add(migrationPath);
104
+ if (contracts !== originalContracts) {
105
+ fs.writeFileSync(contractsPath, `${contracts.trimEnd()}\n`, 'utf8');
106
+ changedFiles.add(contractsPath);
332
107
  touched = true;
333
108
  }
334
109
 
335
- if (fs.existsSync(readmePath)) {
336
- let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
337
- const originalReadme = readme;
338
- const managedReadme = replaceReadmeManagedBlock(
339
- readme,
340
- JWT_AUTH_PERSISTENCE_MARKERS.start,
341
- JWT_AUTH_PERSISTENCE_MARKERS.end,
342
- JWT_AUTH_DB_PRISMA_PERSISTENCE_BLOCK,
110
+ let authTypes = fs.readFileSync(authTypesPath, 'utf8').replace(/\r\n/g, '\n');
111
+ const originalAuthTypes = authTypes;
112
+ if (!authTypes.includes('roles?: string[];')) {
113
+ authTypes = authTypes.replace(
114
+ " exp?: number;",
115
+ " exp?: number;\n roles?: string[];\n permissions?: string[];",
343
116
  );
344
- if (managedReadme !== readme) {
345
- readme = managedReadme;
346
- } else {
347
- readme = readme.replace(
348
- '- refresh token persistence: disabled by default (stateless mode; enable it later through a `db-adapter` provider + integration sync)',
349
- '- refresh token persistence: enabled through the `db-adapter` capability (current provider: `db-prisma`)',
350
- );
351
- readme = readme.replace(
352
- /- to enable persistence later:[\s\S]*?2\. run `pnpm forgeon:sync-integrations` to wire auth persistence to the active DB adapter implementation\./m,
353
- '- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
354
- );
355
- }
356
- if (readme !== originalReadme) {
357
- fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
358
- changedFiles.add(readmePath);
359
- touched = true;
360
- }
361
- }
362
-
363
- if (!touched) {
364
- return { applied: false, reason: 'already synced' };
365
- }
366
- return { applied: true };
367
- }
368
-
369
- function syncJwtRbacClaims({ rootDir, changedFiles }) {
370
- const authContractsPath = path.join(rootDir, 'packages', 'auth-contracts', 'src', 'index.ts');
371
- const authServicePath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.service.ts');
372
- const authControllerPath = path.join(rootDir, 'packages', 'auth-api', 'src', 'auth.controller.ts');
373
- const readmePath = path.join(rootDir, 'README.md');
374
-
375
- if (!fs.existsSync(authContractsPath) || !fs.existsSync(authServicePath) || !fs.existsSync(authControllerPath)) {
376
- return { applied: false, reason: 'auth package files are missing' };
377
117
  }
378
-
379
- let touched = false;
380
-
381
- let authContracts = fs.readFileSync(authContractsPath, 'utf8').replace(/\r\n/g, '\n');
382
- const originalAuthContracts = authContracts;
383
- if (!authContracts.includes('permissions?: string[];')) {
384
- authContracts = authContracts.replace(
385
- ' roles: string[];',
386
- ` roles: string[];
387
- permissions?: string[];`,
118
+ const refreshPattern = /export interface AuthRefreshTokenPayload[\s\S]*?\{[\s\S]*?exp\?: number;[\s\S]*?\}/m;
119
+ const refreshMatch = authTypes.match(refreshPattern)?.[0] ?? '';
120
+ if (refreshMatch && !refreshMatch.includes('roles?: string[];')) {
121
+ authTypes = authTypes.replace(
122
+ "export interface AuthRefreshTokenPayload extends AuthRefreshClaims {\n iat?: number;\n exp?: number;\n}",
123
+ "export interface AuthRefreshTokenPayload extends AuthRefreshClaims {\n iat?: number;\n exp?: number;\n roles?: string[];\n permissions?: string[];\n}",
388
124
  );
389
125
  }
390
- if (authContracts !== originalAuthContracts) {
391
- fs.writeFileSync(authContractsPath, `${authContracts.trimEnd()}\n`, 'utf8');
392
- changedFiles.add(authContractsPath);
126
+ if (authTypes !== originalAuthTypes) {
127
+ fs.writeFileSync(authTypesPath, `${authTypes.trimEnd()}\n`, 'utf8');
128
+ changedFiles.add(authTypesPath);
393
129
  touched = true;
394
130
  }
395
131
 
396
- let authService = fs.readFileSync(authServicePath, 'utf8').replace(/\r\n/g, '\n');
397
- const originalAuthService = authService;
398
- authService = authService.replace(
399
- /roles: \['user'\],/g,
400
- `roles: ['admin'],
401
- permissions: ['health.rbac'],`,
132
+ let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
133
+ const originalReadme = readme;
134
+ readme = replaceReadmeManagedBlock(
135
+ readme,
136
+ ACCOUNTS_RBAC_MARKERS.start,
137
+ ACCOUNTS_RBAC_MARKERS.end,
138
+ ACCOUNTS_RBAC_ENABLED_BLOCK,
402
139
  );
403
- if (!authService.includes('permissions: user.permissions,')) {
404
- authService = authService.replace(
405
- ' roles: user.roles,',
406
- ` roles: user.roles,
407
- permissions: user.permissions,`,
408
- );
409
- }
410
- if (!authService.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],')) {
411
- authService = authService.replace(
412
- " roles: Array.isArray(payload.roles) ? payload.roles : ['user'],",
413
- ` roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
414
- permissions: Array.isArray(payload.permissions) ? payload.permissions : [],`,
415
- );
416
- }
417
- if (!authService.includes('demoPermissions: [')) {
418
- authService = authService.replace(
419
- " demoEmail: this.configService.demoEmail,",
420
- ` demoEmail: this.configService.demoEmail,
421
- demoPermissions: ['health.rbac'],`,
422
- );
423
- }
424
- if (authService !== originalAuthService) {
425
- fs.writeFileSync(authServicePath, `${authService.trimEnd()}\n`, 'utf8');
426
- changedFiles.add(authServicePath);
140
+ if (readme !== originalReadme) {
141
+ fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
142
+ changedFiles.add(readmePath);
427
143
  touched = true;
428
144
  }
429
145
 
430
- let authController = fs.readFileSync(authControllerPath, 'utf8').replace(/\r\n/g, '\n');
431
- const originalAuthController = authController;
432
- if (!authController.includes('permissions: Array.isArray(payload.permissions) ? payload.permissions : [],')) {
433
- authController = authController.replace(
434
- " roles: Array.isArray(payload.roles) ? payload.roles : ['user'],",
435
- ` roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
436
- permissions: Array.isArray(payload.permissions) ? payload.permissions : [],`,
437
- );
438
- }
439
- if (authController !== originalAuthController) {
440
- fs.writeFileSync(authControllerPath, `${authController.trimEnd()}\n`, 'utf8');
441
- changedFiles.add(authControllerPath);
442
- touched = true;
443
- }
444
-
445
- if (fs.existsSync(readmePath)) {
446
- let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
447
- const originalReadme = readme;
448
- const managedReadme = replaceReadmeManagedBlock(
449
- readme,
450
- JWT_AUTH_RBAC_MARKERS.start,
451
- JWT_AUTH_RBAC_MARKERS.end,
452
- JWT_AUTH_RBAC_ENABLED_BLOCK,
453
- );
454
- if (managedReadme !== readme) {
455
- readme = managedReadme;
456
- } else if (!readme.includes('- RBAC integration: demo auth tokens include `health.rbac` permission')) {
457
- const marker = 'Default demo credentials:';
458
- if (readme.includes(marker)) {
459
- readme = readme.replace(
460
- marker,
461
- '- RBAC integration: demo auth tokens include `health.rbac` permission\n\nDefault demo credentials:',
462
- );
463
- }
464
- }
465
- if (readme !== originalReadme) {
466
- fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
467
- changedFiles.add(readmePath);
468
- touched = true;
469
- }
470
- }
471
-
472
146
  if (!touched) {
473
147
  return { applied: false, reason: 'already synced' };
474
148
  }
475
149
  return { applied: true };
476
150
  }
477
151
 
478
- export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
152
+ function getGroupParticipants(group) {
153
+ return Array.isArray(group.participants) ? group.participants : [];
154
+ }
155
+
156
+ function getGroupRelatedModules(group) {
157
+ return Array.isArray(group.relatedModules) ? group.relatedModules : getGroupParticipants(group);
158
+ }
159
+
160
+ export function syncIntegrations({ targetRoot, groupIds = null }) {
479
161
  const rootDir = path.resolve(targetRoot);
480
162
  const changedFiles = new Set();
481
163
  const detected = detectModules(rootDir);
482
- const summary = [];
483
164
  const available = INTEGRATION_GROUPS.filter(
484
- (group) => group.isAvailable(detected) && group.isPending(rootDir, detected),
165
+ (group) => group.isAvailable(detected) && group.isPending(rootDir),
485
166
  );
486
167
  const selected = Array.isArray(groupIds)
487
168
  ? available.filter((group) => groupIds.includes(group.id))
488
169
  : available;
489
170
 
490
- for (const group of selected) {
491
- summary.push({
492
- id: group.id,
493
- title: group.title,
494
- modules: getGroupParticipants(group),
495
- result: group.apply({ rootDir, packageRoot, changedFiles }),
496
- });
497
- }
171
+ const summary = selected.map((group) => ({
172
+ id: group.id,
173
+ title: group.title,
174
+ modules: [...getGroupParticipants(group)],
175
+ result: group.apply({ rootDir, changedFiles }),
176
+ }));
498
177
 
499
178
  return {
500
179
  summary,
@@ -502,7 +181,7 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
502
181
  id: group.id,
503
182
  title: group.title,
504
183
  modules: [...getGroupParticipants(group)],
505
- description: [...getGroupDescription(group, detected)],
184
+ description: [...group.description],
506
185
  })),
507
186
  changedFiles: [...changedFiles].sort().map((filePath) => path.relative(rootDir, filePath)),
508
187
  };
@@ -511,18 +190,19 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
511
190
  export function scanIntegrations({ targetRoot, relatedModuleId = null }) {
512
191
  const rootDir = path.resolve(targetRoot);
513
192
  const detected = detectModules(rootDir);
514
- const available = INTEGRATION_GROUPS.filter(
193
+ const groups = INTEGRATION_GROUPS.filter(
515
194
  (group) =>
516
195
  group.isAvailable(detected) &&
517
- group.isPending(rootDir, detected) &&
196
+ group.isPending(rootDir) &&
518
197
  (!relatedModuleId || getGroupRelatedModules(group).includes(relatedModuleId)),
519
198
  );
199
+
520
200
  return {
521
- groups: available.map((group) => ({
201
+ groups: groups.map((group) => ({
522
202
  id: group.id,
523
203
  title: group.title,
524
204
  modules: [...getGroupParticipants(group)],
525
- description: [...getGroupDescription(group, detected)],
205
+ description: [...group.description],
526
206
  })),
527
207
  };
528
- }
208
+ }