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.
@@ -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.2",
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();