create-forgeon 0.3.22 → 0.3.24
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/README.md +14 -12
- package/package.json +1 -1
- package/src/modules/accounts.mjs +3 -1
- package/src/modules/communications.mjs +232 -0
- package/src/modules/dependencies.test.mjs +16 -2
- package/src/modules/executor.mjs +4 -0
- package/src/modules/executor.test.mjs +60 -31
- package/src/modules/files-access.mjs +25 -1
- package/src/modules/registry.mjs +18 -2
- package/src/modules/shared/probes.mjs +3 -1
- package/src/run-add-module.test.mjs +4 -0
- package/templates/base/apps/web/src/App.tsx +75 -11
- package/templates/base/apps/web/src/probes.ts +11 -1
- package/templates/base/apps/web/src/styles.css +25 -0
- package/templates/module-fragments/accounts/20_scope.md +3 -2
- package/templates/module-fragments/accounts/90_status_implemented.md +2 -2
- package/templates/module-fragments/accounts/90_status_planned.md +1 -1
- package/templates/module-fragments/communications/00_title.md +1 -0
- package/templates/module-fragments/communications/10_overview.md +6 -0
- package/templates/module-fragments/communications/20_scope.md +20 -0
- package/templates/module-fragments/communications/90_status_implemented.md +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +60 -20
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +2 -6
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +1 -1
- package/templates/module-presets/communications/packages/communications/package.json +23 -0
- package/templates/module-presets/communications/packages/communications/src/communications-config.loader.ts +58 -0
- package/templates/module-presets/communications/packages/communications/src/communications-config.module.ts +11 -0
- package/templates/module-presets/communications/packages/communications/src/communications-config.service.ts +60 -0
- package/templates/module-presets/communications/packages/communications/src/communications-env.schema.ts +24 -0
- package/templates/module-presets/communications/packages/communications/src/communications.constants.ts +3 -0
- package/templates/module-presets/communications/packages/communications/src/communications.probe.controller.ts +18 -0
- package/templates/module-presets/communications/packages/communications/src/communications.service.ts +104 -0
- package/templates/module-presets/communications/packages/communications/src/communications.types.ts +55 -0
- package/templates/module-presets/communications/packages/communications/src/dto/send-communications-probe.dto.ts +6 -0
- package/templates/module-presets/communications/packages/communications/src/email/email-channel.service.ts +90 -0
- package/templates/module-presets/communications/packages/communications/src/email/email-provider.port.ts +16 -0
- package/templates/module-presets/communications/packages/communications/src/email/providers/gmail-smtp-email.provider.ts +64 -0
- package/templates/module-presets/communications/packages/communications/src/forgeon-communications.module.ts +64 -0
- package/templates/module-presets/communications/packages/communications/src/index.ts +21 -0
- package/templates/module-presets/communications/packages/communications/src/push/providers/stub-push.provider.ts +16 -0
- package/templates/module-presets/communications/packages/communications/src/push/push-channel.service.ts +56 -0
- package/templates/module-presets/communications/packages/communications/src/push/push-provider.port.ts +14 -0
- package/templates/module-presets/communications/packages/communications/src/sms/providers/stub-sms.provider.ts +16 -0
- package/templates/module-presets/communications/packages/communications/src/sms/sms-channel.service.ts +56 -0
- package/templates/module-presets/communications/packages/communications/src/sms/sms-provider.port.ts +14 -0
- package/templates/module-presets/communications/packages/communications/src/template-loader.service.ts +98 -0
- package/templates/module-presets/communications/packages/communications/src/template-renderer.service.ts +30 -0
- package/templates/module-presets/communications/packages/communications/tsconfig.json +9 -0
- package/templates/module-presets/communications/resources/communications/email/communications_probe.html +9 -0
- package/templates/module-presets/communications/resources/communications/email/communications_probe.subject.txt +1 -0
- package/templates/module-presets/communications/resources/communications/email/email_verification_code.html +8 -0
- package/templates/module-presets/communications/resources/communications/email/email_verification_code.subject.txt +1 -0
- package/templates/module-presets/communications/resources/communications/email/password_reset.html +8 -0
- package/templates/module-presets/communications/resources/communications/email/password_reset.subject.txt +1 -0
- package/templates/module-presets/communications/resources/communications/email/welcome_email.html +8 -0
- package/templates/module-presets/communications/resources/communications/email/welcome_email.subject.txt +1 -0
- package/templates/module-presets/communications/resources/communications/push/login_alert.json +4 -0
- package/templates/module-presets/communications/resources/communications/sms/phone_verification.txt +1 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/package.json +21 -20
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -118
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +21 -19
- package/templates/module-presets/i18n/apps/web/src/App.tsx +72 -8
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +0 -13
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# create-forgeon
|
|
1
|
+
# create-forgeon
|
|
2
2
|
|
|
3
3
|
CLI package for generating Forgeon fullstack monorepo projects.
|
|
4
4
|
|
|
@@ -9,19 +9,20 @@ CLI package for generating Forgeon fullstack monorepo projects.
|
|
|
9
9
|
> 
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
```bash
|
|
14
14
|
npx create-forgeon@latest my-app --i18n true --db-prisma true --proxy caddy
|
|
15
15
|
npx create-forgeon@latest my-app --db-prisma false --proxy caddy
|
|
16
16
|
```
|
|
17
|
-
|
|
18
|
-
If flags are omitted, the CLI asks interactive questions.
|
|
19
|
-
Project name stays text input; fixed-choice prompts use arrow-key selection (`Up/Down + Enter`).
|
|
20
|
-
|
|
17
|
+
|
|
18
|
+
If flags are omitted, the CLI asks interactive questions.
|
|
19
|
+
Project name stays text input; fixed-choice prompts use arrow-key selection (`Up/Down + Enter`).
|
|
20
|
+
|
|
21
21
|
```bash
|
|
22
22
|
npx create-forgeon@latest add --list
|
|
23
23
|
npx create-forgeon@latest add i18n --project ./my-app
|
|
24
|
-
npx create-forgeon@latest add
|
|
24
|
+
npx create-forgeon@latest add communications --project ./my-app
|
|
25
|
+
npx create-forgeon@latest add accounts --project ./my-app --with-required
|
|
25
26
|
npx create-forgeon@latest add files --with-required --provider db-adapter=db-prisma
|
|
26
27
|
```
|
|
27
28
|
|
|
@@ -29,16 +30,17 @@ npx create-forgeon@latest add files --with-required --provider db-adapter=db-pri
|
|
|
29
30
|
cd my-app
|
|
30
31
|
pnpm forgeon:sync-integrations
|
|
31
32
|
```
|
|
32
|
-
|
|
33
|
-
## Notes
|
|
34
|
-
|
|
33
|
+
|
|
34
|
+
## Notes
|
|
35
|
+
|
|
35
36
|
- Canonical runtime stack is fixed: NestJS + React + Docker.
|
|
36
37
|
- DB is module-driven: `db-prisma` is default-on and can be disabled at scaffold time.
|
|
37
38
|
- Reverse proxy options: `caddy` (default), `nginx`, `none`.
|
|
38
39
|
- `add i18n` is implemented and applies backend/frontend i18n wiring.
|
|
39
|
-
- `add
|
|
40
|
+
- `add communications` is implemented and adds the shared email/sms/push orchestration surface.
|
|
41
|
+
- `add accounts` is implemented and installs the DB-backed accounts/authentication runtime.
|
|
40
42
|
- Integration sync is bundled by default and runs after `add` commands (best-effort).
|
|
41
43
|
- Module notes are written under `modules/<module-id>/README.md`.
|
|
42
44
|
- Hard prerequisites are explicit:
|
|
43
45
|
- TTY mode prompts for provider resolution and install plan confirmation
|
|
44
|
-
- non-TTY mode can use `--with-required` plus `--provider <capability>=<module>`
|
|
46
|
+
- non-TTY mode can use `--with-required` plus `--provider <capability>=<module>`
|
package/package.json
CHANGED
package/src/modules/accounts.mjs
CHANGED
|
@@ -336,7 +336,7 @@ function patchReadme(targetRoot) {
|
|
|
336
336
|
'',
|
|
337
337
|
'Current boundaries:',
|
|
338
338
|
'- `UsersModule.register({ user, profile, settings })` controls runtime defaults for JSON-backed extension fields',
|
|
339
|
-
'- email verification and password
|
|
339
|
+
'- email verification and password-reset request flows send best-effort communication intents through `CommunicationsService`',
|
|
340
340
|
'- base accounts schema does not include RBAC storage',
|
|
341
341
|
ACCOUNTS_RBAC_MARKERS.start,
|
|
342
342
|
ACCOUNTS_DEFAULT_RBAC_BLOCK,
|
|
@@ -420,3 +420,5 @@ export function applyAccountsModule({ packageRoot, targetRoot }) {
|
|
|
420
420
|
|
|
421
421
|
|
|
422
422
|
|
|
423
|
+
|
|
424
|
+
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
+
import {
|
|
5
|
+
ensureBuildSteps,
|
|
6
|
+
ensureDependency,
|
|
7
|
+
ensureLineAfter,
|
|
8
|
+
ensureLineBefore,
|
|
9
|
+
upsertEnvLines,
|
|
10
|
+
} from './shared/patch-utils.mjs';
|
|
11
|
+
import { patchAppModuleRegistration } from './shared/nest-runtime-wiring.mjs';
|
|
12
|
+
import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
|
|
13
|
+
|
|
14
|
+
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
15
|
+
const source = path.join(packageRoot, 'templates', 'module-presets', 'communications', relativePath);
|
|
16
|
+
if (!fs.existsSync(source)) {
|
|
17
|
+
throw new Error(`Missing communications preset template: ${source}`);
|
|
18
|
+
}
|
|
19
|
+
const destination = path.join(targetRoot, relativePath);
|
|
20
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
21
|
+
copyRecursive(source, destination);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function patchApiPackage(targetRoot) {
|
|
25
|
+
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
26
|
+
if (!fs.existsSync(packagePath)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
31
|
+
ensureDependency(packageJson, '@forgeon/communications', 'workspace:*');
|
|
32
|
+
ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/communications build']);
|
|
33
|
+
writeJson(packagePath, packageJson);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function patchAppModule(targetRoot) {
|
|
37
|
+
patchAppModuleRegistration(targetRoot, {
|
|
38
|
+
importLine: "import { communicationsConfig, communicationsEnvSchema, ForgeonCommunicationsModule } from '@forgeon/communications';",
|
|
39
|
+
loadItem: 'communicationsConfig',
|
|
40
|
+
envSchema: 'communicationsEnvSchema',
|
|
41
|
+
moduleLine: ' ForgeonCommunicationsModule.register(),',
|
|
42
|
+
afterAnchors: [' DbPrismaModule,', ' ForgeonLoggerModule,'],
|
|
43
|
+
fallbackAnchor: ' CoreErrorsModule,',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function registerWebProbe(targetRoot) {
|
|
48
|
+
const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'communications' });
|
|
49
|
+
ensureWebProbeDefinition({
|
|
50
|
+
targetRoot,
|
|
51
|
+
probeTargets,
|
|
52
|
+
definition: {
|
|
53
|
+
id: 'communications',
|
|
54
|
+
title: 'Communications',
|
|
55
|
+
buttonLabel: 'Send communications probe email',
|
|
56
|
+
resultTitle: 'Communications probe response',
|
|
57
|
+
path: '/health/communications',
|
|
58
|
+
request: {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
body: {
|
|
61
|
+
email: '$INPUT.email$',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
inputs: [
|
|
65
|
+
{
|
|
66
|
+
id: 'email',
|
|
67
|
+
label: 'Test email',
|
|
68
|
+
type: 'email',
|
|
69
|
+
placeholder: 'you@example.com',
|
|
70
|
+
defaultValue: 'probe@example.com',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function patchApiDockerfile(targetRoot) {
|
|
78
|
+
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
79
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
84
|
+
const packageAnchors = [
|
|
85
|
+
'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
|
|
86
|
+
'COPY packages/logger/package.json packages/logger/package.json',
|
|
87
|
+
'COPY packages/i18n/package.json packages/i18n/package.json',
|
|
88
|
+
'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
|
|
89
|
+
'COPY packages/core/package.json packages/core/package.json',
|
|
90
|
+
];
|
|
91
|
+
const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
|
|
92
|
+
content = ensureLineAfter(content, packageAnchor, 'COPY packages/communications/package.json packages/communications/package.json');
|
|
93
|
+
|
|
94
|
+
const sourceAnchors = [
|
|
95
|
+
'COPY packages/accounts-api packages/accounts-api',
|
|
96
|
+
'COPY packages/logger packages/logger',
|
|
97
|
+
'COPY packages/i18n packages/i18n',
|
|
98
|
+
'COPY packages/db-prisma packages/db-prisma',
|
|
99
|
+
'COPY packages/core packages/core',
|
|
100
|
+
];
|
|
101
|
+
const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
|
|
102
|
+
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/communications packages/communications');
|
|
103
|
+
|
|
104
|
+
content = content.replace(/^RUN pnpm --filter @forgeon\/communications build\r?\n?/gm, '');
|
|
105
|
+
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
|
|
106
|
+
? 'RUN pnpm --filter @forgeon/api prisma:generate'
|
|
107
|
+
: 'RUN pnpm --filter @forgeon/api build';
|
|
108
|
+
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/communications build');
|
|
109
|
+
|
|
110
|
+
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function patchCompose(targetRoot) {
|
|
114
|
+
const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
|
|
115
|
+
if (!fs.existsSync(composePath)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
|
|
120
|
+
if (!content.includes('COMMUNICATIONS_EMAIL_PROVIDER: ${COMMUNICATIONS_EMAIL_PROVIDER}')) {
|
|
121
|
+
content = content.replace(
|
|
122
|
+
/^(\s+API_PREFIX:.*)$/m,
|
|
123
|
+
`$1
|
|
124
|
+
COMMUNICATIONS_TEMPLATES_ROOT: \${COMMUNICATIONS_TEMPLATES_ROOT}
|
|
125
|
+
COMMUNICATIONS_EMAIL_PROVIDER: \${COMMUNICATIONS_EMAIL_PROVIDER}
|
|
126
|
+
COMMUNICATIONS_EMAIL_FROM: \${COMMUNICATIONS_EMAIL_FROM}
|
|
127
|
+
COMMUNICATIONS_EMAIL_REPLY_TO: \${COMMUNICATIONS_EMAIL_REPLY_TO}
|
|
128
|
+
COMMUNICATIONS_EMAIL_SUBJECT_PREFIX: \${COMMUNICATIONS_EMAIL_SUBJECT_PREFIX}
|
|
129
|
+
COMMUNICATIONS_EMAIL_SMTP_HOST: \${COMMUNICATIONS_EMAIL_SMTP_HOST}
|
|
130
|
+
COMMUNICATIONS_EMAIL_SMTP_PORT: \${COMMUNICATIONS_EMAIL_SMTP_PORT}
|
|
131
|
+
COMMUNICATIONS_EMAIL_SMTP_SECURE: \${COMMUNICATIONS_EMAIL_SMTP_SECURE}
|
|
132
|
+
COMMUNICATIONS_EMAIL_SMTP_USER: \${COMMUNICATIONS_EMAIL_SMTP_USER}
|
|
133
|
+
COMMUNICATIONS_EMAIL_SMTP_PASS: \${COMMUNICATIONS_EMAIL_SMTP_PASS}
|
|
134
|
+
COMMUNICATIONS_SMS_PROVIDER: \${COMMUNICATIONS_SMS_PROVIDER}
|
|
135
|
+
COMMUNICATIONS_PUSH_PROVIDER: \${COMMUNICATIONS_PUSH_PROVIDER}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function patchReadme(targetRoot) {
|
|
143
|
+
const readmePath = path.join(targetRoot, 'README.md');
|
|
144
|
+
if (!fs.existsSync(readmePath)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const section = [
|
|
149
|
+
'## Communications Module',
|
|
150
|
+
'',
|
|
151
|
+
'The communications add-module provides a single orchestration surface for email, sms, and push delivery.',
|
|
152
|
+
'',
|
|
153
|
+
'What it adds:',
|
|
154
|
+
'- `@forgeon/communications` backend runtime with file-based template loading and simple placeholder rendering',
|
|
155
|
+
'- real email delivery through the Gmail SMTP transport configuration',
|
|
156
|
+
'- SMS and PUSH stub channels for future expansion',
|
|
157
|
+
'- probe routes: `GET /api/health/communications` and `POST /api/health/communications`',
|
|
158
|
+
'- generated resources under `resources/communications/email`, `resources/communications/sms`, and `resources/communications/push`',
|
|
159
|
+
'',
|
|
160
|
+
'Current boundaries:',
|
|
161
|
+
'- domain modules should inject only `CommunicationsService`',
|
|
162
|
+
'- provider selection is module-owned configuration, never a runtime input field',
|
|
163
|
+
'- queues, scheduling, delivery history, and retries are intentionally out of scope for v1',
|
|
164
|
+
'',
|
|
165
|
+
'Example env keys:',
|
|
166
|
+
'- `COMMUNICATIONS_EMAIL_PROVIDER=gmail-smtp`',
|
|
167
|
+
'- `COMMUNICATIONS_EMAIL_SMTP_HOST=smtp.gmail.com`',
|
|
168
|
+
'- `COMMUNICATIONS_EMAIL_SMTP_USER=`',
|
|
169
|
+
'- `COMMUNICATIONS_EMAIL_SMTP_PASS=`',
|
|
170
|
+
].join('\n');
|
|
171
|
+
|
|
172
|
+
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
173
|
+
const sectionHeading = '## Communications Module';
|
|
174
|
+
if (content.includes(sectionHeading)) {
|
|
175
|
+
const start = content.indexOf(sectionHeading);
|
|
176
|
+
const tail = content.slice(start + sectionHeading.length);
|
|
177
|
+
const nextHeadingMatch = tail.match(/\n##\s+/);
|
|
178
|
+
const end =
|
|
179
|
+
nextHeadingMatch && nextHeadingMatch.index !== undefined
|
|
180
|
+
? start + sectionHeading.length + nextHeadingMatch.index + 1
|
|
181
|
+
: content.length;
|
|
182
|
+
content = `${content.slice(0, start)}${section}\n\n${content.slice(end).replace(/^\n+/, '')}`;
|
|
183
|
+
} else if (content.includes('## Prisma In Docker Start')) {
|
|
184
|
+
content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
|
|
185
|
+
} else {
|
|
186
|
+
content = `${content.trimEnd()}\n\n${section}\n`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function applyCommunicationsModule({ packageRoot, targetRoot }) {
|
|
193
|
+
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'communications'));
|
|
194
|
+
copyFromPreset(packageRoot, targetRoot, path.join('resources', 'communications'));
|
|
195
|
+
|
|
196
|
+
patchApiPackage(targetRoot);
|
|
197
|
+
patchAppModule(targetRoot);
|
|
198
|
+
registerWebProbe(targetRoot);
|
|
199
|
+
patchApiDockerfile(targetRoot);
|
|
200
|
+
patchCompose(targetRoot);
|
|
201
|
+
patchReadme(targetRoot);
|
|
202
|
+
|
|
203
|
+
upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
|
|
204
|
+
'COMMUNICATIONS_TEMPLATES_ROOT=resources/communications',
|
|
205
|
+
'COMMUNICATIONS_EMAIL_PROVIDER=gmail-smtp',
|
|
206
|
+
'COMMUNICATIONS_EMAIL_FROM=Forgeon <no-reply@example.com>',
|
|
207
|
+
'COMMUNICATIONS_EMAIL_REPLY_TO=',
|
|
208
|
+
'COMMUNICATIONS_EMAIL_SUBJECT_PREFIX=[Forgeon]',
|
|
209
|
+
'COMMUNICATIONS_EMAIL_SMTP_HOST=smtp.gmail.com',
|
|
210
|
+
'COMMUNICATIONS_EMAIL_SMTP_PORT=587',
|
|
211
|
+
'COMMUNICATIONS_EMAIL_SMTP_SECURE=false',
|
|
212
|
+
'COMMUNICATIONS_EMAIL_SMTP_USER=',
|
|
213
|
+
'COMMUNICATIONS_EMAIL_SMTP_PASS=',
|
|
214
|
+
'COMMUNICATIONS_SMS_PROVIDER=stub',
|
|
215
|
+
'COMMUNICATIONS_PUSH_PROVIDER=stub',
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
|
|
219
|
+
'COMMUNICATIONS_TEMPLATES_ROOT=resources/communications',
|
|
220
|
+
'COMMUNICATIONS_EMAIL_PROVIDER=gmail-smtp',
|
|
221
|
+
'COMMUNICATIONS_EMAIL_FROM=Forgeon <no-reply@example.com>',
|
|
222
|
+
'COMMUNICATIONS_EMAIL_REPLY_TO=',
|
|
223
|
+
'COMMUNICATIONS_EMAIL_SUBJECT_PREFIX=[Forgeon]',
|
|
224
|
+
'COMMUNICATIONS_EMAIL_SMTP_HOST=smtp.gmail.com',
|
|
225
|
+
'COMMUNICATIONS_EMAIL_SMTP_PORT=587',
|
|
226
|
+
'COMMUNICATIONS_EMAIL_SMTP_SECURE=false',
|
|
227
|
+
'COMMUNICATIONS_EMAIL_SMTP_USER=',
|
|
228
|
+
'COMMUNICATIONS_EMAIL_SMTP_PASS=',
|
|
229
|
+
'COMMUNICATIONS_SMS_PROVIDER=stub',
|
|
230
|
+
'COMMUNICATIONS_PUSH_PROVIDER=stub',
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
@@ -26,13 +26,22 @@ const TEST_PRESETS = [
|
|
|
26
26
|
requires: [],
|
|
27
27
|
optionalIntegrations: [],
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
id: 'communications',
|
|
31
|
+
label: 'Communications',
|
|
32
|
+
implemented: true,
|
|
33
|
+
detectionPaths: ['packages/communications/package.json'],
|
|
34
|
+
provides: ['communications-runtime'],
|
|
35
|
+
requires: [],
|
|
36
|
+
optionalIntegrations: [],
|
|
37
|
+
},
|
|
29
38
|
{
|
|
30
39
|
id: 'accounts',
|
|
31
40
|
label: 'Accounts',
|
|
32
41
|
implemented: true,
|
|
33
42
|
detectionPaths: ['packages/accounts-api/package.json'],
|
|
34
43
|
provides: ['accounts-runtime'],
|
|
35
|
-
requires: [{ type: 'capability', id: 'db-adapter' }],
|
|
44
|
+
requires: [{ type: 'capability', id: 'db-adapter' }, { type: 'capability', id: 'communications-runtime' }],
|
|
36
45
|
optionalIntegrations: [
|
|
37
46
|
{
|
|
38
47
|
id: 'accounts-rbac',
|
|
@@ -204,9 +213,10 @@ describe('module dependency helpers', () => {
|
|
|
204
213
|
});
|
|
205
214
|
|
|
206
215
|
assert.equal(result.cancelled, false);
|
|
207
|
-
assert.deepEqual(result.moduleSequence, ['db-prisma', 'accounts']);
|
|
216
|
+
assert.deepEqual(result.moduleSequence, ['db-prisma', 'communications', 'accounts']);
|
|
208
217
|
assert.deepEqual(result.selectedProviders, {
|
|
209
218
|
'db-adapter': 'db-prisma',
|
|
219
|
+
'communications-runtime': 'communications',
|
|
210
220
|
});
|
|
211
221
|
} finally {
|
|
212
222
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
@@ -375,6 +385,7 @@ describe('module dependency helpers', () => {
|
|
|
375
385
|
assert.equal(result.cancelled, false);
|
|
376
386
|
assert.deepEqual(result.moduleSequence, [
|
|
377
387
|
'db-prisma',
|
|
388
|
+
'communications',
|
|
378
389
|
'accounts',
|
|
379
390
|
'rbac',
|
|
380
391
|
'files-local',
|
|
@@ -388,6 +399,7 @@ describe('module dependency helpers', () => {
|
|
|
388
399
|
assert.deepEqual(result.selectedProviders, {
|
|
389
400
|
'files-storage-adapter': 'files-local',
|
|
390
401
|
'db-adapter': 'db-prisma',
|
|
402
|
+
'communications-runtime': 'communications',
|
|
391
403
|
'files-runtime': 'files',
|
|
392
404
|
'queue-runtime': 'queue',
|
|
393
405
|
});
|
|
@@ -499,3 +511,5 @@ describe('module dependency helpers', () => {
|
|
|
499
511
|
}
|
|
500
512
|
});
|
|
501
513
|
});
|
|
514
|
+
|
|
515
|
+
|
package/src/modules/executor.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { applyFilesLocalModule } from './files-local.mjs';
|
|
|
11
11
|
import { applyFilesS3Module } from './files-s3.mjs';
|
|
12
12
|
import { applyI18nModule } from './i18n.mjs';
|
|
13
13
|
import { applyAccountsModule } from './accounts.mjs';
|
|
14
|
+
import { applyCommunicationsModule } from './communications.mjs';
|
|
14
15
|
import { applyLoggerModule } from './logger.mjs';
|
|
15
16
|
import { applyRateLimitModule } from './rate-limit.mjs';
|
|
16
17
|
import { applyRbacModule } from './rbac.mjs';
|
|
@@ -44,6 +45,7 @@ const MODULE_APPLIERS = {
|
|
|
44
45
|
'files-s3': applyFilesS3Module,
|
|
45
46
|
i18n: applyI18nModule,
|
|
46
47
|
accounts: applyAccountsModule,
|
|
48
|
+
communications: applyCommunicationsModule,
|
|
47
49
|
logger: applyLoggerModule,
|
|
48
50
|
queue: applyQueueModule,
|
|
49
51
|
'rate-limit': applyRateLimitModule,
|
|
@@ -86,3 +88,5 @@ export function addModule({ moduleId, targetRoot, packageRoot, writeDocs = true
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
|
|
91
|
+
|
|
92
|
+
|
|
@@ -506,6 +506,16 @@ function assertFilesAccessWiring(projectRoot) {
|
|
|
506
506
|
assert.match(filesController, /@Req\(\) req: any/);
|
|
507
507
|
assert.match(filesController, /openDownload\(publicId,\s*variant\)/);
|
|
508
508
|
|
|
509
|
+
const filesModule = fs.readFileSync(
|
|
510
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
|
|
511
|
+
'utf8',
|
|
512
|
+
);
|
|
513
|
+
assert.match(filesModule, /import \{ ForgeonFilesAccessModule \} from '@forgeon\/files-access';/);
|
|
514
|
+
assert.match(
|
|
515
|
+
filesModule,
|
|
516
|
+
/imports: \[FilesConfigModule, ForgeonFilesAccessModule, (?:ForgeonFilesImageModule, )?DbPrismaModule, \.\.\.\(options\.imports \?\? \[\]\)\],/,
|
|
517
|
+
);
|
|
518
|
+
|
|
509
519
|
const healthController = fs.readFileSync(
|
|
510
520
|
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
511
521
|
'utf8',
|
|
@@ -532,14 +542,14 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
532
542
|
assert.match(appModule, /filesQuotasEnvSchema/);
|
|
533
543
|
assert.match(appModule, /ForgeonFilesQuotasModule/);
|
|
534
544
|
|
|
535
|
-
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
536
|
-
assert.match(apiPackage, /@forgeon\/files-quotas/);
|
|
537
|
-
assert.match(apiPackage, /pnpm --filter @forgeon\/files-quotas build/);
|
|
538
|
-
assert.equal(
|
|
539
|
-
apiPackage.indexOf('pnpm --filter @forgeon/files-quotas build') >
|
|
540
|
-
apiPackage.indexOf('pnpm --filter @forgeon/files build'),
|
|
541
|
-
true,
|
|
542
|
-
);
|
|
545
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
546
|
+
assert.match(apiPackage, /@forgeon\/files-quotas/);
|
|
547
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files-quotas build/);
|
|
548
|
+
assert.equal(
|
|
549
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files-quotas build') >
|
|
550
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files build'),
|
|
551
|
+
true,
|
|
552
|
+
);
|
|
543
553
|
|
|
544
554
|
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
545
555
|
assert.doesNotMatch(filesPackage, /@forgeon\/files-quotas/);
|
|
@@ -550,13 +560,13 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
550
560
|
apiDockerfile,
|
|
551
561
|
/COPY packages\/files-quotas\/package\.json packages\/files-quotas\/package\.json/,
|
|
552
562
|
);
|
|
553
|
-
assert.match(apiDockerfile, /COPY packages\/files-quotas packages\/files-quotas/);
|
|
554
|
-
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-quotas build/);
|
|
555
|
-
assert.equal(
|
|
556
|
-
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-quotas build') >
|
|
557
|
-
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
|
|
558
|
-
true,
|
|
559
|
-
);
|
|
563
|
+
assert.match(apiDockerfile, /COPY packages\/files-quotas packages\/files-quotas/);
|
|
564
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-quotas build/);
|
|
565
|
+
assert.equal(
|
|
566
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-quotas build') >
|
|
567
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
|
|
568
|
+
true,
|
|
569
|
+
);
|
|
560
570
|
|
|
561
571
|
const filesController = fs.readFileSync(
|
|
562
572
|
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
@@ -591,6 +601,22 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
591
601
|
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
592
602
|
assert.match(readme, /## Files Quotas Module/);
|
|
593
603
|
assert.match(readme, /owner-based limits/i);
|
|
604
|
+
|
|
605
|
+
const filesQuotasService = fs.readFileSync(
|
|
606
|
+
path.join(projectRoot, 'packages', 'files-quotas', 'src', 'files-quotas.service.ts'),
|
|
607
|
+
'utf8',
|
|
608
|
+
);
|
|
609
|
+
assert.match(filesQuotasService, /FilesStore/);
|
|
610
|
+
assert.match(filesQuotasService, /filesStore\.countOwnerUsage/);
|
|
611
|
+
assert.doesNotMatch(filesQuotasService, /FilesService/);
|
|
612
|
+
|
|
613
|
+
const filesQuotasModule = fs.readFileSync(
|
|
614
|
+
path.join(projectRoot, 'packages', 'files-quotas', 'src', 'forgeon-files-quotas.module.ts'),
|
|
615
|
+
'utf8',
|
|
616
|
+
);
|
|
617
|
+
assert.match(filesQuotasModule, /DbPrismaModule/);
|
|
618
|
+
assert.match(filesQuotasModule, /FilesStore/);
|
|
619
|
+
assert.doesNotMatch(filesQuotasModule, /ForgeonFilesModule/);
|
|
594
620
|
}
|
|
595
621
|
|
|
596
622
|
function assertFilesImageWiring(projectRoot) {
|
|
@@ -611,20 +637,20 @@ function assertFilesImageWiring(projectRoot) {
|
|
|
611
637
|
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
612
638
|
assert.match(filesPackage, /@forgeon\/files-image/);
|
|
613
639
|
|
|
614
|
-
const filesModule = fs.readFileSync(
|
|
615
|
-
path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
|
|
616
|
-
'utf8',
|
|
617
|
-
);
|
|
618
|
-
assert.match(filesModule, /ForgeonFilesImageModule/);
|
|
619
|
-
assert.match(
|
|
620
|
-
filesModule,
|
|
621
|
-
/imports: \[FilesConfigModule, (?:ForgeonFilesAccessModule, )?ForgeonFilesImageModule, DbPrismaModule, \.\.\.\(options\.imports \?\? \[\]\)\],/,
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
const filesService = fs.readFileSync(
|
|
625
|
-
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
626
|
-
'utf8',
|
|
627
|
-
);
|
|
640
|
+
const filesModule = fs.readFileSync(
|
|
641
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
|
|
642
|
+
'utf8',
|
|
643
|
+
);
|
|
644
|
+
assert.match(filesModule, /ForgeonFilesImageModule/);
|
|
645
|
+
assert.match(
|
|
646
|
+
filesModule,
|
|
647
|
+
/imports: \[FilesConfigModule, (?:ForgeonFilesAccessModule, )?ForgeonFilesImageModule, DbPrismaModule, \.\.\.\(options\.imports \?\? \[\]\)\],/,
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const filesService = fs.readFileSync(
|
|
651
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
652
|
+
'utf8',
|
|
653
|
+
);
|
|
628
654
|
assert.match(filesService, /FilesImageService/);
|
|
629
655
|
assert.match(filesService, /filesImageService\.sanitizeForStorage/);
|
|
630
656
|
assert.match(filesService, /sanitizeForStorage\({/);
|
|
@@ -770,7 +796,7 @@ function assertAccountsWiring(projectRoot) {
|
|
|
770
796
|
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
771
797
|
assert.match(readme, /## Accounts Module/);
|
|
772
798
|
assert.match(readme, /owner-scoped user routes/);
|
|
773
|
-
assert.match(readme, /
|
|
799
|
+
assert.match(readme, /CommunicationsService/);
|
|
774
800
|
|
|
775
801
|
const authServiceSource = fs.readFileSync(
|
|
776
802
|
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.service.ts'),
|
|
@@ -789,7 +815,8 @@ function assertAccountsWiring(projectRoot) {
|
|
|
789
815
|
path.join(projectRoot, 'packages', 'accounts-api', 'package.json'),
|
|
790
816
|
'utf8',
|
|
791
817
|
);
|
|
792
|
-
assert.match(accountsApiPackage, /@forgeon\/db-prisma/);
|
|
818
|
+
assert.match(accountsApiPackage, /@forgeon\/db-prisma/);
|
|
819
|
+
assert.match(accountsApiPackage, /@forgeon\/communications/);
|
|
793
820
|
|
|
794
821
|
const authStoreSource = fs.readFileSync(
|
|
795
822
|
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.store.ts'),
|
|
@@ -2681,5 +2708,7 @@ describe('addModule', () => {
|
|
|
2681
2708
|
|
|
2682
2709
|
|
|
2683
2710
|
|
|
2711
|
+
|
|
2712
|
+
|
|
2684
2713
|
|
|
2685
2714
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
4
|
import {
|
|
@@ -51,6 +51,29 @@ function patchFilesPackage(targetRoot) {
|
|
|
51
51
|
writeJson(packagePath, packageJson);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function patchFilesModule(targetRoot) {
|
|
55
|
+
const filePath = path.join(targetRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts');
|
|
56
|
+
if (!fs.existsSync(filePath)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
61
|
+
content = ensureImportLine(content, "import { ForgeonFilesAccessModule } from '@forgeon/files-access';");
|
|
62
|
+
|
|
63
|
+
if (!content.includes('imports: [FilesConfigModule, ForgeonFilesAccessModule,')) {
|
|
64
|
+
content = content.replace(
|
|
65
|
+
'imports: [FilesConfigModule, ForgeonFilesImageModule, DbPrismaModule, ...(options.imports ?? [])],',
|
|
66
|
+
'imports: [FilesConfigModule, ForgeonFilesAccessModule, ForgeonFilesImageModule, DbPrismaModule, ...(options.imports ?? [])],',
|
|
67
|
+
);
|
|
68
|
+
content = content.replace(
|
|
69
|
+
'imports: [FilesConfigModule, DbPrismaModule, ...(options.imports ?? [])],',
|
|
70
|
+
'imports: [FilesConfigModule, ForgeonFilesAccessModule, DbPrismaModule, ...(options.imports ?? [])],',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
function patchAppModule(targetRoot) {
|
|
55
78
|
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
56
79
|
if (!fs.existsSync(filePath)) {
|
|
@@ -366,6 +389,7 @@ export function applyFilesAccessModule({ packageRoot, targetRoot }) {
|
|
|
366
389
|
|
|
367
390
|
patchApiPackage(targetRoot);
|
|
368
391
|
patchFilesPackage(targetRoot);
|
|
392
|
+
patchFilesModule(targetRoot);
|
|
369
393
|
patchAppModule(targetRoot);
|
|
370
394
|
patchFilesController(targetRoot);
|
|
371
395
|
patchHealthController(targetRoot, probeTargets);
|
package/src/modules/registry.mjs
CHANGED
|
@@ -139,16 +139,29 @@ const MODULE_PRESETS = {
|
|
|
139
139
|
optionalIntegrations: [],
|
|
140
140
|
docFragments: ['00_title', '10_overview', '20_scope', '90_status_implemented'],
|
|
141
141
|
},
|
|
142
|
+
communications: {
|
|
143
|
+
id: 'communications',
|
|
144
|
+
label: 'Communications',
|
|
145
|
+
category: 'platform-communications',
|
|
146
|
+
implemented: true,
|
|
147
|
+
description:
|
|
148
|
+
'Canonical communication orchestration module with file-based templates, Gmail SMTP email delivery, and SMS/PUSH stubs.',
|
|
149
|
+
detectionPaths: ['packages/communications/package.json'],
|
|
150
|
+
provides: ['communications-runtime'],
|
|
151
|
+
requires: [],
|
|
152
|
+
optionalIntegrations: [],
|
|
153
|
+
docFragments: ['00_title', '10_overview', '20_scope', '90_status_implemented'],
|
|
154
|
+
},
|
|
142
155
|
accounts: {
|
|
143
156
|
id: 'accounts',
|
|
144
157
|
label: 'Accounts',
|
|
145
158
|
category: 'auth-security',
|
|
146
159
|
implemented: true,
|
|
147
160
|
description:
|
|
148
|
-
'Accounts umbrella module with DB-backed users/auth runtime, argon2 passwords, JWT access+refresh rotation,
|
|
161
|
+
'Accounts umbrella module with DB-backed users/auth runtime, argon2 passwords, JWT access+refresh rotation, owner-scoped self-service routes, and communications integration.',
|
|
149
162
|
detectionPaths: ['packages/accounts-api/package.json'],
|
|
150
163
|
provides: ['accounts-runtime'],
|
|
151
|
-
requires: [{ type: 'capability', id: 'db-adapter' }],
|
|
164
|
+
requires: [{ type: 'capability', id: 'db-adapter' }, { type: 'capability', id: 'communications-runtime' }],
|
|
152
165
|
optionalIntegrations: [
|
|
153
166
|
{
|
|
154
167
|
id: 'accounts-rbac',
|
|
@@ -307,3 +320,6 @@ export function ensureModuleExists(moduleId) {
|
|
|
307
320
|
|
|
308
321
|
|
|
309
322
|
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { readJson } from '../../utils/fs.mjs';
|
|
4
4
|
|
|
@@ -17,6 +17,7 @@ const probeOrders = {
|
|
|
17
17
|
validation: 30,
|
|
18
18
|
db: 40,
|
|
19
19
|
auth: 50,
|
|
20
|
+
communications: 55,
|
|
20
21
|
rbac: 60,
|
|
21
22
|
'rate-limit': 70,
|
|
22
23
|
files: 80,
|
|
@@ -233,3 +234,4 @@ export function readManagedWebProbeDefinitions(targetRoot) {
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
|
|
237
|
+
|