create-forgeon 0.3.3 → 0.3.4
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/modules/executor.test.mjs +3 -0
- package/src/modules/rate-limit.mjs +2 -0
- package/src/modules/sync-integrations.mjs +81 -14
- package/templates/base/apps/web/src/App.tsx +4 -1
- package/templates/base/scripts/forgeon-sync-integrations.mjs +29 -4
- package/templates/module-presets/i18n/apps/web/src/App.tsx +2 -1
package/package.json
CHANGED
|
@@ -70,10 +70,13 @@ function assertRateLimitWiring(projectRoot) {
|
|
|
70
70
|
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
71
71
|
'utf8',
|
|
72
72
|
);
|
|
73
|
+
assert.match(healthController, /import \{ Header \} from '@nestjs\/common';/);
|
|
74
|
+
assert.match(healthController, /@Header\('Cache-Control', 'no-store, no-cache, must-revalidate'\)/);
|
|
73
75
|
assert.match(healthController, /@Get\('rate-limit'\)/);
|
|
74
76
|
assert.match(healthController, /TOO_MANY_REQUESTS/);
|
|
75
77
|
|
|
76
78
|
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
79
|
+
assert.match(appTsx, /cache: 'no-store'/);
|
|
77
80
|
assert.match(appTsx, /Check rate limit \(click repeatedly\)/);
|
|
78
81
|
assert.match(appTsx, /Rate limit probe response/);
|
|
79
82
|
|
|
@@ -120,8 +120,10 @@ function patchHealthController(targetRoot) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
123
|
+
content = ensureImportLine(content, "import { Header } from '@nestjs/common';");
|
|
123
124
|
if (!content.includes("@Get('rate-limit')")) {
|
|
124
125
|
const method = `
|
|
126
|
+
@Header('Cache-Control', 'no-store, no-cache, must-revalidate')
|
|
125
127
|
@Get('rate-limit')
|
|
126
128
|
getRateLimitProbe() {
|
|
127
129
|
return {
|
|
@@ -24,6 +24,25 @@ const PRISMA_AUTH_MIGRATION_TEMPLATE = path.join(
|
|
|
24
24
|
'0002_auth_refresh_token_hash',
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
+
const AUTH_PERSISTENCE_STRATEGIES = [
|
|
28
|
+
{
|
|
29
|
+
id: 'db-prisma',
|
|
30
|
+
capability: 'db-adapter',
|
|
31
|
+
providerLabel: 'db-prisma',
|
|
32
|
+
participants: ['jwt-auth', 'db-adapter'],
|
|
33
|
+
relatedModules: ['jwt-auth', 'db-prisma'],
|
|
34
|
+
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',
|
|
39
|
+
],
|
|
40
|
+
isDetected: (detected) => detected.dbPrisma,
|
|
41
|
+
isPending: isAuthPersistencePending,
|
|
42
|
+
apply: syncJwtDbPrisma,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
27
46
|
function ensureLineAfter(content, anchorLine, lineToInsert) {
|
|
28
47
|
if (content.includes(lineToInsert)) {
|
|
29
48
|
return content;
|
|
@@ -97,15 +116,10 @@ const INTEGRATION_GROUPS = [
|
|
|
97
116
|
title: 'Auth Persistence Integration',
|
|
98
117
|
participants: ['jwt-auth', 'db-adapter'],
|
|
99
118
|
relatedModules: ['jwt-auth', 'db-prisma'],
|
|
100
|
-
description:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
'Update JWT auth README note to reflect db-adapter-backed refresh-token persistence',
|
|
105
|
-
],
|
|
106
|
-
isAvailable: (detected) => detected.jwtAuth && detected.dbPrisma,
|
|
107
|
-
isPending: (rootDir) => isAuthPersistencePending(rootDir),
|
|
108
|
-
apply: syncJwtDbPrisma,
|
|
119
|
+
description: (detected) => getAuthPersistenceDescription(detected),
|
|
120
|
+
isAvailable: (detected) => detected.jwtAuth && hasSingleAuthPersistenceStrategy(detected),
|
|
121
|
+
isPending: (rootDir, detected) => isAuthPersistencePendingForDetected(rootDir, detected),
|
|
122
|
+
apply: applyAuthPersistenceSync,
|
|
109
123
|
},
|
|
110
124
|
{
|
|
111
125
|
id: 'auth-rbac-claims',
|
|
@@ -141,6 +155,52 @@ function detectModules(rootDir) {
|
|
|
141
155
|
};
|
|
142
156
|
}
|
|
143
157
|
|
|
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
|
+
}
|
|
183
|
+
|
|
184
|
+
function isAuthPersistencePendingForDetected(rootDir, detected) {
|
|
185
|
+
const resolved = resolveAuthPersistenceStrategy(detected);
|
|
186
|
+
if (resolved.kind !== 'single') {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return resolved.strategy.isPending(rootDir);
|
|
190
|
+
}
|
|
191
|
+
|
|
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
|
+
}
|
|
203
|
+
|
|
144
204
|
function getGroupParticipants(group) {
|
|
145
205
|
return Array.isArray(group.participants) && group.participants.length > 0
|
|
146
206
|
? group.participants
|
|
@@ -155,6 +215,13 @@ function getGroupRelatedModules(group) {
|
|
|
155
215
|
: getGroupParticipants(group);
|
|
156
216
|
}
|
|
157
217
|
|
|
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
|
+
|
|
158
225
|
function syncJwtDbPrisma({ rootDir, packageRoot, changedFiles }) {
|
|
159
226
|
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
160
227
|
const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
|
|
@@ -381,7 +448,7 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
|
|
|
381
448
|
const detected = detectModules(rootDir);
|
|
382
449
|
const summary = [];
|
|
383
450
|
const available = INTEGRATION_GROUPS.filter(
|
|
384
|
-
(group) => group.isAvailable(detected) && group.isPending(rootDir),
|
|
451
|
+
(group) => group.isAvailable(detected) && group.isPending(rootDir, detected),
|
|
385
452
|
);
|
|
386
453
|
const selected = Array.isArray(groupIds)
|
|
387
454
|
? available.filter((group) => groupIds.includes(group.id))
|
|
@@ -391,7 +458,7 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
|
|
|
391
458
|
summary.push({
|
|
392
459
|
id: group.id,
|
|
393
460
|
title: group.title,
|
|
394
|
-
modules: group
|
|
461
|
+
modules: getGroupParticipants(group),
|
|
395
462
|
result: group.apply({ rootDir, packageRoot, changedFiles }),
|
|
396
463
|
});
|
|
397
464
|
}
|
|
@@ -402,7 +469,7 @@ export function syncIntegrations({ targetRoot, packageRoot, groupIds = null }) {
|
|
|
402
469
|
id: group.id,
|
|
403
470
|
title: group.title,
|
|
404
471
|
modules: [...getGroupParticipants(group)],
|
|
405
|
-
description: [...group
|
|
472
|
+
description: [...getGroupDescription(group, detected)],
|
|
406
473
|
})),
|
|
407
474
|
changedFiles: [...changedFiles].sort().map((filePath) => path.relative(rootDir, filePath)),
|
|
408
475
|
};
|
|
@@ -414,7 +481,7 @@ export function scanIntegrations({ targetRoot, relatedModuleId = null }) {
|
|
|
414
481
|
const available = INTEGRATION_GROUPS.filter(
|
|
415
482
|
(group) =>
|
|
416
483
|
group.isAvailable(detected) &&
|
|
417
|
-
group.isPending(rootDir) &&
|
|
484
|
+
group.isPending(rootDir, detected) &&
|
|
418
485
|
(!relatedModuleId || getGroupRelatedModules(group).includes(relatedModuleId)),
|
|
419
486
|
);
|
|
420
487
|
return {
|
|
@@ -422,7 +489,7 @@ export function scanIntegrations({ targetRoot, relatedModuleId = null }) {
|
|
|
422
489
|
id: group.id,
|
|
423
490
|
title: group.title,
|
|
424
491
|
modules: [...getGroupParticipants(group)],
|
|
425
|
-
description: [...group
|
|
492
|
+
description: [...getGroupDescription(group, detected)],
|
|
426
493
|
})),
|
|
427
494
|
};
|
|
428
495
|
}
|
|
@@ -13,7 +13,10 @@ export default function App() {
|
|
|
13
13
|
const [networkError, setNetworkError] = useState<string | null>(null);
|
|
14
14
|
|
|
15
15
|
const requestProbe = async (url: string, init?: RequestInit): Promise<ProbeResult> => {
|
|
16
|
-
const response = await fetch(url,
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
...(init ?? {}),
|
|
18
|
+
cache: 'no-store',
|
|
19
|
+
});
|
|
17
20
|
let body: unknown = null;
|
|
18
21
|
|
|
19
22
|
try {
|
|
@@ -45,6 +45,15 @@ ALTER TABLE "User"
|
|
|
45
45
|
ADD COLUMN "refreshTokenHash" TEXT;
|
|
46
46
|
`;
|
|
47
47
|
|
|
48
|
+
const AUTH_PERSISTENCE_STRATEGIES = [
|
|
49
|
+
{
|
|
50
|
+
id: 'db-prisma',
|
|
51
|
+
providerLabel: 'db-prisma',
|
|
52
|
+
isDetected: (detected) => detected.dbPrisma,
|
|
53
|
+
apply: syncJwtDbPrisma,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
48
57
|
function detectModules(rootDir) {
|
|
49
58
|
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
50
59
|
const appModuleText = fs.existsSync(appModulePath) ? fs.readFileSync(appModulePath, 'utf8') : '';
|
|
@@ -74,6 +83,17 @@ function ensureLineAfter(content, anchorLine, lineToInsert) {
|
|
|
74
83
|
return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
function resolveAuthPersistenceStrategy(detected) {
|
|
87
|
+
const matched = AUTH_PERSISTENCE_STRATEGIES.filter((strategy) => strategy.isDetected(detected));
|
|
88
|
+
if (matched.length === 0) {
|
|
89
|
+
return { kind: 'none' };
|
|
90
|
+
}
|
|
91
|
+
if (matched.length > 1) {
|
|
92
|
+
return { kind: 'conflict', strategies: matched };
|
|
93
|
+
}
|
|
94
|
+
return { kind: 'single', strategy: matched[0] };
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
function syncJwtDbPrisma({ rootDir, changedFiles }) {
|
|
78
98
|
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
79
99
|
const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
|
|
@@ -290,16 +310,21 @@ function run() {
|
|
|
290
310
|
const changedFiles = new Set();
|
|
291
311
|
const detected = detectModules(rootDir);
|
|
292
312
|
const summary = [];
|
|
313
|
+
const authPersistence = resolveAuthPersistenceStrategy(detected);
|
|
293
314
|
|
|
294
|
-
if (detected.jwtAuth &&
|
|
315
|
+
if (detected.jwtAuth && authPersistence.kind === 'single') {
|
|
295
316
|
summary.push({
|
|
296
|
-
feature:
|
|
297
|
-
result:
|
|
317
|
+
feature: `jwt-auth + db-adapter (current provider: ${authPersistence.strategy.providerLabel})`,
|
|
318
|
+
result: authPersistence.strategy.apply({ rootDir, changedFiles }),
|
|
298
319
|
});
|
|
299
320
|
} else {
|
|
321
|
+
const reason =
|
|
322
|
+
authPersistence.kind === 'conflict'
|
|
323
|
+
? 'multiple db-adapter providers detected'
|
|
324
|
+
: 'required components are not both available';
|
|
300
325
|
summary.push({
|
|
301
326
|
feature: 'jwt-auth + db-adapter (current provider: db-prisma)',
|
|
302
|
-
result: { applied: false, reason
|
|
327
|
+
result: { applied: false, reason },
|
|
303
328
|
});
|
|
304
329
|
}
|
|
305
330
|
|
|
@@ -27,7 +27,8 @@ export default function App() {
|
|
|
27
27
|
|
|
28
28
|
const requestProbe = async (path: string, init?: RequestInit): Promise<ProbeResult> => {
|
|
29
29
|
const response = await fetch(`/api${path}${toLangQuery(locale)}`, {
|
|
30
|
-
...init,
|
|
30
|
+
...(init ?? {}),
|
|
31
|
+
cache: 'no-store',
|
|
31
32
|
headers: {
|
|
32
33
|
...(init?.headers ?? {}),
|
|
33
34
|
'Accept-Language': locale,
|