create-forgeon 0.3.15 → 0.3.17

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