create-forgeon 0.3.14 → 0.3.16
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/core/docs.test.mjs +79 -40
- package/src/core/scaffold.test.mjs +99 -0
- package/src/modules/db-prisma.mjs +23 -55
- package/src/modules/executor.test.mjs +2575 -2419
- package/src/modules/files-access.mjs +27 -98
- package/src/modules/files-image.mjs +26 -100
- package/src/modules/files-quotas.mjs +67 -87
- package/src/modules/files.mjs +35 -104
- package/src/modules/i18n.mjs +17 -121
- package/src/modules/idempotency.test.mjs +174 -0
- package/src/modules/jwt-auth.mjs +90 -209
- package/src/modules/logger.mjs +0 -9
- package/src/modules/probes.test.mjs +202 -0
- package/src/modules/queue.mjs +325 -412
- package/src/modules/rate-limit.mjs +22 -66
- package/src/modules/rbac.mjs +27 -67
- package/src/modules/scheduler.mjs +44 -167
- package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
- package/src/modules/shared/probes.mjs +235 -0
- package/src/modules/sync-integrations.mjs +54 -21
- package/src/modules/sync-integrations.test.mjs +220 -0
- package/src/run-add-module.test.mjs +153 -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 +55 -11
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
- 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/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-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJson } from '../../utils/fs.mjs';
|
|
4
|
+
|
|
5
|
+
const ansi = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
yellow: '\x1b[33m',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const MODULE_PROBES_START = ' // forgeon:module-probes:start';
|
|
11
|
+
const MODULE_PROBES_END = ' // forgeon:module-probes:end';
|
|
12
|
+
const probeEntryPattern = / \/\/ forgeon:probe:([a-z0-9-]+):start\n([\s\S]*?)\n \/\/ forgeon:probe:\1:end/g;
|
|
13
|
+
|
|
14
|
+
const probeOrders = {
|
|
15
|
+
health: 10,
|
|
16
|
+
error: 20,
|
|
17
|
+
validation: 30,
|
|
18
|
+
db: 40,
|
|
19
|
+
auth: 50,
|
|
20
|
+
rbac: 60,
|
|
21
|
+
'rate-limit': 70,
|
|
22
|
+
files: 80,
|
|
23
|
+
'files-variants': 81,
|
|
24
|
+
'files-access': 82,
|
|
25
|
+
'files-quotas': 83,
|
|
26
|
+
'files-image': 84,
|
|
27
|
+
queue: 90,
|
|
28
|
+
scheduler: 100,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
function colorize(text) {
|
|
33
|
+
return `${ansi.yellow}${text}${ansi.reset}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function warn(moduleId, message) {
|
|
37
|
+
console.log(colorize(`[forgeon:probes] ${moduleId}: ${message}`));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalize(content) {
|
|
41
|
+
return content.replace(/\r\n/g, '\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasProbeContainer(content) {
|
|
45
|
+
return /id=(['"])probes\1/.test(content);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getPackageJson(targetRoot) {
|
|
49
|
+
const packagePath = path.join(targetRoot, 'package.json');
|
|
50
|
+
if (!fs.existsSync(packagePath)) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return readJson(packagePath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getWebRegistryPath(targetRoot) {
|
|
58
|
+
return path.join(targetRoot, 'apps', 'web', 'src', 'probes.ts');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getModuleProbeEntries(content) {
|
|
62
|
+
const entries = new Map();
|
|
63
|
+
for (const match of content.matchAll(probeEntryPattern)) {
|
|
64
|
+
const probeId = match[1];
|
|
65
|
+
const jsonBlock = match[2].trim().replace(/,\s*$/, '');
|
|
66
|
+
try {
|
|
67
|
+
entries.set(probeId, JSON.parse(jsonBlock));
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatEntry(definition) {
|
|
76
|
+
const jsonBlock = JSON.stringify(definition, null, 2)
|
|
77
|
+
.split('\n')
|
|
78
|
+
.map((line) => ` ${line}`)
|
|
79
|
+
.join('\n');
|
|
80
|
+
|
|
81
|
+
return ` // forgeon:probe:${definition.id}:start\n${jsonBlock},\n // forgeon:probe:${definition.id}:end`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function replaceManagedBlock(content, entries) {
|
|
85
|
+
const sortedDefinitions = [...entries.values()].sort((left, right) => {
|
|
86
|
+
if (left.order !== right.order) {
|
|
87
|
+
return left.order - right.order;
|
|
88
|
+
}
|
|
89
|
+
return left.id.localeCompare(right.id);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const body = sortedDefinitions.map((definition) => formatEntry(definition)).join('\n\n');
|
|
93
|
+
const nextBlock = body.length > 0
|
|
94
|
+
? `${MODULE_PROBES_START}\n${body}\n${MODULE_PROBES_END}`
|
|
95
|
+
: `${MODULE_PROBES_START}\n${MODULE_PROBES_END}`;
|
|
96
|
+
|
|
97
|
+
const blockPattern = new RegExp(
|
|
98
|
+
`${escapeRegExp(MODULE_PROBES_START)}(?:\\n[\\s\\S]*?)?\\n${escapeRegExp(MODULE_PROBES_END)}`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!blockPattern.test(content)) {
|
|
102
|
+
return content;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return content.replace(blockPattern, nextBlock);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function escapeRegExp(value) {
|
|
109
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getProbeOrder(probeId) {
|
|
113
|
+
return probeOrders[probeId] ?? 999;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveProbeTargets({ targetRoot, moduleId }) {
|
|
117
|
+
const packageJson = getPackageJson(targetRoot);
|
|
118
|
+
const probesEnabled = packageJson.forgeon?.diagnostics?.probes?.enabled !== false;
|
|
119
|
+
if (!probesEnabled) {
|
|
120
|
+
warn(moduleId, 'probe wiring skipped because forgeon.diagnostics.probes.enabled=false.');
|
|
121
|
+
return {
|
|
122
|
+
allowApi: false,
|
|
123
|
+
allowWeb: false,
|
|
124
|
+
reason: 'disabled-by-config',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const healthControllerPath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
|
|
129
|
+
if (!fs.existsSync(healthControllerPath)) {
|
|
130
|
+
warn(moduleId, 'probe wiring skipped because apps/api/src/health/health.controller.ts is missing.');
|
|
131
|
+
return {
|
|
132
|
+
allowApi: false,
|
|
133
|
+
allowWeb: false,
|
|
134
|
+
reason: 'missing-health-surface',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const webAppPath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
|
|
139
|
+
if (!fs.existsSync(webAppPath)) {
|
|
140
|
+
warn(moduleId, 'web probe skipped because apps/web/src/App.tsx is missing.');
|
|
141
|
+
return {
|
|
142
|
+
allowApi: true,
|
|
143
|
+
allowWeb: false,
|
|
144
|
+
reason: 'missing-web-app',
|
|
145
|
+
healthControllerPath,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const webAppContent = normalize(fs.readFileSync(webAppPath, 'utf8'));
|
|
150
|
+
if (!hasProbeContainer(webAppContent)) {
|
|
151
|
+
warn(moduleId, 'web probe skipped because App.tsx does not expose a #probes container.');
|
|
152
|
+
return {
|
|
153
|
+
allowApi: true,
|
|
154
|
+
allowWeb: false,
|
|
155
|
+
reason: 'missing-web-probes-container',
|
|
156
|
+
healthControllerPath,
|
|
157
|
+
webAppPath,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const probesFilePath = getWebRegistryPath(targetRoot);
|
|
162
|
+
if (!fs.existsSync(probesFilePath)) {
|
|
163
|
+
warn(moduleId, 'web probe skipped because apps/web/src/probes.ts is missing.');
|
|
164
|
+
return {
|
|
165
|
+
allowApi: true,
|
|
166
|
+
allowWeb: false,
|
|
167
|
+
reason: 'missing-web-probes-registry',
|
|
168
|
+
healthControllerPath,
|
|
169
|
+
webAppPath,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const probesContent = normalize(fs.readFileSync(probesFilePath, 'utf8'));
|
|
174
|
+
if (!probesContent.includes(MODULE_PROBES_START) || !probesContent.includes(MODULE_PROBES_END)) {
|
|
175
|
+
warn(moduleId, 'web probe skipped because apps/web/src/probes.ts is missing managed module markers.');
|
|
176
|
+
return {
|
|
177
|
+
allowApi: true,
|
|
178
|
+
allowWeb: false,
|
|
179
|
+
reason: 'missing-web-probes-markers',
|
|
180
|
+
healthControllerPath,
|
|
181
|
+
webAppPath,
|
|
182
|
+
probesFilePath,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
allowApi: true,
|
|
188
|
+
allowWeb: true,
|
|
189
|
+
reason: 'ready',
|
|
190
|
+
healthControllerPath,
|
|
191
|
+
webAppPath,
|
|
192
|
+
probesFilePath,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function ensureWebProbeDefinition({ targetRoot, probeTargets, definition }) {
|
|
197
|
+
if (!probeTargets?.allowWeb) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const probesFilePath = probeTargets.probesFilePath ?? getWebRegistryPath(targetRoot);
|
|
202
|
+
if (!fs.existsSync(probesFilePath)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const normalizedDefinition = {
|
|
207
|
+
...definition,
|
|
208
|
+
order: definition.order ?? getProbeOrder(definition.id),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const content = normalize(fs.readFileSync(probesFilePath, 'utf8'));
|
|
212
|
+
const entries = getModuleProbeEntries(content);
|
|
213
|
+
entries.set(normalizedDefinition.id, normalizedDefinition);
|
|
214
|
+
|
|
215
|
+
const nextContent = replaceManagedBlock(content, entries);
|
|
216
|
+
fs.writeFileSync(probesFilePath, `${nextContent.trimEnd()}\n`, 'utf8');
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function readManagedWebProbeDefinitions(targetRoot) {
|
|
221
|
+
const probesFilePath = getWebRegistryPath(targetRoot);
|
|
222
|
+
if (!fs.existsSync(probesFilePath)) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const content = normalize(fs.readFileSync(probesFilePath, 'utf8'));
|
|
227
|
+
return [...getModuleProbeEntries(content).values()].sort((left, right) => {
|
|
228
|
+
if (left.order !== right.order) {
|
|
229
|
+
return left.order - right.order;
|
|
230
|
+
}
|
|
231
|
+
return left.id.localeCompare(right.id);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { copyRecursive } from '../utils/fs.mjs';
|
|
3
|
+
import { copyRecursive } from '../utils/fs.mjs';
|
|
4
|
+
import { ensureLineAfter } from './shared/patch-utils.mjs';
|
|
4
5
|
|
|
5
6
|
const PRISMA_AUTH_STORE_TEMPLATE = path.join(
|
|
6
7
|
'templates',
|
|
@@ -43,18 +44,34 @@ const AUTH_PERSISTENCE_STRATEGIES = [
|
|
|
43
44
|
},
|
|
44
45
|
];
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
function escapeRegExp(value) {
|
|
65
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function replaceReadmeManagedBlock(content, startMarker, endMarker, nextBody) {
|
|
69
|
+
const pattern = new RegExp(`${escapeRegExp(startMarker)}\\n[\\s\\S]*?\\n${escapeRegExp(endMarker)}`);
|
|
70
|
+
if (!pattern.test(content)) {
|
|
48
71
|
return content;
|
|
49
72
|
}
|
|
50
|
-
|
|
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)}`;
|
|
73
|
+
return content.replace(pattern, `${startMarker}\n${nextBody}\n${endMarker}`);
|
|
56
74
|
}
|
|
57
|
-
|
|
58
75
|
function isAuthPersistencePending(rootDir) {
|
|
59
76
|
const appModulePath = path.join(rootDir, 'apps', 'api', 'src', 'app.module.ts');
|
|
60
77
|
const schemaPath = path.join(rootDir, 'apps', 'api', 'prisma', 'schema.prisma');
|
|
@@ -318,14 +335,24 @@ function syncJwtDbPrisma({ rootDir, packageRoot, changedFiles }) {
|
|
|
318
335
|
if (fs.existsSync(readmePath)) {
|
|
319
336
|
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
320
337
|
const originalReadme = readme;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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`',
|
|
338
|
+
const managedReadme = replaceReadmeManagedBlock(
|
|
339
|
+
readme,
|
|
340
|
+
JWT_AUTH_PERSISTENCE_MARKERS.start,
|
|
341
|
+
JWT_AUTH_PERSISTENCE_MARKERS.end,
|
|
342
|
+
JWT_AUTH_DB_PRISMA_PERSISTENCE_BLOCK,
|
|
328
343
|
);
|
|
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
|
+
}
|
|
329
356
|
if (readme !== originalReadme) {
|
|
330
357
|
fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
|
|
331
358
|
changedFiles.add(readmePath);
|
|
@@ -418,14 +445,20 @@ function syncJwtRbacClaims({ rootDir, changedFiles }) {
|
|
|
418
445
|
if (fs.existsSync(readmePath)) {
|
|
419
446
|
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
420
447
|
const originalReadme = readme;
|
|
421
|
-
|
|
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')) {
|
|
422
457
|
const marker = 'Default demo credentials:';
|
|
423
458
|
if (readme.includes(marker)) {
|
|
424
459
|
readme = readme.replace(
|
|
425
460
|
marker,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
Default demo credentials:`,
|
|
461
|
+
'- RBAC integration: demo auth tokens include `health.rbac` permission\n\nDefault demo credentials:',
|
|
429
462
|
);
|
|
430
463
|
}
|
|
431
464
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { addModule } from './executor.mjs';
|
|
8
|
+
import { scanIntegrations, syncIntegrations } from './sync-integrations.mjs';
|
|
9
|
+
import { scaffoldProject } from '../core/scaffold.mjs';
|
|
10
|
+
|
|
11
|
+
function makeTempDir(prefix) {
|
|
12
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function scaffoldBaseProject({ packageRoot, targetRoot, projectName, dbPrismaEnabled = false }) {
|
|
16
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
17
|
+
scaffoldProject({
|
|
18
|
+
templateRoot,
|
|
19
|
+
packageRoot,
|
|
20
|
+
targetRoot,
|
|
21
|
+
projectName,
|
|
22
|
+
frontend: 'react',
|
|
23
|
+
db: 'prisma',
|
|
24
|
+
dbPrismaEnabled,
|
|
25
|
+
i18nEnabled: false,
|
|
26
|
+
proxy: 'caddy',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('sync integrations', () => {
|
|
31
|
+
const modulesDir = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const packageRoot = path.resolve(modulesDir, '..', '..');
|
|
33
|
+
|
|
34
|
+
it('does not expose auth persistence integration without a db-adapter provider', () => {
|
|
35
|
+
const tempRoot = makeTempDir('forgeon-sync-no-provider-');
|
|
36
|
+
const projectRoot = path.join(tempRoot, 'demo-sync-no-provider');
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
scaffoldBaseProject({
|
|
40
|
+
packageRoot,
|
|
41
|
+
targetRoot: projectRoot,
|
|
42
|
+
projectName: 'demo-sync-no-provider',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
addModule({ moduleId: 'jwt-auth', targetRoot: projectRoot, packageRoot });
|
|
46
|
+
|
|
47
|
+
const scan = scanIntegrations({
|
|
48
|
+
targetRoot: projectRoot,
|
|
49
|
+
relatedModuleId: 'jwt-auth',
|
|
50
|
+
});
|
|
51
|
+
assert.equal(scan.groups.some((group) => group.id === 'auth-persistence'), false);
|
|
52
|
+
|
|
53
|
+
const syncResult = syncIntegrations({
|
|
54
|
+
targetRoot: projectRoot,
|
|
55
|
+
packageRoot,
|
|
56
|
+
groupIds: ['auth-persistence'],
|
|
57
|
+
});
|
|
58
|
+
assert.deepEqual(syncResult.summary, []);
|
|
59
|
+
assert.deepEqual(syncResult.changedFiles, []);
|
|
60
|
+
} finally {
|
|
61
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('treats auth persistence sync as a no-op after it has already been applied', () => {
|
|
66
|
+
const tempRoot = makeTempDir('forgeon-sync-db-noop-');
|
|
67
|
+
const projectRoot = path.join(tempRoot, 'demo-sync-db-noop');
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
scaffoldBaseProject({
|
|
71
|
+
packageRoot,
|
|
72
|
+
targetRoot: projectRoot,
|
|
73
|
+
projectName: 'demo-sync-db-noop',
|
|
74
|
+
dbPrismaEnabled: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
addModule({ moduleId: 'jwt-auth', targetRoot: projectRoot, packageRoot });
|
|
78
|
+
|
|
79
|
+
const firstSync = syncIntegrations({
|
|
80
|
+
targetRoot: projectRoot,
|
|
81
|
+
packageRoot,
|
|
82
|
+
groupIds: ['auth-persistence'],
|
|
83
|
+
});
|
|
84
|
+
assert.equal(firstSync.summary.length, 1);
|
|
85
|
+
assert.equal(firstSync.summary[0].id, 'auth-persistence');
|
|
86
|
+
assert.equal(firstSync.summary[0].result.applied, true);
|
|
87
|
+
|
|
88
|
+
const secondScan = scanIntegrations({
|
|
89
|
+
targetRoot: projectRoot,
|
|
90
|
+
relatedModuleId: 'jwt-auth',
|
|
91
|
+
});
|
|
92
|
+
assert.equal(secondScan.groups.some((group) => group.id === 'auth-persistence'), false);
|
|
93
|
+
|
|
94
|
+
const secondSync = syncIntegrations({
|
|
95
|
+
targetRoot: projectRoot,
|
|
96
|
+
packageRoot,
|
|
97
|
+
groupIds: ['auth-persistence'],
|
|
98
|
+
});
|
|
99
|
+
assert.deepEqual(secondSync.summary, []);
|
|
100
|
+
assert.deepEqual(secondSync.changedFiles, []);
|
|
101
|
+
} finally {
|
|
102
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('treats auth claims sync as a no-op after it has already been applied', () => {
|
|
107
|
+
const tempRoot = makeTempDir('forgeon-sync-rbac-noop-');
|
|
108
|
+
const projectRoot = path.join(tempRoot, 'demo-sync-rbac-noop');
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
scaffoldBaseProject({
|
|
112
|
+
packageRoot,
|
|
113
|
+
targetRoot: projectRoot,
|
|
114
|
+
projectName: 'demo-sync-rbac-noop',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
addModule({ moduleId: 'rbac', targetRoot: projectRoot, packageRoot });
|
|
118
|
+
addModule({ moduleId: 'jwt-auth', targetRoot: projectRoot, packageRoot });
|
|
119
|
+
|
|
120
|
+
const firstSync = syncIntegrations({
|
|
121
|
+
targetRoot: projectRoot,
|
|
122
|
+
packageRoot,
|
|
123
|
+
groupIds: ['auth-rbac-claims'],
|
|
124
|
+
});
|
|
125
|
+
assert.equal(firstSync.summary.length, 1);
|
|
126
|
+
assert.equal(firstSync.summary[0].id, 'auth-rbac-claims');
|
|
127
|
+
assert.equal(firstSync.summary[0].result.applied, true);
|
|
128
|
+
|
|
129
|
+
const secondScan = scanIntegrations({
|
|
130
|
+
targetRoot: projectRoot,
|
|
131
|
+
relatedModuleId: 'jwt-auth',
|
|
132
|
+
});
|
|
133
|
+
assert.equal(secondScan.groups.some((group) => group.id === 'auth-rbac-claims'), false);
|
|
134
|
+
|
|
135
|
+
const secondSync = syncIntegrations({
|
|
136
|
+
targetRoot: projectRoot,
|
|
137
|
+
packageRoot,
|
|
138
|
+
groupIds: ['auth-rbac-claims'],
|
|
139
|
+
});
|
|
140
|
+
assert.deepEqual(secondSync.summary, []);
|
|
141
|
+
assert.deepEqual(secondSync.changedFiles, []);
|
|
142
|
+
} finally {
|
|
143
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
it('uses persistence markers instead of README prose when auth persistence sync runs', () => {
|
|
147
|
+
const tempRoot = makeTempDir('forgeon-sync-db-markers-');
|
|
148
|
+
const projectRoot = path.join(tempRoot, 'demo-sync-db-markers');
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
scaffoldBaseProject({
|
|
152
|
+
packageRoot,
|
|
153
|
+
targetRoot: projectRoot,
|
|
154
|
+
projectName: 'demo-sync-db-markers',
|
|
155
|
+
dbPrismaEnabled: true,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
addModule({ moduleId: 'jwt-auth', targetRoot: projectRoot, packageRoot });
|
|
159
|
+
|
|
160
|
+
const readmePath = path.join(projectRoot, 'README.md');
|
|
161
|
+
const customizedReadme = fs
|
|
162
|
+
.readFileSync(readmePath, 'utf8')
|
|
163
|
+
.replace('refresh token persistence: disabled by default', 'refresh token persistence: custom local note');
|
|
164
|
+
fs.writeFileSync(readmePath, customizedReadme, 'utf8');
|
|
165
|
+
|
|
166
|
+
const syncResult = syncIntegrations({
|
|
167
|
+
targetRoot: projectRoot,
|
|
168
|
+
packageRoot,
|
|
169
|
+
groupIds: ['auth-persistence'],
|
|
170
|
+
});
|
|
171
|
+
assert.equal(syncResult.summary.length, 1);
|
|
172
|
+
assert.equal(syncResult.summary[0].result.applied, true);
|
|
173
|
+
|
|
174
|
+
const readme = fs.readFileSync(readmePath, 'utf8');
|
|
175
|
+
assert.match(readme, /forgeon:jwt-auth:persistence:start/);
|
|
176
|
+
assert.match(readme, /refresh token persistence: enabled through the `db-adapter` capability/);
|
|
177
|
+
assert.match(readme, /0002_auth_refresh_token_hash/);
|
|
178
|
+
assert.doesNotMatch(readme, /custom local note/);
|
|
179
|
+
} finally {
|
|
180
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('uses RBAC markers instead of the demo credentials heading when auth claims sync runs', () => {
|
|
185
|
+
const tempRoot = makeTempDir('forgeon-sync-rbac-markers-');
|
|
186
|
+
const projectRoot = path.join(tempRoot, 'demo-sync-rbac-markers');
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
scaffoldBaseProject({
|
|
190
|
+
packageRoot,
|
|
191
|
+
targetRoot: projectRoot,
|
|
192
|
+
projectName: 'demo-sync-rbac-markers',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
addModule({ moduleId: 'rbac', targetRoot: projectRoot, packageRoot });
|
|
196
|
+
addModule({ moduleId: 'jwt-auth', targetRoot: projectRoot, packageRoot });
|
|
197
|
+
|
|
198
|
+
const readmePath = path.join(projectRoot, 'README.md');
|
|
199
|
+
const customizedReadme = fs
|
|
200
|
+
.readFileSync(readmePath, 'utf8')
|
|
201
|
+
.replace('Default demo credentials:', 'Demo credentials:');
|
|
202
|
+
fs.writeFileSync(readmePath, customizedReadme, 'utf8');
|
|
203
|
+
|
|
204
|
+
const syncResult = syncIntegrations({
|
|
205
|
+
targetRoot: projectRoot,
|
|
206
|
+
packageRoot,
|
|
207
|
+
groupIds: ['auth-rbac-claims'],
|
|
208
|
+
});
|
|
209
|
+
assert.equal(syncResult.summary.length, 1);
|
|
210
|
+
assert.equal(syncResult.summary[0].result.applied, true);
|
|
211
|
+
|
|
212
|
+
const readme = fs.readFileSync(readmePath, 'utf8');
|
|
213
|
+
assert.match(readme, /forgeon:jwt-auth:rbac:start/);
|
|
214
|
+
assert.match(readme, /RBAC integration: demo auth tokens include `health\.rbac` permission/);
|
|
215
|
+
assert.match(readme, /Demo credentials:/);
|
|
216
|
+
} finally {
|
|
217
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|