proteum 2.1.2 → 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 +3 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +8 -0
- package/cli/presentation/commands.ts +27 -1
- package/cli/runtime/commands.ts +28 -0
- package/client/dev/profiler/index.tsx +338 -163
- package/common/dev/requestTrace.ts +4 -0
- package/common/dev/session.ts +24 -0
- package/package.json +1 -1
- package/server/app/container/trace/index.ts +48 -0
- package/server/services/router/http/index.ts +86 -0
- package/server/services/router/response/index.ts +1 -0
|
@@ -82,8 +82,10 @@ export type TTraceCall = {
|
|
|
82
82
|
errorMessage?: string;
|
|
83
83
|
requestDataKeys: string[];
|
|
84
84
|
requestData?: TTraceSummaryValue;
|
|
85
|
+
requestDataJson?: unknown;
|
|
85
86
|
resultKeys: string[];
|
|
86
87
|
result?: TTraceSummaryValue;
|
|
88
|
+
resultJson?: unknown;
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
export type TRequestTrace = {
|
|
@@ -103,6 +105,8 @@ export type TRequestTrace = {
|
|
|
103
105
|
droppedEvents: number;
|
|
104
106
|
persistedFilepath?: string;
|
|
105
107
|
errorMessage?: string;
|
|
108
|
+
requestDataJson?: unknown;
|
|
109
|
+
resultJson?: unknown;
|
|
106
110
|
calls: TTraceCall[];
|
|
107
111
|
events: TTraceEvent[];
|
|
108
112
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type TDevSessionUserSummary = {
|
|
2
|
+
email: string;
|
|
3
|
+
name: string | null;
|
|
4
|
+
type: string;
|
|
5
|
+
roles: string[];
|
|
6
|
+
locale?: string | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type TDevSessionPayload = {
|
|
10
|
+
token: string;
|
|
11
|
+
cookieName: 'authorization';
|
|
12
|
+
expiresInMs: number;
|
|
13
|
+
issuedAt: string;
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type TDevSessionStartResponse = {
|
|
18
|
+
user: TDevSessionUserSummary;
|
|
19
|
+
session: TDevSessionPayload;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type TDevSessionErrorResponse = {
|
|
23
|
+
error: string;
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proteum",
|
|
3
3
|
"description": "LLM-first Opinionated Typescript Framework for web applications.",
|
|
4
|
-
"version": "2.1.
|
|
4
|
+
"version": "2.1.3-1",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/proteum.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -38,6 +38,44 @@ const isSensitiveKeyPath = (keyPath: string[]) => sensitiveKeyPattern.test(keyPa
|
|
|
38
38
|
const summarizeString = (value: string) =>
|
|
39
39
|
value.length <= maxStringLength ? value : `${value.slice(0, maxStringLength)}…`;
|
|
40
40
|
|
|
41
|
+
const serializeJsonValue = (value: unknown, keyPath: string[], seen: WeakSet<object>): unknown => {
|
|
42
|
+
if (isSensitiveKeyPath(keyPath)) return `[redacted: Sensitive key ${keyPath[keyPath.length - 1] || 'value'}]`;
|
|
43
|
+
if (value === undefined || value === null) return value;
|
|
44
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
45
|
+
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
46
|
+
if (typeof value === 'symbol') return value.toString();
|
|
47
|
+
if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`;
|
|
48
|
+
|
|
49
|
+
if (value instanceof Date) return value.toISOString();
|
|
50
|
+
if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack };
|
|
51
|
+
if (Buffer.isBuffer(value)) return `[Buffer ${value.byteLength} bytes]`;
|
|
52
|
+
if (value instanceof Map) return Array.from(value.entries()).map(([entryKey, entryValue], index) =>
|
|
53
|
+
serializeJsonValue([entryKey, entryValue], [...keyPath, `[${index}]`], seen),
|
|
54
|
+
);
|
|
55
|
+
if (value instanceof Set) {
|
|
56
|
+
return Array.from(value.values()).map((entryValue, index) => serializeJsonValue(entryValue, [...keyPath, `[${index}]`], seen));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof value !== 'object') return String(value);
|
|
60
|
+
if (seen.has(value)) return `[Circular ${value.constructor?.name || 'Object'}]`;
|
|
61
|
+
|
|
62
|
+
seen.add(value);
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
return value.map((item, index) => serializeJsonValue(item, [...keyPath, `[${index}]`], seen));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const serialized: Record<string, unknown> = {};
|
|
69
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
70
|
+
const nextValue = serializeJsonValue(entryValue, [...keyPath, entryKey], seen);
|
|
71
|
+
if (nextValue !== undefined) serialized[entryKey] = nextValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return serialized;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const serializeCaptureValue = (value: TTraceInspectable, key: string) => serializeJsonValue(value, [key], new WeakSet<object>());
|
|
78
|
+
|
|
41
79
|
const summarizeError = (error: Error): TTraceSummaryValue => ({
|
|
42
80
|
kind: 'error',
|
|
43
81
|
name: error.name,
|
|
@@ -169,6 +207,7 @@ export default class Trace {
|
|
|
169
207
|
profilerParentRequestId: input.profilerParentRequestId,
|
|
170
208
|
startedAt: nowIso(),
|
|
171
209
|
droppedEvents: 0,
|
|
210
|
+
requestDataJson: serializeCaptureValue(input.data, 'requestData'),
|
|
172
211
|
calls: [],
|
|
173
212
|
events: [],
|
|
174
213
|
};
|
|
@@ -269,6 +308,7 @@ export default class Trace {
|
|
|
269
308
|
startedAt: nowIso(),
|
|
270
309
|
requestDataKeys: input.requestDataKeys || [],
|
|
271
310
|
requestData: input.requestData !== undefined ? summarizeCaptureValue(input.requestData, trace.capture, 'requestData') : undefined,
|
|
311
|
+
requestDataJson: input.requestData !== undefined ? serializeCaptureValue(input.requestData, 'requestData') : undefined,
|
|
272
312
|
resultKeys: [],
|
|
273
313
|
};
|
|
274
314
|
|
|
@@ -298,6 +338,14 @@ export default class Trace {
|
|
|
298
338
|
call.errorMessage = output.errorMessage;
|
|
299
339
|
call.resultKeys = output.resultKeys || [];
|
|
300
340
|
call.result = output.result !== undefined ? summarizeCaptureValue(output.result, trace.capture, 'result') : undefined;
|
|
341
|
+
call.resultJson = output.result !== undefined ? serializeCaptureValue(output.result, 'result') : undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
public setRequestResult(requestId: string, result: TTraceInspectable) {
|
|
345
|
+
const trace = this.requests.get(requestId);
|
|
346
|
+
if (!trace) return;
|
|
347
|
+
|
|
348
|
+
trace.resultJson = serializeCaptureValue(result, 'result');
|
|
301
349
|
}
|
|
302
350
|
|
|
303
351
|
public listRequests(limit = 20): TRequestTraceListItem[] {
|
|
@@ -22,7 +22,9 @@ import cookieParser from 'cookie-parser';
|
|
|
22
22
|
import Container from '@server/app/container';
|
|
23
23
|
import type CronManager from '@server/services/cron';
|
|
24
24
|
import type CronTask from '@server/services/cron/CronTask';
|
|
25
|
+
import type { TBasicUser } from '@server/services/auth';
|
|
25
26
|
import type { TServerRouter } from '..';
|
|
27
|
+
import type { TDevSessionStartResponse, TDevSessionUserSummary } from '@common/dev/session';
|
|
26
28
|
import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
|
|
27
29
|
import { explainSectionNames } from '@common/dev/diagnostics';
|
|
28
30
|
|
|
@@ -54,6 +56,15 @@ export type Hooks = {};
|
|
|
54
56
|
|
|
55
57
|
type TContentSecurityPolicyOptions = NonNullable<Parameters<typeof helmet.contentSecurityPolicy>[0]>;
|
|
56
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
|
+
};
|
|
57
68
|
|
|
58
69
|
const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPolicyOptions => {
|
|
59
70
|
const directives: TContentSecurityPolicyDirectives = {
|
|
@@ -101,6 +112,32 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
101
112
|
this.app.on('cleanup', () => this.cleanup());
|
|
102
113
|
}
|
|
103
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
|
+
|
|
104
141
|
/*----------------------------------
|
|
105
142
|
- HOOKS
|
|
106
143
|
----------------------------------*/
|
|
@@ -358,6 +395,55 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
358
395
|
}
|
|
359
396
|
});
|
|
360
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
|
+
|
|
361
447
|
routes.post('/__proteum/cron/tasks/run', async (req, res) => {
|
|
362
448
|
const cron = this.getCronManager();
|
|
363
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();
|