create-forgeon 0.3.25 → 0.3.26
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/accounts-communications.mjs +146 -0
- package/src/modules/accounts.mjs +17 -5
- package/src/modules/dependencies.test.mjs +31 -5
- package/src/modules/executor.mjs +2 -0
- package/src/modules/executor.test.mjs +18 -17
- package/src/modules/registry.mjs +18 -2
- package/templates/module-fragments/accounts/20_scope.md +4 -5
- package/templates/module-fragments/accounts/90_status_implemented.md +2 -2
- package/templates/module-fragments/accounts-communications/00_title.md +1 -0
- package/templates/module-fragments/accounts-communications/10_overview.md +3 -0
- package/templates/module-fragments/accounts-communications/20_scope.md +24 -0
- package/templates/module-fragments/accounts-communications/90_status_implemented.md +3 -0
- package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +22 -1
- package/templates/module-presets/accounts/packages/accounts-api/package.json +0 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +122 -117
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-pending-operations.ts +9 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +2 -21
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.handlers.ts +45 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +19 -18
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.store.ts +87 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +30 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +2 -0
- package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +37 -1
- package/templates/module-presets/accounts-communications/packages/accounts-communications/package.json +22 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/auth-communications.controller.ts +69 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/auth-communications.service.ts +221 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/confirmed-change-password.handler.ts +16 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/confirm-change-email.dto.ts +8 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/confirm-change-password.dto.ts +8 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/confirm-password-reset.dto.ts +12 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/index.ts +6 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/request-change-email.dto.ts +7 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/request-password-reset.dto.ts +7 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/dto/verify-email.dto.ts +8 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/index.ts +5 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/src/pending-verification-register.handler.ts +13 -0
- package/templates/module-presets/accounts-communications/packages/accounts-communications/tsconfig.json +9 -0
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
ensureImportLine,
|
|
8
|
+
ensureLineAfter,
|
|
9
|
+
ensureLineBefore,
|
|
10
|
+
} from './shared/patch-utils.mjs';
|
|
11
|
+
|
|
12
|
+
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
13
|
+
const source = path.join(packageRoot, 'templates', 'module-presets', 'accounts-communications', relativePath);
|
|
14
|
+
if (!fs.existsSync(source)) {
|
|
15
|
+
throw new Error(`Missing accounts-communications preset template: ${source}`);
|
|
16
|
+
}
|
|
17
|
+
const destination = path.join(targetRoot, relativePath);
|
|
18
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
19
|
+
copyRecursive(source, destination);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function patchApiPackage(targetRoot) {
|
|
23
|
+
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
24
|
+
if (!fs.existsSync(packagePath)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
29
|
+
ensureDependency(packageJson, '@forgeon/accounts-communications', 'workspace:*');
|
|
30
|
+
ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/accounts-communications build']);
|
|
31
|
+
writeJson(packagePath, packageJson);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function patchAppModule(targetRoot) {
|
|
35
|
+
const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
|
|
36
|
+
if (!fs.existsSync(filePath)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
41
|
+
content = ensureImportLine(
|
|
42
|
+
content,
|
|
43
|
+
"import { AuthCommunicationsController, AuthCommunicationsService, ConfirmedChangePasswordHandler, PendingVerificationRegisterHandler } from '@forgeon/accounts-communications';",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const accountsModuleLine = ` ForgeonAccountsModule.register({
|
|
47
|
+
users: UsersModule.register({}),
|
|
48
|
+
controllers: [AuthCommunicationsController],
|
|
49
|
+
providers: [
|
|
50
|
+
AuthCommunicationsService,
|
|
51
|
+
PendingVerificationRegisterHandler,
|
|
52
|
+
ConfirmedChangePasswordHandler,
|
|
53
|
+
],
|
|
54
|
+
handlers: {
|
|
55
|
+
register: PendingVerificationRegisterHandler,
|
|
56
|
+
changePassword: ConfirmedChangePasswordHandler,
|
|
57
|
+
},
|
|
58
|
+
}),`;
|
|
59
|
+
|
|
60
|
+
if (content.includes(' ForgeonAccountsModule.register({')) {
|
|
61
|
+
content = content.replace(
|
|
62
|
+
/ {4}ForgeonAccountsModule\.register\([\s\S]*? {4}\}\),/m,
|
|
63
|
+
accountsModuleLine,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function patchApiDockerfile(targetRoot) {
|
|
71
|
+
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
72
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
77
|
+
content = ensureLineAfter(
|
|
78
|
+
content,
|
|
79
|
+
'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
|
|
80
|
+
'COPY packages/accounts-communications/package.json packages/accounts-communications/package.json',
|
|
81
|
+
);
|
|
82
|
+
content = ensureLineAfter(
|
|
83
|
+
content,
|
|
84
|
+
'COPY packages/accounts-api packages/accounts-api',
|
|
85
|
+
'COPY packages/accounts-communications packages/accounts-communications',
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
content = content.replace(/^RUN pnpm --filter @forgeon\/accounts-communications build\r?\n?/gm, '');
|
|
89
|
+
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
|
|
90
|
+
? 'RUN pnpm --filter @forgeon/api prisma:generate'
|
|
91
|
+
: 'RUN pnpm --filter @forgeon/api build';
|
|
92
|
+
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/accounts-communications build');
|
|
93
|
+
|
|
94
|
+
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function patchReadme(targetRoot) {
|
|
98
|
+
const readmePath = path.join(targetRoot, 'README.md');
|
|
99
|
+
if (!fs.existsSync(readmePath)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const section = [
|
|
104
|
+
'## Accounts Communications Module',
|
|
105
|
+
'',
|
|
106
|
+
'The accounts-communications add-module extends the base accounts runtime with communications-backed auth/account flows.',
|
|
107
|
+
'',
|
|
108
|
+
'What it adds:',
|
|
109
|
+
'- `@forgeon/accounts-communications` extension runtime for messaging-based auth/account operations',
|
|
110
|
+
'- pending-verification registration mode',
|
|
111
|
+
'- confirmable password changes and password reset flows',
|
|
112
|
+
'- email change confirmation routes under the same `/api/auth/*` namespace',
|
|
113
|
+
'',
|
|
114
|
+
'Current boundaries:',
|
|
115
|
+
'- requires both `accounts` and `communications`',
|
|
116
|
+
'- rebinds `register` and `change-password` handler implementations through the accounts composition point',
|
|
117
|
+
'- keeps base account state and pending-operation records inside `accounts`',
|
|
118
|
+
].join('\n');
|
|
119
|
+
|
|
120
|
+
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
121
|
+
const sectionHeading = '## Accounts Communications Module';
|
|
122
|
+
if (content.includes(sectionHeading)) {
|
|
123
|
+
const start = content.indexOf(sectionHeading);
|
|
124
|
+
const tail = content.slice(start + sectionHeading.length);
|
|
125
|
+
const nextHeadingMatch = tail.match(/\n##\s+/);
|
|
126
|
+
const end =
|
|
127
|
+
nextHeadingMatch && nextHeadingMatch.index !== undefined
|
|
128
|
+
? start + sectionHeading.length + nextHeadingMatch.index + 1
|
|
129
|
+
: content.length;
|
|
130
|
+
content = `${content.slice(0, start)}${section}\n\n${content.slice(end).replace(/^\n+/, '')}`;
|
|
131
|
+
} else if (content.includes('## Prisma In Docker Start')) {
|
|
132
|
+
content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
|
|
133
|
+
} else {
|
|
134
|
+
content = `${content.trimEnd()}\n\n${section}\n`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function applyAccountsCommunicationsModule({ packageRoot, targetRoot }) {
|
|
141
|
+
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-communications'));
|
|
142
|
+
patchApiPackage(targetRoot);
|
|
143
|
+
patchAppModule(targetRoot);
|
|
144
|
+
patchApiDockerfile(targetRoot);
|
|
145
|
+
patchReadme(targetRoot);
|
|
146
|
+
}
|
package/src/modules/accounts.mjs
CHANGED
|
@@ -57,10 +57,11 @@ function patchPrismaSchema(targetRoot) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
let content = fs.readFileSync(schemaPath, 'utf8').replace(/\r\n/g, '\n');
|
|
60
|
-
|
|
60
|
+
const userModel = `model User {
|
|
61
61
|
id String @id @default(cuid())
|
|
62
62
|
status String @default("active")
|
|
63
63
|
data Json?
|
|
64
|
+
emailVerifiedAt DateTime?
|
|
64
65
|
createdAt DateTime @default(now())
|
|
65
66
|
updatedAt DateTime @updatedAt
|
|
66
67
|
deletedAt DateTime?
|
|
@@ -69,6 +70,7 @@ function patchPrismaSchema(targetRoot) {
|
|
|
69
70
|
authIdentities AuthIdentity[]
|
|
70
71
|
authCredential AuthCredential?
|
|
71
72
|
authRefreshTokens AuthRefreshToken[]
|
|
73
|
+
authPendingOperations AuthPendingOperation[]
|
|
72
74
|
}`;
|
|
73
75
|
|
|
74
76
|
if (/model User \{[\s\S]*?\n\}/m.test(content)) {
|
|
@@ -119,6 +121,19 @@ function patchPrismaSchema(targetRoot) {
|
|
|
119
121
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
120
122
|
|
|
121
123
|
@@index([userId, createdAt])
|
|
124
|
+
}`,
|
|
125
|
+
`model AuthPendingOperation {
|
|
126
|
+
id String @id @default(cuid())
|
|
127
|
+
userId String
|
|
128
|
+
type String
|
|
129
|
+
tokenHash String
|
|
130
|
+
metadata Json?
|
|
131
|
+
expiresAt DateTime
|
|
132
|
+
consumedAt DateTime?
|
|
133
|
+
createdAt DateTime @default(now())
|
|
134
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
135
|
+
|
|
136
|
+
@@index([userId, type, createdAt])
|
|
122
137
|
}`,
|
|
123
138
|
];
|
|
124
139
|
|
|
@@ -336,7 +351,7 @@ function patchReadme(targetRoot) {
|
|
|
336
351
|
'',
|
|
337
352
|
'Current boundaries:',
|
|
338
353
|
'- `UsersModule.register({ user, profile, settings })` controls runtime defaults for JSON-backed extension fields',
|
|
339
|
-
'-
|
|
354
|
+
'- base accounts runtime works without `communications`; delivery-assisted auth/account flows belong to the optional `accounts-communications` extension',
|
|
340
355
|
'- base accounts schema does not include RBAC storage',
|
|
341
356
|
ACCOUNTS_RBAC_MARKERS.start,
|
|
342
357
|
ACCOUNTS_DEFAULT_RBAC_BLOCK,
|
|
@@ -349,9 +364,6 @@ function patchReadme(targetRoot) {
|
|
|
349
364
|
'- `POST /api/auth/logout`',
|
|
350
365
|
'- `GET /api/auth/me`',
|
|
351
366
|
'- `POST /api/auth/change-password`',
|
|
352
|
-
'- `POST /api/auth/verify-email` (stub)',
|
|
353
|
-
'- `POST /api/auth/password-reset/request` (stub)',
|
|
354
|
-
'- `POST /api/auth/password-reset/confirm` (stub)',
|
|
355
367
|
].join('\n');
|
|
356
368
|
|
|
357
369
|
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
@@ -41,7 +41,7 @@ const TEST_PRESETS = [
|
|
|
41
41
|
implemented: true,
|
|
42
42
|
detectionPaths: ['packages/accounts-api/package.json'],
|
|
43
43
|
provides: ['accounts-runtime'],
|
|
44
|
-
requires: [{ type: 'capability', id: 'db-adapter' }
|
|
44
|
+
requires: [{ type: 'capability', id: 'db-adapter' }],
|
|
45
45
|
optionalIntegrations: [
|
|
46
46
|
{
|
|
47
47
|
id: 'accounts-rbac',
|
|
@@ -56,6 +56,15 @@ const TEST_PRESETS = [
|
|
|
56
56
|
},
|
|
57
57
|
],
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
id: 'accounts-communications',
|
|
61
|
+
label: 'Accounts Communications',
|
|
62
|
+
implemented: true,
|
|
63
|
+
detectionPaths: ['packages/accounts-communications/package.json'],
|
|
64
|
+
provides: ['accounts-communications-runtime'],
|
|
65
|
+
requires: [{ type: 'module', id: 'accounts' }, { type: 'module', id: 'communications' }],
|
|
66
|
+
optionalIntegrations: [],
|
|
67
|
+
},
|
|
59
68
|
{
|
|
60
69
|
id: 'rbac',
|
|
61
70
|
label: 'RBAC',
|
|
@@ -213,10 +222,9 @@ describe('module dependency helpers', () => {
|
|
|
213
222
|
});
|
|
214
223
|
|
|
215
224
|
assert.equal(result.cancelled, false);
|
|
216
|
-
assert.deepEqual(result.moduleSequence, ['db-prisma', '
|
|
225
|
+
assert.deepEqual(result.moduleSequence, ['db-prisma', 'accounts']);
|
|
217
226
|
assert.deepEqual(result.selectedProviders, {
|
|
218
227
|
'db-adapter': 'db-prisma',
|
|
219
|
-
'communications-runtime': 'communications',
|
|
220
228
|
});
|
|
221
229
|
} finally {
|
|
222
230
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
@@ -384,6 +392,7 @@ describe('module dependency helpers', () => {
|
|
|
384
392
|
'db-prisma',
|
|
385
393
|
'communications',
|
|
386
394
|
'accounts',
|
|
395
|
+
'accounts-communications',
|
|
387
396
|
'rbac',
|
|
388
397
|
'files-local',
|
|
389
398
|
'files',
|
|
@@ -396,8 +405,6 @@ describe('module dependency helpers', () => {
|
|
|
396
405
|
assert.deepEqual(result.selectedProviders, {
|
|
397
406
|
'files-storage-adapter': 'files-local',
|
|
398
407
|
'db-adapter': 'db-prisma',
|
|
399
|
-
'communications-runtime': 'communications',
|
|
400
|
-
'files-runtime': 'files',
|
|
401
408
|
'queue-runtime': 'queue',
|
|
402
409
|
});
|
|
403
410
|
assert.equal(result.rootModuleIds.includes('files-s3'), false);
|
|
@@ -448,6 +455,25 @@ describe('module dependency helpers', () => {
|
|
|
448
455
|
}
|
|
449
456
|
});
|
|
450
457
|
|
|
458
|
+
it('builds a concrete install plan for accounts-communications with required parent modules', async () => {
|
|
459
|
+
const targetRoot = mkTmp('forgeon-deps-accounts-comms-');
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const result = await resolveModuleInstallPlan({
|
|
463
|
+
moduleId: 'accounts-communications',
|
|
464
|
+
targetRoot,
|
|
465
|
+
presets: TEST_PRESETS,
|
|
466
|
+
withRequired: true,
|
|
467
|
+
isInteractive: false,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
assert.equal(result.cancelled, false);
|
|
471
|
+
assert.deepEqual(result.moduleSequence, ['db-prisma', 'accounts', 'communications', 'accounts-communications']);
|
|
472
|
+
} finally {
|
|
473
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
451
477
|
it('keeps the requested module in the plan even when it is already installed', async () => {
|
|
452
478
|
const targetRoot = mkTmp('forgeon-deps-reapply-');
|
|
453
479
|
|
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 { applyAccountsCommunicationsModule } from './accounts-communications.mjs';
|
|
14
15
|
import { applyCommunicationsModule } from './communications.mjs';
|
|
15
16
|
import { applyLoggerModule } from './logger.mjs';
|
|
16
17
|
import { applyRateLimitModule } from './rate-limit.mjs';
|
|
@@ -45,6 +46,7 @@ const MODULE_APPLIERS = {
|
|
|
45
46
|
'files-s3': applyFilesS3Module,
|
|
46
47
|
i18n: applyI18nModule,
|
|
47
48
|
accounts: applyAccountsModule,
|
|
49
|
+
'accounts-communications': applyAccountsCommunicationsModule,
|
|
48
50
|
communications: applyCommunicationsModule,
|
|
49
51
|
logger: applyLoggerModule,
|
|
50
52
|
queue: applyQueueModule,
|
|
@@ -793,16 +793,17 @@ function assertAccountsWiring(projectRoot) {
|
|
|
793
793
|
assert.match(compose, /JWT_REFRESH_SECRET: \$\{JWT_REFRESH_SECRET\}/);
|
|
794
794
|
assert.match(compose, /AUTH_ARGON2_MEMORY_COST: \$\{AUTH_ARGON2_MEMORY_COST\}/);
|
|
795
795
|
|
|
796
|
-
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
797
|
-
assert.match(readme, /## Accounts Module/);
|
|
798
|
-
assert.match(readme, /owner-scoped user routes/);
|
|
799
|
-
assert.match(readme, /
|
|
800
|
-
|
|
801
|
-
const authServiceSource = fs.readFileSync(
|
|
802
|
-
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.service.ts'),
|
|
803
|
-
'utf8',
|
|
804
|
-
);
|
|
805
|
-
assert.match(authServiceSource, /
|
|
796
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
797
|
+
assert.match(readme, /## Accounts Module/);
|
|
798
|
+
assert.match(readme, /owner-scoped user routes/);
|
|
799
|
+
assert.match(readme, /works without `communications`/);
|
|
800
|
+
|
|
801
|
+
const authServiceSource = fs.readFileSync(
|
|
802
|
+
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.service.ts'),
|
|
803
|
+
'utf8',
|
|
804
|
+
);
|
|
805
|
+
assert.match(authServiceSource, /RegisterRequest/);
|
|
806
|
+
assert.match(authServiceSource, /REGISTER_HANDLER/);
|
|
806
807
|
|
|
807
808
|
const authCoreSource = fs.readFileSync(
|
|
808
809
|
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth-core.service.ts'),
|
|
@@ -811,12 +812,12 @@ function assertAccountsWiring(projectRoot) {
|
|
|
811
812
|
assert.match(authCoreSource, /AuthStore/);
|
|
812
813
|
assert.doesNotMatch(authCoreSource, /ACCOUNTS_PERSISTENCE_PORT/);
|
|
813
814
|
|
|
814
|
-
const accountsApiPackage = fs.readFileSync(
|
|
815
|
-
path.join(projectRoot, 'packages', 'accounts-api', 'package.json'),
|
|
816
|
-
'utf8',
|
|
817
|
-
);
|
|
818
|
-
assert.match(accountsApiPackage, /@forgeon\/db-prisma/);
|
|
819
|
-
assert.
|
|
815
|
+
const accountsApiPackage = fs.readFileSync(
|
|
816
|
+
path.join(projectRoot, 'packages', 'accounts-api', 'package.json'),
|
|
817
|
+
'utf8',
|
|
818
|
+
);
|
|
819
|
+
assert.match(accountsApiPackage, /@forgeon\/db-prisma/);
|
|
820
|
+
assert.doesNotMatch(accountsApiPackage, /@forgeon\/communications/);
|
|
820
821
|
|
|
821
822
|
const authStoreSource = fs.readFileSync(
|
|
822
823
|
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.store.ts'),
|
|
@@ -2364,7 +2365,7 @@ describe('addModule', () => {
|
|
|
2364
2365
|
|
|
2365
2366
|
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
2366
2367
|
assert.match(readme, /POST \/api\/auth\/register/);
|
|
2367
|
-
assert.
|
|
2368
|
+
assert.doesNotMatch(readme, /POST \/api\/auth\/password-reset\/request/);
|
|
2368
2369
|
assert.match(readme, /\/api\/users\/:id\/settings/);
|
|
2369
2370
|
|
|
2370
2371
|
const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
|
package/src/modules/registry.mjs
CHANGED
|
@@ -158,10 +158,10 @@ const MODULE_PRESETS = {
|
|
|
158
158
|
category: 'auth-security',
|
|
159
159
|
implemented: true,
|
|
160
160
|
description:
|
|
161
|
-
'Accounts umbrella module with DB-backed users/auth runtime, argon2 passwords, JWT access+refresh rotation, owner-scoped self-service routes, and
|
|
161
|
+
'Accounts umbrella module with standalone DB-backed users/auth runtime, argon2 passwords, JWT access+refresh rotation, owner-scoped self-service routes, and handler-based extension seams.',
|
|
162
162
|
detectionPaths: ['packages/accounts-api/package.json'],
|
|
163
163
|
provides: ['accounts-runtime'],
|
|
164
|
-
requires: [{ type: 'capability', id: 'db-adapter' }
|
|
164
|
+
requires: [{ type: 'capability', id: 'db-adapter' }],
|
|
165
165
|
optionalIntegrations: [
|
|
166
166
|
{
|
|
167
167
|
id: 'accounts-rbac',
|
|
@@ -180,6 +180,22 @@ const MODULE_PRESETS = {
|
|
|
180
180
|
],
|
|
181
181
|
docFragments: ['00_title', '10_overview', '20_scope', '90_status_implemented'],
|
|
182
182
|
},
|
|
183
|
+
'accounts-communications': {
|
|
184
|
+
id: 'accounts-communications',
|
|
185
|
+
label: 'Accounts Communications',
|
|
186
|
+
category: 'auth-security',
|
|
187
|
+
implemented: true,
|
|
188
|
+
description:
|
|
189
|
+
'Optional bridge module that extends accounts with communications-backed registration verification, password reset, password change confirmation, and email change confirmation flows.',
|
|
190
|
+
detectionPaths: ['packages/accounts-communications/package.json'],
|
|
191
|
+
provides: ['accounts-communications-runtime'],
|
|
192
|
+
requires: [
|
|
193
|
+
{ type: 'module', id: 'accounts' },
|
|
194
|
+
{ type: 'module', id: 'communications' },
|
|
195
|
+
],
|
|
196
|
+
optionalIntegrations: [],
|
|
197
|
+
docFragments: ['00_title', '10_overview', '20_scope', '90_status_implemented'],
|
|
198
|
+
},
|
|
183
199
|
'rate-limit': {
|
|
184
200
|
id: 'rate-limit',
|
|
185
201
|
label: 'Rate Limit',
|
|
@@ -5,11 +5,10 @@ Implemented scope:
|
|
|
5
5
|
1. Public installer surface:
|
|
6
6
|
- single umbrella add-module: `accounts`
|
|
7
7
|
- requires `db-adapter`
|
|
8
|
-
- requires `communications-runtime`
|
|
9
8
|
2. Internal runtime split:
|
|
10
9
|
- `@forgeon/accounts-contracts`
|
|
11
10
|
- `@forgeon/accounts-api`
|
|
12
|
-
- users core, auth core, auth-jwt, auth-password
|
|
11
|
+
- users core, auth core, handlers, auth-jwt, auth-password
|
|
13
12
|
3. API runtime:
|
|
14
13
|
- `POST /api/auth/register`
|
|
15
14
|
- `POST /api/auth/login`
|
|
@@ -17,14 +16,14 @@ Implemented scope:
|
|
|
17
16
|
- `POST /api/auth/logout`
|
|
18
17
|
- `GET /api/auth/me`
|
|
19
18
|
- `POST /api/auth/change-password`
|
|
20
|
-
- stub endpoints for verify-email and password reset confirmation
|
|
21
19
|
4. Users surface:
|
|
22
20
|
- owner-scoped routes under `/api/users/:id`, `/api/users/:id/profile`, `/api/users/:id/settings`
|
|
23
21
|
- `/users/me` is resolved through the same owner-scoped route surface
|
|
24
22
|
5. Persistence and security:
|
|
25
|
-
- DB-backed `User`, `UserProfile`, `UserSettings`, `AuthIdentity`, `AuthCredential`, `AuthRefreshToken`
|
|
23
|
+
- DB-backed `User`, `UserProfile`, `UserSettings`, `AuthIdentity`, `AuthCredential`, `AuthRefreshToken`, `AuthPendingOperation`
|
|
26
24
|
- argon2 for password and refresh-token hashing
|
|
27
25
|
- refresh token rotation + revoke with per-token storage rows
|
|
26
|
+
- pending operation records for delayed confirmation and recovery flows
|
|
28
27
|
6. Module checks:
|
|
29
28
|
- API probe endpoint: `GET /api/health/auth`
|
|
30
|
-
- default web probe button + result block
|
|
29
|
+
- default web probe button + result block
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
Status: implemented.
|
|
4
4
|
|
|
5
5
|
Notes:
|
|
6
|
-
- `accounts` is a hard consumer of the `db-adapter`
|
|
6
|
+
- `accounts` is a hard consumer of the `db-adapter` capability only.
|
|
7
7
|
- The base accounts schema does not store RBAC roles or permissions.
|
|
8
|
-
-
|
|
8
|
+
- Delivery-assisted auth/account flows belong to the optional `accounts-communications` extension.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# {{MODULE_LABEL}}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## Scope
|
|
2
|
+
|
|
3
|
+
Implemented scope:
|
|
4
|
+
|
|
5
|
+
1. Public installer surface:
|
|
6
|
+
- single add-module: `accounts-communications`
|
|
7
|
+
- requires `accounts`
|
|
8
|
+
- requires `communications`
|
|
9
|
+
2. Runtime package:
|
|
10
|
+
- `@forgeon/accounts-communications`
|
|
11
|
+
3. Handler rebinding:
|
|
12
|
+
- pending-verification `register`
|
|
13
|
+
- confirmable `change-password`
|
|
14
|
+
4. Extension routes:
|
|
15
|
+
- `POST /api/auth/verify-email`
|
|
16
|
+
- `POST /api/auth/password-reset/request`
|
|
17
|
+
- `POST /api/auth/password-reset/confirm`
|
|
18
|
+
- `POST /api/auth/change-password/confirm`
|
|
19
|
+
- `POST /api/auth/change-email/request`
|
|
20
|
+
- `POST /api/auth/change-email/confirm`
|
|
21
|
+
5. Runtime boundaries:
|
|
22
|
+
- one `AuthCommunicationsController`
|
|
23
|
+
- one `AuthCommunicationsService`
|
|
24
|
+
- base account/auth state remains owned by `accounts`
|
|
@@ -6,6 +6,7 @@ ALTER TABLE "User"
|
|
|
6
6
|
DROP COLUMN IF EXISTS "email",
|
|
7
7
|
ADD COLUMN IF NOT EXISTS "status" TEXT NOT NULL DEFAULT 'active',
|
|
8
8
|
ADD COLUMN IF NOT EXISTS "data" JSONB,
|
|
9
|
+
ADD COLUMN IF NOT EXISTS "emailVerifiedAt" TIMESTAMP(3),
|
|
9
10
|
ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3),
|
|
10
11
|
ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
11
12
|
|
|
@@ -57,10 +58,24 @@ CREATE TABLE IF NOT EXISTS "AuthRefreshToken" (
|
|
|
57
58
|
CONSTRAINT "AuthRefreshToken_pkey" PRIMARY KEY ("id")
|
|
58
59
|
);
|
|
59
60
|
|
|
61
|
+
-- CreateTable
|
|
62
|
+
CREATE TABLE IF NOT EXISTS "AuthPendingOperation" (
|
|
63
|
+
"id" TEXT NOT NULL,
|
|
64
|
+
"userId" TEXT NOT NULL,
|
|
65
|
+
"type" TEXT NOT NULL,
|
|
66
|
+
"tokenHash" TEXT NOT NULL,
|
|
67
|
+
"metadata" JSONB,
|
|
68
|
+
"expiresAt" TIMESTAMP(3) NOT NULL,
|
|
69
|
+
"consumedAt" TIMESTAMP(3),
|
|
70
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
71
|
+
CONSTRAINT "AuthPendingOperation_pkey" PRIMARY KEY ("id")
|
|
72
|
+
);
|
|
73
|
+
|
|
60
74
|
-- Indexes
|
|
61
75
|
CREATE UNIQUE INDEX IF NOT EXISTS "AuthIdentity_provider_providerId_key" ON "AuthIdentity"("provider", "providerId");
|
|
62
76
|
CREATE UNIQUE INDEX IF NOT EXISTS "AuthCredential_userId_key" ON "AuthCredential"("userId");
|
|
63
77
|
CREATE INDEX IF NOT EXISTS "AuthRefreshToken_userId_createdAt_idx" ON "AuthRefreshToken"("userId", "createdAt");
|
|
78
|
+
CREATE INDEX IF NOT EXISTS "AuthPendingOperation_userId_type_createdAt_idx" ON "AuthPendingOperation"("userId", "type", "createdAt");
|
|
64
79
|
|
|
65
80
|
-- Foreign keys
|
|
66
81
|
DO $$
|
|
@@ -94,4 +109,10 @@ BEGIN
|
|
|
94
109
|
ADD CONSTRAINT "AuthRefreshToken_userId_fkey"
|
|
95
110
|
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
96
111
|
END IF;
|
|
97
|
-
|
|
112
|
+
|
|
113
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'AuthPendingOperation_userId_fkey') THEN
|
|
114
|
+
ALTER TABLE "AuthPendingOperation"
|
|
115
|
+
ADD CONSTRAINT "AuthPendingOperation_userId_fkey"
|
|
116
|
+
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
117
|
+
END IF;
|
|
118
|
+
END $$;
|