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,153 @@
|
|
|
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 { scaffoldProject } from './core/scaffold.mjs';
|
|
8
|
+
import { runAddModule } from './run-add-module.mjs';
|
|
9
|
+
|
|
10
|
+
function makeTempDir(prefix) {
|
|
11
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readFile(filePath) {
|
|
15
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeJson(filePath, value) {
|
|
19
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scaffoldBaseProject({ packageRoot, targetRoot, projectName, proxy }) {
|
|
23
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
24
|
+
scaffoldProject({
|
|
25
|
+
templateRoot,
|
|
26
|
+
packageRoot,
|
|
27
|
+
targetRoot,
|
|
28
|
+
projectName,
|
|
29
|
+
frontend: 'react',
|
|
30
|
+
db: 'prisma',
|
|
31
|
+
dbPrismaEnabled: false,
|
|
32
|
+
i18nEnabled: false,
|
|
33
|
+
proxy,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stripSyncTooling(targetRoot) {
|
|
38
|
+
const packagePath = path.join(targetRoot, 'package.json');
|
|
39
|
+
const packageJson = JSON.parse(readFile(packagePath));
|
|
40
|
+
if (packageJson.scripts) {
|
|
41
|
+
delete packageJson.scripts['forgeon:sync-integrations'];
|
|
42
|
+
}
|
|
43
|
+
writeJson(packagePath, packageJson);
|
|
44
|
+
|
|
45
|
+
fs.rmSync(path.join(targetRoot, 'scripts', 'forgeon-sync-integrations.mjs'), { force: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function captureLogs(work) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
const originalLog = console.log;
|
|
51
|
+
console.log = (...args) => {
|
|
52
|
+
lines.push(args.join(' '));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await work(lines);
|
|
57
|
+
} finally {
|
|
58
|
+
console.log = originalLog;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('runAddModule', () => {
|
|
65
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
66
|
+
const packageRoot = path.resolve(thisDir, '..');
|
|
67
|
+
|
|
68
|
+
it('installs files stack non-interactively with provider selection and restores sync tooling', async () => {
|
|
69
|
+
const tempRoot = makeTempDir('forgeon-run-add-files-');
|
|
70
|
+
const targetRoot = path.join(tempRoot, 'demo-run-add-files');
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
scaffoldBaseProject({
|
|
74
|
+
packageRoot,
|
|
75
|
+
targetRoot,
|
|
76
|
+
projectName: 'demo-run-add-files',
|
|
77
|
+
proxy: 'nginx',
|
|
78
|
+
});
|
|
79
|
+
stripSyncTooling(targetRoot);
|
|
80
|
+
|
|
81
|
+
const output = await captureLogs(async () => {
|
|
82
|
+
await runAddModule([
|
|
83
|
+
'files',
|
|
84
|
+
'--project',
|
|
85
|
+
targetRoot,
|
|
86
|
+
'--with-required',
|
|
87
|
+
'--provider',
|
|
88
|
+
'files-storage-adapter=files-local',
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'db-prisma', 'package.json')), true);
|
|
93
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'files-local', 'package.json')), true);
|
|
94
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'files', 'package.json')), true);
|
|
95
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'scripts', 'forgeon-sync-integrations.mjs')), true);
|
|
96
|
+
|
|
97
|
+
const packageJson = JSON.parse(readFile(path.join(targetRoot, 'package.json')));
|
|
98
|
+
assert.equal(packageJson.scripts['forgeon:sync-integrations'], 'node scripts/forgeon-sync-integrations.mjs');
|
|
99
|
+
|
|
100
|
+
const compose = readFile(path.join(targetRoot, 'infra', 'docker', 'compose.yml'));
|
|
101
|
+
assert.match(compose, /^\s{2}nginx:\s*$/m);
|
|
102
|
+
assert.doesNotMatch(compose, /^\s{2}caddy:\s*$/m);
|
|
103
|
+
|
|
104
|
+
const apiEnv = readFile(path.join(targetRoot, 'apps', 'api', '.env.example'));
|
|
105
|
+
assert.match(apiEnv, /DATABASE_URL=postgresql:\/\/postgres:postgres@localhost:5432\/app\?schema=public/);
|
|
106
|
+
assert.match(apiEnv, /FILES_STORAGE_DRIVER=local/);
|
|
107
|
+
|
|
108
|
+
const healthController = readFile(path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'));
|
|
109
|
+
assert.match(healthController, /@Post\('files'\)/);
|
|
110
|
+
|
|
111
|
+
assert.match(output, /Recommended companion modules are available:/);
|
|
112
|
+
assert.match(output, /No integration groups found\./);
|
|
113
|
+
assert.match(output, /Next: run pnpm install/);
|
|
114
|
+
} finally {
|
|
115
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('installs scheduler with required queue on proxy=none scaffold', async () => {
|
|
120
|
+
const tempRoot = makeTempDir('forgeon-run-add-scheduler-');
|
|
121
|
+
const targetRoot = path.join(tempRoot, 'demo-run-add-scheduler');
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
scaffoldBaseProject({
|
|
125
|
+
packageRoot,
|
|
126
|
+
targetRoot,
|
|
127
|
+
projectName: 'demo-run-add-scheduler',
|
|
128
|
+
proxy: 'none',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const output = await captureLogs(async () => {
|
|
132
|
+
await runAddModule(['scheduler', '--project', targetRoot, '--with-required']);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'queue', 'package.json')), true);
|
|
136
|
+
assert.equal(fs.existsSync(path.join(targetRoot, 'packages', 'scheduler', 'package.json')), true);
|
|
137
|
+
|
|
138
|
+
const compose = readFile(path.join(targetRoot, 'infra', 'docker', 'compose.yml'));
|
|
139
|
+
assert.match(compose, /^\s{2}redis:\s*$/m);
|
|
140
|
+
assert.doesNotMatch(compose, /^\s{2}caddy:\s*$/m);
|
|
141
|
+
assert.doesNotMatch(compose, /^\s{2}nginx:\s*$/m);
|
|
142
|
+
|
|
143
|
+
const healthController = readFile(path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'));
|
|
144
|
+
assert.match(healthController, /@Get\('queue'\)/);
|
|
145
|
+
assert.match(healthController, /@Get\('scheduler'\)/);
|
|
146
|
+
|
|
147
|
+
assert.match(output, /No integration groups found\./);
|
|
148
|
+
assert.match(output, /Next: run pnpm install/);
|
|
149
|
+
} finally {
|
|
150
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
package/templates/base/README.md
CHANGED
|
@@ -1,58 +1,10 @@
|
|
|
1
|
-
# Forgeon
|
|
2
|
-
|
|
3
|
-
Canonical monorepo scaffold for NestJS + frontend with shared packages, built-in docs, optional i18n (enabled by default), and default DB stack Prisma + Postgres.
|
|
4
|
-
|
|
5
|
-
## Quick Start (Dev)
|
|
6
|
-
|
|
7
|
-
1. Install dependencies:
|
|
8
|
-
```bash
|
|
9
|
-
pnpm install
|
|
10
|
-
```
|
|
11
|
-
2. Start local Postgres (Docker):
|
|
12
|
-
```bash
|
|
13
|
-
docker compose --env-file infra/docker/.env.example -f infra/docker/compose.yml up db -d
|
|
14
|
-
```
|
|
15
|
-
3. Run API + web in dev mode:
|
|
16
|
-
```bash
|
|
17
|
-
pnpm dev
|
|
18
|
-
```
|
|
19
|
-
4. Open:
|
|
20
|
-
- Web: `http://localhost:5173`
|
|
21
|
-
- API health: `http://localhost:3000/api/health`
|
|
22
|
-
|
|
23
|
-
## Quick Start (Docker)
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
docker compose --env-file infra/docker/.env.example -f infra/docker/compose.yml up --build
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Open `http://localhost:8080`.
|
|
1
|
+
# Forgeon Base Template
|
|
30
2
|
|
|
31
|
-
|
|
3
|
+
Internal scaffold substrate copied before presets and generated docs are applied.
|
|
32
4
|
|
|
33
|
-
|
|
5
|
+
Notes for Forgeon development:
|
|
34
6
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Current sync coverage:
|
|
40
|
-
- `jwt-auth + rbac`: extends demo auth tokens with the `health.rbac` permission.
|
|
41
|
-
- `jwt-auth + db-adapter` (current provider: `db-prisma`): wires persistent refresh-token storage for auth.
|
|
42
|
-
|
|
43
|
-
`create-forgeon add <module>` scans for relevant integration groups and can apply them immediately.
|
|
44
|
-
|
|
45
|
-
## i18n Configuration
|
|
46
|
-
|
|
47
|
-
Set in env (when i18n module is installed):
|
|
48
|
-
- `I18N_DEFAULT_LANG=en`
|
|
49
|
-
- `I18N_FALLBACK_LANG=en`
|
|
50
|
-
|
|
51
|
-
## Prisma In Docker Start
|
|
52
|
-
|
|
53
|
-
API container starts with:
|
|
54
|
-
1. `prisma migrate deploy`
|
|
55
|
-
2. `node apps/api/dist/main.js`
|
|
56
|
-
|
|
57
|
-
This keeps container startup production-like while still simple.
|
|
58
|
-
|
|
7
|
+
- the final generated project `README.md` is produced from `templates/docs-fragments/README/*`
|
|
8
|
+
- module notes are produced under `modules/<module-id>/README.md` from `templates/module-fragments/*`
|
|
9
|
+
- this file is a temporary template placeholder and is normally overwritten during scaffold
|
|
10
|
+
- generated-project commands such as `pnpm forgeon:sync-integrations` belong to scaffolded output, not to the root development repo
|
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { probeDefinitions, type ProbeDefinition, type ProbeResult } from './probes';
|
|
2
3
|
import './styles.css';
|
|
3
4
|
|
|
4
|
-
type
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
type ProbeState = {
|
|
6
|
+
result: ProbeResult | null;
|
|
7
|
+
error: string | null;
|
|
8
|
+
loading: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const emptyProbeState: ProbeState = {
|
|
12
|
+
result: null,
|
|
13
|
+
error: null,
|
|
14
|
+
loading: false,
|
|
7
15
|
};
|
|
8
16
|
|
|
9
17
|
export default function App() {
|
|
10
|
-
const [
|
|
11
|
-
const [errorProbeResult, setErrorProbeResult] = useState<ProbeResult | null>(null);
|
|
12
|
-
const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);
|
|
13
|
-
const [networkError, setNetworkError] = useState<string | null>(null);
|
|
18
|
+
const [probeState, setProbeState] = useState<Record<string, ProbeState>>({});
|
|
14
19
|
|
|
15
|
-
const requestProbe = async (
|
|
16
|
-
const response = await fetch(
|
|
17
|
-
...(
|
|
20
|
+
const requestProbe = async (probe: ProbeDefinition): Promise<ProbeResult> => {
|
|
21
|
+
const response = await fetch(`/api${probe.path}`, {
|
|
22
|
+
...(probe.request ?? {}),
|
|
18
23
|
cache: 'no-store',
|
|
24
|
+
headers: {
|
|
25
|
+
...(probe.request?.headers ?? {}),
|
|
26
|
+
},
|
|
19
27
|
});
|
|
20
28
|
let body: unknown = null;
|
|
21
29
|
|
|
@@ -31,46 +39,66 @@ export default function App() {
|
|
|
31
39
|
};
|
|
32
40
|
};
|
|
33
41
|
|
|
34
|
-
const runProbe = async (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
const runProbe = async (probe: ProbeDefinition) => {
|
|
43
|
+
setProbeState((current) => ({
|
|
44
|
+
...current,
|
|
45
|
+
[probe.id]: {
|
|
46
|
+
...(current[probe.id] ?? emptyProbeState),
|
|
47
|
+
error: null,
|
|
48
|
+
loading: true,
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
|
|
40
52
|
try {
|
|
41
|
-
const result = await requestProbe(
|
|
42
|
-
|
|
53
|
+
const result = await requestProbe(probe);
|
|
54
|
+
setProbeState((current) => ({
|
|
55
|
+
...current,
|
|
56
|
+
[probe.id]: {
|
|
57
|
+
result,
|
|
58
|
+
error: null,
|
|
59
|
+
loading: false,
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
43
62
|
} catch (err) {
|
|
44
|
-
|
|
63
|
+
setProbeState((current) => ({
|
|
64
|
+
...current,
|
|
65
|
+
[probe.id]: {
|
|
66
|
+
result: current[probe.id]?.result ?? null,
|
|
67
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
68
|
+
loading: false,
|
|
69
|
+
},
|
|
70
|
+
}));
|
|
45
71
|
}
|
|
46
72
|
};
|
|
47
73
|
|
|
48
|
-
const renderResult = (title: string, result: ProbeResult | null) => (
|
|
49
|
-
<section>
|
|
50
|
-
<h3>{title}</h3>
|
|
51
|
-
{result ? <pre>{JSON.stringify(result, null, 2)}</pre> : null}
|
|
52
|
-
</section>
|
|
53
|
-
);
|
|
54
|
-
|
|
55
74
|
return (
|
|
56
75
|
<main className="page">
|
|
57
76
|
<h1>Forgeon Fullstack Scaffold</h1>
|
|
58
77
|
<p>Default frontend preset: React + Vite + TypeScript.</p>
|
|
59
|
-
<div className="
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
<div id="probes" className="probes">
|
|
79
|
+
{probeDefinitions.map((probe) => {
|
|
80
|
+
const current = probeState[probe.id] ?? emptyProbeState;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<section key={probe.id} className="probe">
|
|
84
|
+
<div className="probe-header">
|
|
85
|
+
<h2>{probe.title}</h2>
|
|
86
|
+
<button type="button" onClick={() => runProbe(probe)} disabled={current.loading}>
|
|
87
|
+
{current.loading ? 'Running...' : probe.buttonLabel}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="probe-output">
|
|
91
|
+
<h3>{probe.resultTitle}</h3>
|
|
92
|
+
{current.error ? <p className="error">{current.error}</p> : null}
|
|
93
|
+
{current.result ? <pre>{JSON.stringify(current.result, null, 2)}</pre> : null}
|
|
94
|
+
{!current.error && !current.result ? (
|
|
95
|
+
<p className="placeholder">No probe result yet.</p>
|
|
96
|
+
) : null}
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
67
101
|
</div>
|
|
68
|
-
{renderResult('Health response', healthResult)}
|
|
69
|
-
{renderResult('Error probe response', errorProbeResult)}
|
|
70
|
-
{renderResult('Validation probe response', validationProbeResult)}
|
|
71
|
-
{networkError ? <p className="error">{networkError}</p> : null}
|
|
72
102
|
</main>
|
|
73
103
|
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type ProbeResult = {
|
|
2
|
+
statusCode: number;
|
|
3
|
+
body: unknown;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type ProbeRequest = {
|
|
7
|
+
method?: 'GET' | 'POST';
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ProbeDefinition = {
|
|
12
|
+
id: string;
|
|
13
|
+
order: number;
|
|
14
|
+
title: string;
|
|
15
|
+
buttonLabel: string;
|
|
16
|
+
resultTitle: string;
|
|
17
|
+
path: string;
|
|
18
|
+
request?: ProbeRequest;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const baseProbeDefinitions: ProbeDefinition[] = [
|
|
22
|
+
{
|
|
23
|
+
id: 'health',
|
|
24
|
+
order: 10,
|
|
25
|
+
title: 'API Health',
|
|
26
|
+
buttonLabel: 'Check API health',
|
|
27
|
+
resultTitle: 'Health response',
|
|
28
|
+
path: '/health',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'error',
|
|
32
|
+
order: 20,
|
|
33
|
+
title: 'Error Envelope',
|
|
34
|
+
buttonLabel: 'Check error envelope',
|
|
35
|
+
resultTitle: 'Error probe response',
|
|
36
|
+
path: '/health/error',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'validation',
|
|
40
|
+
order: 30,
|
|
41
|
+
title: 'Validation',
|
|
42
|
+
buttonLabel: 'Check validation (expect 400)',
|
|
43
|
+
resultTitle: 'Validation probe response',
|
|
44
|
+
path: '/health/validation',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const moduleProbeDefinitions: ProbeDefinition[] = [
|
|
49
|
+
// forgeon:module-probes:start
|
|
50
|
+
// forgeon:module-probes:end
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function compareProbeOrder(left: ProbeDefinition, right: ProbeDefinition): number {
|
|
54
|
+
if (left.order !== right.order) {
|
|
55
|
+
return left.order - right.order;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return left.id.localeCompare(right.id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const probeDefinitions = [...baseProbeDefinitions, ...moduleProbeDefinitions].sort(compareProbeOrder);
|
|
@@ -1,45 +1,106 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
body {
|
|
6
|
-
margin: 0;
|
|
7
|
-
background: #f8fafc;
|
|
8
|
-
color: #0f172a;
|
|
9
|
-
}
|
|
10
|
-
|
|
1
|
+
:root {
|
|
2
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
body {
|
|
6
|
+
margin: 0;
|
|
7
|
+
background: #f8fafc;
|
|
8
|
+
color: #0f172a;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
11
|
.page {
|
|
12
|
-
max-width:
|
|
12
|
+
max-width: 1120px;
|
|
13
13
|
margin: 3rem auto;
|
|
14
|
-
padding: 0 1rem;
|
|
14
|
+
padding: 0 1rem 4rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.probes {
|
|
18
|
+
display: grid;
|
|
19
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
20
|
+
gap: 1rem;
|
|
21
|
+
margin-top: 1.5rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.probe {
|
|
25
|
+
display: grid;
|
|
26
|
+
gap: 0.9rem;
|
|
27
|
+
padding: 1rem;
|
|
28
|
+
border: 1px solid #cbd5e1;
|
|
29
|
+
border-radius: 0.9rem;
|
|
30
|
+
background: #ffffff;
|
|
31
|
+
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.probe-header {
|
|
35
|
+
display: grid;
|
|
36
|
+
gap: 0.75rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.probe-header h2 {
|
|
40
|
+
margin: 0;
|
|
41
|
+
font-size: 1rem;
|
|
15
42
|
}
|
|
16
43
|
|
|
17
|
-
.
|
|
44
|
+
.probe-output {
|
|
18
45
|
display: grid;
|
|
19
|
-
|
|
20
|
-
gap: 0.6rem;
|
|
21
|
-
margin: 1rem 0 1.25rem;
|
|
46
|
+
gap: 0.5rem;
|
|
22
47
|
}
|
|
23
48
|
|
|
24
49
|
button {
|
|
25
|
-
padding: 0.
|
|
50
|
+
padding: 0.7rem 1rem;
|
|
26
51
|
border: 0;
|
|
27
|
-
border-radius: 0.
|
|
52
|
+
border-radius: 0.65rem;
|
|
28
53
|
cursor: pointer;
|
|
54
|
+
background: #0f172a;
|
|
55
|
+
color: #f8fafc;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
button:disabled {
|
|
60
|
+
opacity: 0.7;
|
|
61
|
+
cursor: progress;
|
|
29
62
|
}
|
|
30
63
|
|
|
31
64
|
h3 {
|
|
32
|
-
margin:
|
|
33
|
-
font-size:
|
|
65
|
+
margin: 0;
|
|
66
|
+
font-size: 0.95rem;
|
|
34
67
|
}
|
|
35
68
|
|
|
36
69
|
pre {
|
|
70
|
+
margin: 0;
|
|
37
71
|
background: #e2e8f0;
|
|
38
72
|
padding: 1rem;
|
|
73
|
+
border-radius: 0.65rem;
|
|
74
|
+
overflow: auto;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
label {
|
|
78
|
+
display: inline-block;
|
|
79
|
+
margin-top: 1rem;
|
|
80
|
+
margin-right: 0.5rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
select {
|
|
84
|
+
min-width: 140px;
|
|
85
|
+
padding: 0.55rem 0.7rem;
|
|
39
86
|
border-radius: 0.5rem;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
87
|
+
border: 1px solid #cbd5e1;
|
|
88
|
+
background: #ffffff;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.error {
|
|
92
|
+
margin: 0;
|
|
93
|
+
color: #b91c1c;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.placeholder {
|
|
97
|
+
margin: 0;
|
|
98
|
+
color: #475569;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@media (min-width: 640px) {
|
|
102
|
+
.probe-header {
|
|
103
|
+
grid-template-columns: 1fr auto;
|
|
104
|
+
align-items: start;
|
|
105
|
+
}
|
|
45
106
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "forgeon",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"packageManager": "pnpm@10.0.0",
|
|
1
|
+
{
|
|
2
|
+
"name": "forgeon",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"packageManager": "pnpm@10.0.0",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "pnpm --parallel --filter @forgeon/api --filter @forgeon/web dev",
|
|
8
8
|
"build": "pnpm -r build",
|
|
@@ -12,13 +12,19 @@
|
|
|
12
12
|
"docker:down": "docker compose -f infra/docker/compose.yml down -v"
|
|
13
13
|
},
|
|
14
14
|
"pnpm": {
|
|
15
|
-
"onlyBuiltDependencies": [
|
|
16
|
-
"@nestjs/core",
|
|
17
|
-
"@prisma/client",
|
|
18
|
-
"@prisma/engines",
|
|
19
|
-
"esbuild",
|
|
20
|
-
"prisma"
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
"onlyBuiltDependencies": [
|
|
16
|
+
"@nestjs/core",
|
|
17
|
+
"@prisma/client",
|
|
18
|
+
"@prisma/engines",
|
|
19
|
+
"esbuild",
|
|
20
|
+
"prisma"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"forgeon": {
|
|
24
|
+
"diagnostics": {
|
|
25
|
+
"probes": {
|
|
26
|
+
"enabled": true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -83,6 +83,34 @@ function ensureLineAfter(content, anchorLine, lineToInsert) {
|
|
|
83
83
|
return `${content.slice(0, insertAt)}\n${lineToInsert}${content.slice(insertAt)}`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const JWT_AUTH_PERSISTENCE_MARKERS = {
|
|
87
|
+
start: '<!-- forgeon:jwt-auth:persistence:start -->',
|
|
88
|
+
end: '<!-- forgeon:jwt-auth:persistence:end -->',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const JWT_AUTH_RBAC_MARKERS = {
|
|
92
|
+
start: '<!-- forgeon:jwt-auth:rbac:start -->',
|
|
93
|
+
end: '<!-- forgeon:jwt-auth:rbac:end -->',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const JWT_AUTH_DB_PRISMA_PERSISTENCE_BLOCK = [
|
|
97
|
+
'- refresh token persistence: enabled through the `db-adapter` capability (current provider: `db-prisma`)',
|
|
98
|
+
'- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
|
|
99
|
+
].join('\n');
|
|
100
|
+
|
|
101
|
+
const JWT_AUTH_RBAC_ENABLED_BLOCK = '- RBAC integration: demo auth tokens include `health.rbac` permission';
|
|
102
|
+
|
|
103
|
+
function escapeRegExp(value) {
|
|
104
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function replaceReadmeManagedBlock(content, startMarker, endMarker, nextBody) {
|
|
108
|
+
const pattern = new RegExp(`${escapeRegExp(startMarker)}\\n[\\s\\S]*?\\n${escapeRegExp(endMarker)}`);
|
|
109
|
+
if (!pattern.test(content)) {
|
|
110
|
+
return content;
|
|
111
|
+
}
|
|
112
|
+
return content.replace(pattern, `${startMarker}\n${nextBody}\n${endMarker}`);
|
|
113
|
+
}
|
|
86
114
|
function resolveAuthPersistenceStrategy(detected) {
|
|
87
115
|
const matched = AUTH_PERSISTENCE_STRATEGIES.filter((strategy) => strategy.isDetected(detected));
|
|
88
116
|
if (matched.length === 0) {
|
|
@@ -181,14 +209,24 @@ function syncJwtDbPrisma({ rootDir, changedFiles }) {
|
|
|
181
209
|
if (fs.existsSync(readmePath)) {
|
|
182
210
|
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
183
211
|
const originalReadme = readme;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
/- to enable persistence later:[\s\S]*?2\. run `pnpm forgeon:sync-integrations` to wire auth persistence to the active DB adapter implementation\./m,
|
|
190
|
-
'- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
|
|
212
|
+
const managedReadme = replaceReadmeManagedBlock(
|
|
213
|
+
readme,
|
|
214
|
+
JWT_AUTH_PERSISTENCE_MARKERS.start,
|
|
215
|
+
JWT_AUTH_PERSISTENCE_MARKERS.end,
|
|
216
|
+
JWT_AUTH_DB_PRISMA_PERSISTENCE_BLOCK,
|
|
191
217
|
);
|
|
218
|
+
if (managedReadme !== readme) {
|
|
219
|
+
readme = managedReadme;
|
|
220
|
+
} else {
|
|
221
|
+
readme = readme.replace(
|
|
222
|
+
'- refresh token persistence: disabled by default (stateless mode; enable it later through a `db-adapter` provider + integration sync)',
|
|
223
|
+
'- refresh token persistence: enabled through the `db-adapter` capability (current provider: `db-prisma`)',
|
|
224
|
+
);
|
|
225
|
+
readme = readme.replace(
|
|
226
|
+
/- to enable persistence later:[\s\S]*?2\. run `pnpm forgeon:sync-integrations` to wire auth persistence to the active DB adapter implementation\./m,
|
|
227
|
+
'- migration: `apps/api/prisma/migrations/0002_auth_refresh_token_hash`',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
192
230
|
if (readme !== originalReadme) {
|
|
193
231
|
fs.writeFileSync(readmePath, `${readme.trimEnd()}\n`, 'utf8');
|
|
194
232
|
changedFiles.add(readmePath);
|
|
@@ -281,14 +319,20 @@ function syncJwtRbacClaims({ rootDir, changedFiles }) {
|
|
|
281
319
|
if (fs.existsSync(readmePath)) {
|
|
282
320
|
let readme = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
283
321
|
const originalReadme = readme;
|
|
284
|
-
|
|
322
|
+
const managedReadme = replaceReadmeManagedBlock(
|
|
323
|
+
readme,
|
|
324
|
+
JWT_AUTH_RBAC_MARKERS.start,
|
|
325
|
+
JWT_AUTH_RBAC_MARKERS.end,
|
|
326
|
+
JWT_AUTH_RBAC_ENABLED_BLOCK,
|
|
327
|
+
);
|
|
328
|
+
if (managedReadme !== readme) {
|
|
329
|
+
readme = managedReadme;
|
|
330
|
+
} else if (!readme.includes('- RBAC integration: demo auth tokens include `health.rbac` permission')) {
|
|
285
331
|
const marker = 'Default demo credentials:';
|
|
286
332
|
if (readme.includes(marker)) {
|
|
287
333
|
readme = readme.replace(
|
|
288
334
|
marker,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
Default demo credentials:`,
|
|
335
|
+
'- RBAC integration: demo auth tokens include `health.rbac` permission\n\nDefault demo credentials:',
|
|
292
336
|
);
|
|
293
337
|
}
|
|
294
338
|
}
|