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.
- package/package.json +1 -1
- package/src/cli/add-options.test.mjs +5 -2
- package/src/cli/options.test.mjs +1 -0
- package/src/cli/prompt-select.test.mjs +1 -0
- package/src/core/docs.test.mjs +1 -0
- package/src/core/scaffold.test.mjs +1 -0
- package/src/core/validate.test.mjs +1 -0
- package/src/modules/accounts.mjs +416 -0
- package/src/modules/db-prisma.mjs +5 -9
- package/src/modules/dependencies.test.mjs +71 -29
- package/src/modules/executor.mjs +3 -2
- package/src/modules/executor.test.mjs +521 -477
- package/src/modules/files-access.mjs +9 -7
- package/src/modules/files-image.mjs +9 -7
- package/src/modules/files-local.mjs +15 -6
- package/src/modules/files-quotas.mjs +8 -6
- package/src/modules/files-s3.mjs +17 -6
- package/src/modules/files.mjs +21 -21
- package/src/modules/idempotency.test.mjs +13 -7
- package/src/modules/probes.test.mjs +4 -2
- package/src/modules/queue.mjs +9 -6
- package/src/modules/rate-limit.mjs +14 -10
- package/src/modules/rbac.mjs +12 -11
- package/src/modules/registry.mjs +22 -35
- package/src/modules/scheduler.mjs +9 -6
- package/src/modules/shared/files-runtime-wiring.mjs +81 -0
- package/src/modules/shared/patch-utils.mjs +29 -1
- package/src/modules/sync-integrations.mjs +102 -422
- package/src/modules/sync-integrations.test.mjs +32 -111
- package/src/run-add-module.test.mjs +1 -0
- package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -325
- package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
- package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
- package/templates/module-fragments/accounts/20_scope.md +29 -0
- package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
- package/templates/module-fragments/accounts/90_status_planned.md +7 -0
- package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
- package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
- package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
- package/templates/module-fragments/swagger/20_scope.md +2 -1
- package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
- package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
- package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
- package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
- package/templates/module-presets/db-prisma/apps/api/prisma/seed.ts +40 -19
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
- package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
- package/templates/module-presets/files/packages/files/package.json +1 -2
- package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
- package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
- package/templates/module-presets/files/packages/files/src/index.ts +2 -1
- package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
- package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
- package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
- package/src/modules/jwt-auth.mjs +0 -271
- package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
- package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
- package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
- package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
- package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
- package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
- package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
- package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
- /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
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
- /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
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
|
|
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
|
|
18
|
-
'
|
|
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
|
|
12
|
+
const INTEGRATION_GROUPS = [
|
|
29
13
|
{
|
|
30
|
-
id: '
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
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
|
-
|
|
42
|
-
isPending:
|
|
43
|
-
apply:
|
|
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
|
-
|
|
164
|
-
fs.existsSync(path.join(rootDir, 'packages', '
|
|
165
|
-
appModuleText.includes("from '@forgeon/
|
|
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
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
243
|
-
const
|
|
244
|
-
const
|
|
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(
|
|
258
|
-
return { applied: false, reason: '
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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 (
|
|
391
|
-
fs.writeFileSync(
|
|
392
|
-
changedFiles.add(
|
|
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
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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 (
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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: [...
|
|
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
|
|
193
|
+
const groups = INTEGRATION_GROUPS.filter(
|
|
515
194
|
(group) =>
|
|
516
195
|
group.isAvailable(detected) &&
|
|
517
|
-
group.isPending(rootDir
|
|
196
|
+
group.isPending(rootDir) &&
|
|
518
197
|
(!relatedModuleId || getGroupRelatedModules(group).includes(relatedModuleId)),
|
|
519
198
|
);
|
|
199
|
+
|
|
520
200
|
return {
|
|
521
|
-
groups:
|
|
201
|
+
groups: groups.map((group) => ({
|
|
522
202
|
id: group.id,
|
|
523
203
|
title: group.title,
|
|
524
204
|
modules: [...getGroupParticipants(group)],
|
|
525
|
-
description: [...
|
|
205
|
+
description: [...group.description],
|
|
526
206
|
})),
|
|
527
207
|
};
|
|
528
|
-
}
|
|
208
|
+
}
|