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.
- package/package.json +4 -2
- 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 +80 -40
- package/src/core/scaffold.test.mjs +100 -0
- package/src/core/validate.test.mjs +1 -0
- package/src/modules/accounts.mjs +416 -0
- package/src/modules/db-prisma.mjs +23 -55
- package/src/modules/dependencies.test.mjs +71 -29
- package/src/modules/executor.mjs +3 -2
- package/src/modules/executor.test.mjs +631 -500
- package/src/modules/files-access.mjs +36 -105
- package/src/modules/files-image.mjs +35 -107
- package/src/modules/files-local.mjs +15 -6
- package/src/modules/files-quotas.mjs +75 -93
- package/src/modules/files-s3.mjs +17 -6
- package/src/modules/files.mjs +56 -125
- package/src/modules/i18n.mjs +17 -121
- package/src/modules/idempotency.test.mjs +180 -0
- package/src/modules/logger.mjs +0 -9
- package/src/modules/probes.test.mjs +204 -0
- package/src/modules/queue.mjs +325 -440
- package/src/modules/rate-limit.mjs +36 -76
- package/src/modules/rbac.mjs +39 -78
- package/src/modules/registry.mjs +22 -35
- package/src/modules/scheduler.mjs +51 -171
- package/src/modules/shared/files-runtime-wiring.mjs +81 -0
- package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
- package/src/modules/shared/patch-utils.mjs +29 -1
- package/src/modules/shared/probes.mjs +235 -0
- package/src/modules/sync-integrations.mjs +109 -396
- package/src/modules/sync-integrations.test.mjs +141 -0
- package/src/run-add-module.test.mjs +154 -0
- package/templates/base/README.md +7 -55
- package/templates/base/apps/web/src/App.tsx +70 -42
- package/templates/base/apps/web/src/probes.ts +61 -0
- package/templates/base/apps/web/src/styles.css +86 -25
- package/templates/base/package.json +21 -15
- package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -281
- 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/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-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
- 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/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
- package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
- package/src/modules/jwt-auth.mjs +0 -390
- package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
- package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
- package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
- package/templates/base/docs/AI/PROJECT.md +0 -43
- package/templates/base/docs/AI/ROADMAP.md +0 -171
- package/templates/base/docs/AI/TASKS.md +0 -60
- package/templates/base/docs/AI/VALIDATION.md +0 -31
- package/templates/base/docs/README.md +0 -18
- 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/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
- /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,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
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
|
|
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
|
|
17
|
-
'
|
|
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
|
|
12
|
+
const INTEGRATION_GROUPS = [
|
|
28
13
|
{
|
|
29
|
-
id: '
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
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
|
-
|
|
41
|
-
isPending:
|
|
42
|
-
apply:
|
|
23
|
+
isAvailable: (detected) => detected.accounts && detected.rbac,
|
|
24
|
+
isPending: (rootDir) => isAccountsRbacPending(rootDir),
|
|
25
|
+
apply: syncAccountsRbacCompatibility,
|
|
43
26
|
},
|
|
44
27
|
];
|
|
45
28
|
|
|
46
|
-
function
|
|
47
|
-
|
|
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
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
147
|
-
fs.existsSync(path.join(rootDir, 'packages', '
|
|
148
|
-
appModuleText.includes("from '@forgeon/
|
|
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
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
const
|
|
194
|
-
const
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
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(
|
|
241
|
-
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' };
|
|
242
86
|
}
|
|
243
87
|
|
|
244
88
|
let touched = false;
|
|
245
89
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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 (
|
|
364
|
-
fs.writeFileSync(
|
|
365
|
-
changedFiles.add(
|
|
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
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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 (
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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: [...
|
|
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
|
|
193
|
+
const groups = INTEGRATION_GROUPS.filter(
|
|
482
194
|
(group) =>
|
|
483
195
|
group.isAvailable(detected) &&
|
|
484
|
-
group.isPending(rootDir
|
|
196
|
+
group.isPending(rootDir) &&
|
|
485
197
|
(!relatedModuleId || getGroupRelatedModules(group).includes(relatedModuleId)),
|
|
486
198
|
);
|
|
199
|
+
|
|
487
200
|
return {
|
|
488
|
-
groups:
|
|
201
|
+
groups: groups.map((group) => ({
|
|
489
202
|
id: group.id,
|
|
490
203
|
title: group.title,
|
|
491
204
|
modules: [...getGroupParticipants(group)],
|
|
492
|
-
description: [...
|
|
205
|
+
description: [...group.description],
|
|
493
206
|
})),
|
|
494
207
|
};
|
|
495
|
-
}
|
|
208
|
+
}
|