proteum 2.1.1 → 2.1.3-1
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 +28 -6
- package/agents/framework/AGENTS.md +14 -1
- package/agents/project/AGENTS.md +3 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/dev.ts +2 -1
- package/cli/commands/init.ts +2 -94
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +8 -0
- package/cli/index.ts +1 -4
- package/cli/presentation/commands.ts +72 -10
- package/cli/presentation/devSession.ts +17 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +89 -3
- package/cli/scaffold/index.ts +720 -0
- package/cli/scaffold/templates.ts +344 -0
- package/cli/scaffold/types.ts +26 -0
- package/client/dev/profiler/index.tsx +1410 -235
- package/common/dev/profiler.ts +1 -0
- package/common/dev/requestTrace.ts +10 -0
- package/common/dev/session.ts +24 -0
- package/docs/dev-commands.md +7 -0
- package/docs/diagnostics.md +88 -0
- package/docs/request-tracing.md +10 -0
- package/eslint.js +11 -6
- package/package.json +3 -2
- package/server/app/container/trace/index.ts +48 -0
- package/server/app/index.ts +2 -2
- package/server/index.ts +0 -1
- package/server/services/auth/index.ts +525 -61
- package/server/services/auth/router/index.ts +106 -7
- package/server/services/router/http/index.ts +108 -6
- package/server/services/router/response/index.ts +1 -0
|
@@ -25,12 +25,6 @@ import UsersRequestService from './request';
|
|
|
25
25
|
- TYPES
|
|
26
26
|
----------------------------------*/
|
|
27
27
|
|
|
28
|
-
/*----------------------------------
|
|
29
|
-
- CONFIG
|
|
30
|
-
----------------------------------*/
|
|
31
|
-
|
|
32
|
-
const LogPrefix = '[router][auth]';
|
|
33
|
-
|
|
34
28
|
/*----------------------------------
|
|
35
29
|
- SERVICE
|
|
36
30
|
----------------------------------*/
|
|
@@ -59,11 +53,32 @@ export default class AuthenticationRouterService<
|
|
|
59
53
|
this.users = this.config.users;
|
|
60
54
|
}
|
|
61
55
|
|
|
56
|
+
private traceRouteAuth(
|
|
57
|
+
request: TRequest,
|
|
58
|
+
route: TAnyRoute,
|
|
59
|
+
details: Record<string, any>,
|
|
60
|
+
minimumCapture: 'summary' | 'resolve' | 'deep' = 'resolve',
|
|
61
|
+
) {
|
|
62
|
+
this.app.container.Trace.record(
|
|
63
|
+
request.id,
|
|
64
|
+
'auth.route',
|
|
65
|
+
{
|
|
66
|
+
routePath: route.path || '',
|
|
67
|
+
routeId: route.options.id || '',
|
|
68
|
+
authInput: route.options.auth ?? null,
|
|
69
|
+
tracking: route.options.authTracking ?? null,
|
|
70
|
+
redirectLogged: route.options.redirectLogged ?? null,
|
|
71
|
+
...details,
|
|
72
|
+
},
|
|
73
|
+
minimumCapture,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
public async ready() {
|
|
63
78
|
// Decode current user
|
|
64
79
|
this.parent.on('request', async (request: TRequest) => {
|
|
65
80
|
// TODO: Typings. (context.user ?)
|
|
66
|
-
const decoded = await this.users.decode(request.req, true);
|
|
81
|
+
const decoded = await this.users.decode(request.req, true, request.id);
|
|
67
82
|
|
|
68
83
|
request.user = decoded || null;
|
|
69
84
|
});
|
|
@@ -72,10 +87,48 @@ export default class AuthenticationRouterService<
|
|
|
72
87
|
this.parent.on('resolved', async (route: TAnyRoute, request: TRequest, response: ServerResponse<TRouter>) => {
|
|
73
88
|
if (route.options.auth !== undefined) {
|
|
74
89
|
const tracking = route.options.authTracking ?? null;
|
|
90
|
+
const strategy =
|
|
91
|
+
route.options.auth === false
|
|
92
|
+
? 'guest-only'
|
|
93
|
+
: route.options.auth === null
|
|
94
|
+
? 'authenticated'
|
|
95
|
+
: typeof route.options.auth === 'object'
|
|
96
|
+
? 'conditions'
|
|
97
|
+
: route.options.auth === true
|
|
98
|
+
? tracking !== null && this.users.config.rules
|
|
99
|
+
? 'user-via-rules'
|
|
100
|
+
: 'user'
|
|
101
|
+
: tracking !== null && this.users.config.rules
|
|
102
|
+
? 'role-via-rules'
|
|
103
|
+
: 'role';
|
|
104
|
+
|
|
105
|
+
this.traceRouteAuth(
|
|
106
|
+
request,
|
|
107
|
+
route,
|
|
108
|
+
{
|
|
109
|
+
phase: 'start',
|
|
110
|
+
strategy,
|
|
111
|
+
},
|
|
112
|
+
'resolve',
|
|
113
|
+
);
|
|
75
114
|
|
|
76
115
|
// Guest-only routes can still redirect authenticated users away.
|
|
77
116
|
if (route.options.auth === false) {
|
|
78
117
|
const currentUser = this.users.check(request, false, tracking);
|
|
118
|
+
const redirected = Boolean(route.options.redirectLogged && currentUser);
|
|
119
|
+
|
|
120
|
+
this.traceRouteAuth(
|
|
121
|
+
request,
|
|
122
|
+
route,
|
|
123
|
+
{
|
|
124
|
+
phase: 'result',
|
|
125
|
+
strategy,
|
|
126
|
+
outcome: redirected ? 'redirected' : 'allowed',
|
|
127
|
+
userPresent: currentUser !== null,
|
|
128
|
+
redirectTo: redirected ? route.options.redirectLogged : null,
|
|
129
|
+
},
|
|
130
|
+
'resolve',
|
|
131
|
+
);
|
|
79
132
|
|
|
80
133
|
if (route.options.redirectLogged && currentUser) response.redirect(route.options.redirectLogged);
|
|
81
134
|
return;
|
|
@@ -83,11 +136,13 @@ export default class AuthenticationRouterService<
|
|
|
83
136
|
|
|
84
137
|
if (route.options.auth === null) {
|
|
85
138
|
this.users.check(request, null, tracking);
|
|
139
|
+
this.traceRouteAuth(request, route, { phase: 'result', strategy, outcome: 'allowed' }, 'resolve');
|
|
86
140
|
return;
|
|
87
141
|
}
|
|
88
142
|
|
|
89
143
|
if (typeof route.options.auth === 'object') {
|
|
90
144
|
this.users.check(request, route.options.auth as TAuthCheckConditions, tracking);
|
|
145
|
+
this.traceRouteAuth(request, route, { phase: 'result', strategy, outcome: 'allowed' }, 'resolve');
|
|
91
146
|
return;
|
|
92
147
|
}
|
|
93
148
|
|
|
@@ -95,10 +150,32 @@ export default class AuthenticationRouterService<
|
|
|
95
150
|
if (route.options.auth === true) {
|
|
96
151
|
if (tracking !== null && this.users.config.rules) {
|
|
97
152
|
this.users.check(request, { role: 'USER' }, tracking);
|
|
153
|
+
this.traceRouteAuth(
|
|
154
|
+
request,
|
|
155
|
+
route,
|
|
156
|
+
{
|
|
157
|
+
phase: 'result',
|
|
158
|
+
strategy,
|
|
159
|
+
outcome: 'allowed',
|
|
160
|
+
requiredRole: 'USER',
|
|
161
|
+
},
|
|
162
|
+
'resolve',
|
|
163
|
+
);
|
|
98
164
|
return;
|
|
99
165
|
}
|
|
100
166
|
|
|
101
167
|
this.users.check(request, true);
|
|
168
|
+
this.traceRouteAuth(
|
|
169
|
+
request,
|
|
170
|
+
route,
|
|
171
|
+
{
|
|
172
|
+
phase: 'result',
|
|
173
|
+
strategy,
|
|
174
|
+
outcome: 'allowed',
|
|
175
|
+
requiredRole: 'USER',
|
|
176
|
+
},
|
|
177
|
+
'resolve',
|
|
178
|
+
);
|
|
102
179
|
return;
|
|
103
180
|
}
|
|
104
181
|
|
|
@@ -106,10 +183,32 @@ export default class AuthenticationRouterService<
|
|
|
106
183
|
|
|
107
184
|
if (tracking !== null && this.users.config.rules) {
|
|
108
185
|
this.users.check(request, { role: requiredRole }, tracking);
|
|
186
|
+
this.traceRouteAuth(
|
|
187
|
+
request,
|
|
188
|
+
route,
|
|
189
|
+
{
|
|
190
|
+
phase: 'result',
|
|
191
|
+
strategy,
|
|
192
|
+
outcome: 'allowed',
|
|
193
|
+
requiredRole,
|
|
194
|
+
},
|
|
195
|
+
'resolve',
|
|
196
|
+
);
|
|
109
197
|
return;
|
|
110
198
|
}
|
|
111
199
|
|
|
112
200
|
this.users.check(request, requiredRole);
|
|
201
|
+
this.traceRouteAuth(
|
|
202
|
+
request,
|
|
203
|
+
route,
|
|
204
|
+
{
|
|
205
|
+
phase: 'result',
|
|
206
|
+
strategy,
|
|
207
|
+
outcome: 'allowed',
|
|
208
|
+
requiredRole,
|
|
209
|
+
},
|
|
210
|
+
'resolve',
|
|
211
|
+
);
|
|
113
212
|
}
|
|
114
213
|
});
|
|
115
214
|
}
|
|
@@ -17,13 +17,14 @@ import helmet from 'helmet'; // Diverses protections
|
|
|
17
17
|
import compression from 'compression';
|
|
18
18
|
import fileUpload from 'express-fileupload';
|
|
19
19
|
import cookieParser from 'cookie-parser';
|
|
20
|
-
import * as csp from 'express-csp-header';
|
|
21
20
|
|
|
22
21
|
// Core
|
|
23
22
|
import Container from '@server/app/container';
|
|
24
23
|
import type CronManager from '@server/services/cron';
|
|
25
24
|
import type CronTask from '@server/services/cron/CronTask';
|
|
25
|
+
import type { TBasicUser } from '@server/services/auth';
|
|
26
26
|
import type { TServerRouter } from '..';
|
|
27
|
+
import type { TDevSessionStartResponse, TDevSessionUserSummary } from '@common/dev/session';
|
|
27
28
|
import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
|
|
28
29
|
import { explainSectionNames } from '@common/dev/diagnostics';
|
|
29
30
|
|
|
@@ -53,6 +54,36 @@ export type Config = {
|
|
|
53
54
|
|
|
54
55
|
export type Hooks = {};
|
|
55
56
|
|
|
57
|
+
type TContentSecurityPolicyOptions = NonNullable<Parameters<typeof helmet.contentSecurityPolicy>[0]>;
|
|
58
|
+
type TContentSecurityPolicyDirectives = NonNullable<TContentSecurityPolicyOptions['directives']>;
|
|
59
|
+
type TDevSessionAuthService = {
|
|
60
|
+
config: {
|
|
61
|
+
jwt: {
|
|
62
|
+
expiration: number;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
createSession: (session: { email: string }, request: { id: string; res: express.Response }) => string;
|
|
66
|
+
decodeSession: (session: { email: string }, req: express.Request) => Promise<TBasicUser | null>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPolicyOptions => {
|
|
70
|
+
const directives: TContentSecurityPolicyDirectives = {
|
|
71
|
+
defaultSrc:
|
|
72
|
+
config.default && config.default.length > 0
|
|
73
|
+
? [...config.default]
|
|
74
|
+
: helmet.contentSecurityPolicy.dangerouslyDisableDefaultSrc,
|
|
75
|
+
scriptSrc: ["'unsafe-inline'", "'self'", "'unsafe-eval'", ...config.scripts],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (config.styles && config.styles.length > 0) directives.styleSrc = [...config.styles];
|
|
79
|
+
if (config.images && config.images.length > 0) directives.imgSrc = [...config.images];
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
useDefaults: false,
|
|
83
|
+
directives,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
56
87
|
/*----------------------------------
|
|
57
88
|
- FUNCTION
|
|
58
89
|
----------------------------------*/
|
|
@@ -81,6 +112,32 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
81
112
|
this.app.on('cleanup', () => this.cleanup());
|
|
82
113
|
}
|
|
83
114
|
|
|
115
|
+
private resolveDevSessionAuthService(): TDevSessionAuthService {
|
|
116
|
+
const plugins = Object.values(this.router.config.plugins || {}) as Array<{ users?: unknown }>;
|
|
117
|
+
|
|
118
|
+
for (const plugin of plugins) {
|
|
119
|
+
const users = plugin?.users as Partial<TDevSessionAuthService> | undefined;
|
|
120
|
+
if (!users) continue;
|
|
121
|
+
if (typeof users.createSession !== 'function') continue;
|
|
122
|
+
if (typeof users.decodeSession !== 'function') continue;
|
|
123
|
+
if (typeof users.config?.jwt?.expiration !== 'number') continue;
|
|
124
|
+
|
|
125
|
+
return users as TDevSessionAuthService;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw new Error('No auth router plugin with a compatible users service is registered.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private summarizeDevSessionUser(user: TBasicUser): TDevSessionUserSummary {
|
|
132
|
+
return {
|
|
133
|
+
email: user.email,
|
|
134
|
+
name: user.name,
|
|
135
|
+
type: user.type,
|
|
136
|
+
roles: [...user.roles],
|
|
137
|
+
locale: user.locale ?? null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
84
141
|
/*----------------------------------
|
|
85
142
|
- HOOKS
|
|
86
143
|
----------------------------------*/
|
|
@@ -203,11 +260,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
203
260
|
|
|
204
261
|
if (this.config.cors !== undefined) routes.use(cors(this.config.cors));
|
|
205
262
|
|
|
206
|
-
routes.use(
|
|
207
|
-
csp.expressCspHeader({
|
|
208
|
-
directives: { 'script-src': [csp.INLINE, csp.SELF, csp.UNSAFE_EVAL, ...this.config.csp.scripts] },
|
|
209
|
-
}),
|
|
210
|
-
);
|
|
263
|
+
routes.use(helmet.contentSecurityPolicy(createContentSecurityPolicy(this.config.csp)));
|
|
211
264
|
|
|
212
265
|
this.registerDevTraceRoutes(routes);
|
|
213
266
|
routes.use(routeRequest);
|
|
@@ -342,6 +395,55 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
342
395
|
}
|
|
343
396
|
});
|
|
344
397
|
|
|
398
|
+
routes.post('/__proteum/session/start', async (req, res) => {
|
|
399
|
+
const email = typeof req.body?.email === 'string' ? req.body.email.trim() : '';
|
|
400
|
+
const requiredRole = typeof req.body?.role === 'string' ? req.body.role.trim() : '';
|
|
401
|
+
|
|
402
|
+
if (!email) {
|
|
403
|
+
res.status(400).json({ error: 'Email is required.' });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const auth = this.resolveDevSessionAuthService();
|
|
409
|
+
const user = await auth.decodeSession({ email }, req);
|
|
410
|
+
|
|
411
|
+
if (!user) {
|
|
412
|
+
res.status(404).json({ error: `No user could be resolved for "${email}".` });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (requiredRole && !user.roles.includes(requiredRole)) {
|
|
417
|
+
res.status(403).json({ error: `User "${email}" does not have required role "${requiredRole}".` });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const token = auth.createSession(
|
|
422
|
+
{ email },
|
|
423
|
+
{
|
|
424
|
+
id: `proteum-session:${Date.now()}`,
|
|
425
|
+
res,
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
const issuedAt = new Date().toISOString();
|
|
429
|
+
const expiresAt = new Date(Date.now() + auth.config.jwt.expiration).toISOString();
|
|
430
|
+
const response: TDevSessionStartResponse = {
|
|
431
|
+
user: this.summarizeDevSessionUser(user),
|
|
432
|
+
session: {
|
|
433
|
+
token,
|
|
434
|
+
cookieName: 'authorization',
|
|
435
|
+
expiresInMs: auth.config.jwt.expiration,
|
|
436
|
+
issuedAt,
|
|
437
|
+
expiresAt,
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
res.json(response);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
345
447
|
routes.post('/__proteum/cron/tasks/run', async (req, res) => {
|
|
346
448
|
const cron = this.getCronManager();
|
|
347
449
|
if (!cron) {
|
|
@@ -338,6 +338,7 @@ export default class ServerResponse<
|
|
|
338
338
|
// NOTE: On évite le filtrage sans masque spécifié (performances + risques erreurs)
|
|
339
339
|
if (mask !== undefined) data = await jsonMask(data, mask);
|
|
340
340
|
|
|
341
|
+
this.app.container.Trace.setRequestResult(this.request.id, data);
|
|
341
342
|
this.headers['Content-Type'] = 'application/json';
|
|
342
343
|
this.data = (this.request.isVirtual ? data : JSON.stringify(data)) as TData;
|
|
343
344
|
return this.end();
|