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
package/README.md
CHANGED
|
@@ -289,6 +289,7 @@ Proteum ships with a compact CLI focused on the real app lifecycle:
|
|
|
289
289
|
| `proteum explain` | Explain routes, controllers, services, layouts, conventions, and env |
|
|
290
290
|
| `proteum trace` | Inspect live dev-only request traces from the running SSR server |
|
|
291
291
|
| `proteum command` | Run a dev-only internal command locally or against a running dev server |
|
|
292
|
+
| `proteum session` | Mint a dev-only auth session token and Playwright-ready cookie payload |
|
|
292
293
|
| `proteum init` | Scaffold a new Proteum app with built-in deterministic templates |
|
|
293
294
|
| `proteum create` | Scaffold a page, controller, command, route, or root service inside an app |
|
|
294
295
|
|
|
@@ -311,6 +312,8 @@ proteum explain --routes --controllers --commands
|
|
|
311
312
|
proteum explain --all --json
|
|
312
313
|
proteum command proteum/diagnostics/ping
|
|
313
314
|
proteum command proteum/diagnostics/ping --port 3101
|
|
315
|
+
proteum session admin@example.com --role ADMIN --port 3101
|
|
316
|
+
proteum session god@example.com --role GOD --json
|
|
314
317
|
proteum trace requests
|
|
315
318
|
proteum trace arm --capture deep
|
|
316
319
|
proteum trace latest
|
package/cli/commands/command.ts
CHANGED
|
@@ -61,6 +61,9 @@ const getCommandErrorMessage = (body: TDevCommandErrorResponse | object | string
|
|
|
61
61
|
return `Command request failed with status ${statusCode}.`;
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
const hasStructuredCommandError = (body: TDevCommandErrorResponse | object | string | undefined): body is TDevCommandErrorResponse =>
|
|
65
|
+
typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string';
|
|
66
|
+
|
|
64
67
|
const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GET' | 'POST'; json?: object }) => {
|
|
65
68
|
const attempts: string[] = [];
|
|
66
69
|
|
|
@@ -75,6 +78,11 @@ const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GE
|
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
if (response.statusCode >= 400) {
|
|
81
|
+
if (response.statusCode === 404 && !hasStructuredCommandError(response.body as TDevCommandErrorResponse | object | string | undefined)) {
|
|
82
|
+
attempts.push(`${baseUrl}${pathname}: returned 404`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
throw new UsageError(
|
|
79
87
|
getCommandErrorMessage(response.body as TDevCommandErrorResponse | object | string | undefined, response.statusCode),
|
|
80
88
|
);
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import got from 'got';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { UsageError } from 'clipanion';
|
|
5
|
+
|
|
6
|
+
import cli from '..';
|
|
7
|
+
import type { TDevSessionErrorResponse, TDevSessionStartResponse } from '../../common/dev/session';
|
|
8
|
+
|
|
9
|
+
const localSessionResultMarker = '__PROTEUM_SESSION_RESULT__';
|
|
10
|
+
|
|
11
|
+
type TResolvedSessionOutput = {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
user: TDevSessionStartResponse['user'];
|
|
14
|
+
session: TDevSessionStartResponse['session'];
|
|
15
|
+
browserCookie: string;
|
|
16
|
+
curlCookieHeader: string;
|
|
17
|
+
playwright: {
|
|
18
|
+
cookies: Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
url: string;
|
|
22
|
+
expires: number;
|
|
23
|
+
httpOnly: boolean;
|
|
24
|
+
secure: boolean;
|
|
25
|
+
sameSite: 'Lax';
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
|
|
31
|
+
|
|
32
|
+
const getRouterPortFromManifest = () => {
|
|
33
|
+
const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
|
|
34
|
+
if (!require('fs-extra').existsSync(manifestFilepath)) return undefined;
|
|
35
|
+
|
|
36
|
+
const manifest = require('fs-extra').readJsonSync(manifestFilepath, { throws: false }) as
|
|
37
|
+
| { env?: { resolved?: { routerPort?: number } } }
|
|
38
|
+
| undefined;
|
|
39
|
+
const port = manifest?.env?.resolved?.routerPort;
|
|
40
|
+
|
|
41
|
+
if (typeof port !== 'number' || port <= 0) return undefined;
|
|
42
|
+
|
|
43
|
+
return String(port);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getRouterPort = () => {
|
|
47
|
+
const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
|
|
48
|
+
if (overridePort) return overridePort;
|
|
49
|
+
|
|
50
|
+
const envPort = process.env.PORT?.trim();
|
|
51
|
+
if (envPort) return envPort;
|
|
52
|
+
|
|
53
|
+
const manifestPort = getRouterPortFromManifest();
|
|
54
|
+
if (manifestPort) return manifestPort;
|
|
55
|
+
|
|
56
|
+
throw new UsageError(
|
|
57
|
+
`Could not determine the router port from PORT or .proteum/manifest.json in ${cli.args.workdir as string}. Pass --port or --url explicitly.`,
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getRouterBaseUrls = () => {
|
|
62
|
+
const explicitUrl = typeof cli.args.url === 'string' && cli.args.url ? cli.args.url.trim() : '';
|
|
63
|
+
if (explicitUrl) return [normalizeBaseUrl(explicitUrl)];
|
|
64
|
+
|
|
65
|
+
const port = getRouterPort();
|
|
66
|
+
return [...new Set([`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`])];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getSessionErrorMessage = (body: TDevSessionErrorResponse | object | string | undefined, statusCode: number) => {
|
|
70
|
+
if (typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string') {
|
|
71
|
+
return body.error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return `Session request failed with status ${statusCode}.`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const hasStructuredSessionError = (body: TDevSessionErrorResponse | object | string | undefined): body is TDevSessionErrorResponse =>
|
|
78
|
+
typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string';
|
|
79
|
+
|
|
80
|
+
const requestSession = async (email: string, role: string) => {
|
|
81
|
+
const attempts: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const baseUrl of getRouterBaseUrls()) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await got(`${baseUrl}/__proteum/session/start`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
json: role ? { email, role } : { email },
|
|
88
|
+
responseType: 'json',
|
|
89
|
+
throwHttpErrors: false,
|
|
90
|
+
retry: { limit: 0 },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (response.statusCode >= 400) {
|
|
94
|
+
if (response.statusCode === 404 && !hasStructuredSessionError(response.body as TDevSessionErrorResponse | object | string | undefined)) {
|
|
95
|
+
attempts.push(`${baseUrl}/__proteum/session/start: returned 404`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new UsageError(
|
|
100
|
+
getSessionErrorMessage(response.body as TDevSessionErrorResponse | object | string | undefined, response.statusCode),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
baseUrl,
|
|
106
|
+
response: response.body as TDevSessionStartResponse,
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof UsageError) throw error;
|
|
110
|
+
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
attempts.push(`${baseUrl}/__proteum/session/start: ${message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new UsageError(
|
|
117
|
+
[
|
|
118
|
+
'Could not reach the Proteum session server.',
|
|
119
|
+
...attempts.map((attempt) => `- ${attempt}`),
|
|
120
|
+
'Make sure the app is running with `proteum dev`, or omit --port/--url to run the session request locally.',
|
|
121
|
+
].join('\n'),
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const buildSessionOutput = ({
|
|
126
|
+
baseUrl,
|
|
127
|
+
response,
|
|
128
|
+
}: {
|
|
129
|
+
baseUrl: string;
|
|
130
|
+
response: TDevSessionStartResponse;
|
|
131
|
+
}): TResolvedSessionOutput => {
|
|
132
|
+
const expires = Math.floor(Date.parse(response.session.expiresAt) / 1000);
|
|
133
|
+
const secure = new URL(baseUrl).protocol === 'https:';
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
baseUrl,
|
|
137
|
+
user: response.user,
|
|
138
|
+
session: response.session,
|
|
139
|
+
browserCookie: `${response.session.cookieName}=${response.session.token}; Path=/`,
|
|
140
|
+
curlCookieHeader: `Cookie: ${response.session.cookieName}=${response.session.token}`,
|
|
141
|
+
playwright: {
|
|
142
|
+
cookies: [
|
|
143
|
+
{
|
|
144
|
+
name: response.session.cookieName,
|
|
145
|
+
value: response.session.token,
|
|
146
|
+
url: baseUrl,
|
|
147
|
+
expires,
|
|
148
|
+
httpOnly: false,
|
|
149
|
+
secure,
|
|
150
|
+
sameSite: 'Lax',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const printJson = (value: object) => {
|
|
158
|
+
console.log(JSON.stringify(value, null, 2));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const renderSession = (value: TResolvedSessionOutput) =>
|
|
162
|
+
[
|
|
163
|
+
`Session ${value.user.email}`,
|
|
164
|
+
`- baseUrl=${value.baseUrl}`,
|
|
165
|
+
`- roles=${value.user.roles.join(',')}`,
|
|
166
|
+
`- expiresAt=${value.session.expiresAt}`,
|
|
167
|
+
'Token',
|
|
168
|
+
value.session.token,
|
|
169
|
+
'Playwright',
|
|
170
|
+
JSON.stringify(value.playwright, null, 2),
|
|
171
|
+
'Browser Cookie',
|
|
172
|
+
value.browserCookie,
|
|
173
|
+
].join('\n');
|
|
174
|
+
|
|
175
|
+
const runLocalSession = async (email: string, role: string) => {
|
|
176
|
+
const runnerFilepath = path.join(cli.paths.core.root, 'cli', 'commands', 'sessionLocalRunner.js');
|
|
177
|
+
|
|
178
|
+
return await new Promise<{ baseUrl: string; response: TDevSessionStartResponse }>((resolve, reject) => {
|
|
179
|
+
const stdoutChunks: Buffer[] = [];
|
|
180
|
+
const stderrChunks: Buffer[] = [];
|
|
181
|
+
const child = spawn(process.execPath, [runnerFilepath, cli.args.workdir as string, email, role], {
|
|
182
|
+
cwd: cli.args.workdir as string,
|
|
183
|
+
env: { ...process.env },
|
|
184
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
child.stdout.on('data', (chunk: Buffer | string) => {
|
|
188
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
child.stderr.on('data', (chunk: Buffer | string) => {
|
|
192
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
child.on('error', (error) => reject(error));
|
|
196
|
+
child.on('close', () => {
|
|
197
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
198
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
199
|
+
const markerLine = stdout
|
|
200
|
+
.split(/\r?\n/)
|
|
201
|
+
.find((line) => line.startsWith(localSessionResultMarker));
|
|
202
|
+
|
|
203
|
+
if (stderr.trim()) {
|
|
204
|
+
process.stderr.write(stderr.endsWith('\n') ? stderr : `${stderr}\n`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!markerLine) {
|
|
208
|
+
reject(
|
|
209
|
+
new Error(
|
|
210
|
+
['Local session runner exited without returning a structured result.', stdout.trim() ? `stdout:\n${stdout.trim()}` : undefined]
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join('\n\n'),
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const payload = JSON.parse(markerLine.slice(localSessionResultMarker.length)) as
|
|
219
|
+
| { session: { baseUrl: string; response: TDevSessionStartResponse } }
|
|
220
|
+
| { error: string };
|
|
221
|
+
|
|
222
|
+
if ('session' in payload) {
|
|
223
|
+
resolve(payload.session);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
reject(new Error(payload.error || 'Session runner failed.'));
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const run = async () => {
|
|
233
|
+
const email = typeof cli.args.email === 'string' ? cli.args.email.trim() : '';
|
|
234
|
+
const role = typeof cli.args.role === 'string' ? cli.args.role.trim() : '';
|
|
235
|
+
const shouldPrintJson = cli.args.json === true;
|
|
236
|
+
const shouldUseRemoteServer =
|
|
237
|
+
(typeof cli.args.port === 'string' && cli.args.port.length > 0) ||
|
|
238
|
+
(typeof cli.args.url === 'string' && cli.args.url.length > 0);
|
|
239
|
+
|
|
240
|
+
if (!email) {
|
|
241
|
+
throw new UsageError('An email is required. Example: proteum session admin@example.com --role ADMIN');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const resolved = buildSessionOutput(
|
|
245
|
+
shouldUseRemoteServer ? await requestSession(email, role) : await runLocalSession(email, role),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (shouldPrintJson) {
|
|
249
|
+
printJson(resolved);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(renderSession(resolved));
|
|
254
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const tsNode = require('ts-node');
|
|
5
|
+
|
|
6
|
+
const resultMarker = '__PROTEUM_SESSION_RESULT__';
|
|
7
|
+
|
|
8
|
+
const printPayload = (payload) => {
|
|
9
|
+
process.stdout.write(`${resultMarker}${JSON.stringify(payload)}\n`);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const fail = (error) => {
|
|
13
|
+
printPayload({ error: error instanceof Error ? error.message : String(error) });
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getAvailablePort = async () =>
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
const server = net.createServer();
|
|
20
|
+
|
|
21
|
+
server.once('error', reject);
|
|
22
|
+
server.listen(0, '127.0.0.1', () => {
|
|
23
|
+
const address = server.address();
|
|
24
|
+
|
|
25
|
+
if (!address || typeof address === 'string') {
|
|
26
|
+
server.close(() => reject(new Error('Could not determine a local port for the session runner.')));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
server.close((error) => {
|
|
31
|
+
if (error) {
|
|
32
|
+
reject(error);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resolve(address.port);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const closeMultiCompiler = async (multiCompiler) =>
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
multiCompiler.close((error) => {
|
|
44
|
+
if (error) {
|
|
45
|
+
reject(error);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const runCompiler = async (compiler) => {
|
|
54
|
+
const multiCompiler = await compiler.create();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
multiCompiler.run((error, stats) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
reject(error);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (stats && stats.hasErrors()) {
|
|
65
|
+
reject(new Error('Compilation failed for the local dev session runner.'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
compiler.dispose();
|
|
74
|
+
await closeMultiCompiler(multiCompiler);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const waitForServerReady = async (child) =>
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
let settled = false;
|
|
81
|
+
|
|
82
|
+
const finish = (callback, value) => {
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true;
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
child.off('message', onMessage);
|
|
87
|
+
child.off('error', onError);
|
|
88
|
+
child.off('exit', onExit);
|
|
89
|
+
callback(value);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onMessage = (message) => {
|
|
93
|
+
if (!message || message.type !== 'proteum:server-ready' || typeof message.publicUrl !== 'string') return;
|
|
94
|
+
finish(resolve, message.publicUrl);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const onError = (error) => finish(reject, error);
|
|
98
|
+
const onExit = (code, signal) =>
|
|
99
|
+
finish(reject, new Error(`Local session server exited before becoming ready (code=${code}, signal=${signal}).`));
|
|
100
|
+
const timeout = setTimeout(
|
|
101
|
+
() => finish(reject, new Error('Timed out while waiting for the local session server to become ready.')),
|
|
102
|
+
30000,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
child.on('message', onMessage);
|
|
106
|
+
child.once('error', onError);
|
|
107
|
+
child.once('exit', onExit);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const stopServerProcess = async (child) => {
|
|
111
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
112
|
+
|
|
113
|
+
child.kill('SIGTERM');
|
|
114
|
+
|
|
115
|
+
await new Promise((resolve) => {
|
|
116
|
+
const timeout = setTimeout(() => {
|
|
117
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
118
|
+
}, 5000);
|
|
119
|
+
|
|
120
|
+
child.once('exit', () => {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const requestSession = async (baseUrl, email, role) => {
|
|
128
|
+
const response = await fetch(`${baseUrl}/__proteum/session/start`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'content-type': 'application/json' },
|
|
131
|
+
body: JSON.stringify(role ? { email, role } : { email }),
|
|
132
|
+
});
|
|
133
|
+
const body = await response.json();
|
|
134
|
+
|
|
135
|
+
if (response.status >= 400) {
|
|
136
|
+
throw new Error((body && body.error) || `Session request failed with status ${response.status}.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { baseUrl, response: body };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
(async () => {
|
|
143
|
+
const [, , appRootArg = '', email = '', role = ''] = process.argv;
|
|
144
|
+
const appRoot = path.resolve(appRootArg);
|
|
145
|
+
|
|
146
|
+
if (!appRootArg || !email) {
|
|
147
|
+
fail(new Error('sessionLocalRunner requires <appRoot> and <email>.'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
process.chdir(appRoot);
|
|
152
|
+
|
|
153
|
+
tsNode.register({
|
|
154
|
+
transpileOnly: true,
|
|
155
|
+
project: path.join(__dirname, '..', 'tsconfig.json'),
|
|
156
|
+
files: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const port = await getAvailablePort();
|
|
160
|
+
const cli = require('../context.ts').default;
|
|
161
|
+
cli.setArgs({ workdir: appRoot, port: String(port), url: '', json: true });
|
|
162
|
+
|
|
163
|
+
const app = require('../app/index.ts').default;
|
|
164
|
+
const Compiler = require('../compiler/index.ts').default;
|
|
165
|
+
|
|
166
|
+
if (app.env.profile !== 'dev') {
|
|
167
|
+
fail(new Error(`Proteum sessions are only available when ENV_PROFILE=dev. Current profile: ${app.env.profile}.`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const compiler = new Compiler('dev');
|
|
172
|
+
await runCompiler(compiler);
|
|
173
|
+
|
|
174
|
+
const serverProcess = spawn(process.execPath, ['--preserve-symlinks', path.join(app.outputPath('dev'), 'server.js')], {
|
|
175
|
+
cwd: app.paths.root,
|
|
176
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const baseUrl = await waitForServerReady(serverProcess);
|
|
181
|
+
const session = await requestSession(baseUrl, email, role);
|
|
182
|
+
printPayload({ session });
|
|
183
|
+
} finally {
|
|
184
|
+
await stopServerProcess(serverProcess);
|
|
185
|
+
}
|
|
186
|
+
})().catch((error) => {
|
|
187
|
+
fail(error);
|
|
188
|
+
});
|
package/cli/commands/trace.ts
CHANGED
|
@@ -75,6 +75,9 @@ const getTraceErrorMessage = (body: TRequestTraceErrorResponse | object | string
|
|
|
75
75
|
return `Trace request failed with status ${statusCode}.`;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
const hasStructuredTraceError = (body: TRequestTraceErrorResponse | object | string | undefined): body is TRequestTraceErrorResponse =>
|
|
79
|
+
typeof body === 'object' && body !== null && 'error' in body && typeof body.error === 'string';
|
|
80
|
+
|
|
78
81
|
const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GET' | 'POST'; json?: object }) => {
|
|
79
82
|
const attempts: string[] = [];
|
|
80
83
|
|
|
@@ -89,6 +92,11 @@ const requestJson = async <TResponse>(pathname: string, options?: { method?: 'GE
|
|
|
89
92
|
});
|
|
90
93
|
|
|
91
94
|
if (response.statusCode >= 400) {
|
|
95
|
+
if (response.statusCode === 404 && !hasStructuredTraceError(response.body as TRequestTraceErrorResponse | object | string | undefined)) {
|
|
96
|
+
attempts.push(`${baseUrl}${pathname}: returned 404`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
throw new TraceResponseError(
|
|
93
101
|
getTraceErrorMessage(response.body as TRequestTraceErrorResponse | object | string | undefined, response.statusCode),
|
|
94
102
|
);
|
|
@@ -16,6 +16,7 @@ export const proteumCommandNames = [
|
|
|
16
16
|
'explain',
|
|
17
17
|
'trace',
|
|
18
18
|
'command',
|
|
19
|
+
'session',
|
|
19
20
|
] as const;
|
|
20
21
|
|
|
21
22
|
export type TProteumCommandName = (typeof proteumCommandNames)[number];
|
|
@@ -48,7 +49,7 @@ export const proteumRecommendedFlow: TRow[] = [
|
|
|
48
49
|
export const proteumCommandGroups: Array<{ title: string; names: TProteumCommandName[] }> = [
|
|
49
50
|
{ title: 'Daily workflow', names: ['dev', 'refresh', 'build'] },
|
|
50
51
|
{ title: 'Quality gates', names: ['typecheck', 'lint', 'check'] },
|
|
51
|
-
{ title: 'Manifest and contracts', names: ['doctor', 'explain', 'trace', 'command'] },
|
|
52
|
+
{ title: 'Manifest and contracts', names: ['doctor', 'explain', 'trace', 'command', 'session'] },
|
|
52
53
|
{ title: 'Project scaffolding', names: ['init', 'create'] },
|
|
53
54
|
];
|
|
54
55
|
|
|
@@ -274,6 +275,31 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
|
|
|
274
275
|
],
|
|
275
276
|
status: 'experimental',
|
|
276
277
|
},
|
|
278
|
+
session: {
|
|
279
|
+
name: 'session',
|
|
280
|
+
category: 'Manifest and contracts',
|
|
281
|
+
summary: 'Mint a dev-only auth session token and cookie payload for a known user.',
|
|
282
|
+
usage: 'proteum session <email> [--role <role>] [--port <port>|--url <baseUrl>] [--json]',
|
|
283
|
+
bestFor:
|
|
284
|
+
'Starting browser or API automation from an authenticated state without driving the login UI, while still using the app-configured auth service.',
|
|
285
|
+
examples: [
|
|
286
|
+
{
|
|
287
|
+
description: 'Mint an admin session for a running dev server',
|
|
288
|
+
command: 'proteum session admin@example.com --role ADMIN --port 3101',
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
description: 'Mint a GOD session for unique.domains and print machine-readable cookie data',
|
|
292
|
+
command: 'proteum session god@example.com --role GOD --json',
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
notes: [
|
|
296
|
+
'Sessions are available only in dev mode and use the auth service registered on the current app router.',
|
|
297
|
+
'You must provide the target user email explicitly; Proteum does not guess your admin account universally across apps.',
|
|
298
|
+
'The command returns a token plus Playwright-ready cookie JSON so agents can inject the session into a browser context directly.',
|
|
299
|
+
'Without `--port` or `--url`, Proteum refreshes generated artifacts, builds the dev output, starts a temporary local dev server, creates the session, prints the payload, and exits.',
|
|
300
|
+
],
|
|
301
|
+
status: 'experimental',
|
|
302
|
+
},
|
|
277
303
|
};
|
|
278
304
|
|
|
279
305
|
export const isLikelyProteumAppRoot = (workdir: string) =>
|
package/cli/runtime/commands.ts
CHANGED
|
@@ -296,6 +296,32 @@ class CommandCommand extends ProteumCommand {
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
class SessionCommand extends ProteumCommand {
|
|
300
|
+
public static paths = [['session']];
|
|
301
|
+
|
|
302
|
+
public static usage = buildUsage('session');
|
|
303
|
+
|
|
304
|
+
public role = Option.String('--role', { description: 'Require the resolved user to have the given role.' });
|
|
305
|
+
public port = Option.String('--port', { description: 'Target an existing dev server on the given port.' });
|
|
306
|
+
public url = Option.String('--url', { description: 'Target an existing dev server at the given base URL.' });
|
|
307
|
+
public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
|
|
308
|
+
public args = Option.Rest();
|
|
309
|
+
|
|
310
|
+
public async execute() {
|
|
311
|
+
const [email = ''] = this.args;
|
|
312
|
+
|
|
313
|
+
this.setCliArgs({
|
|
314
|
+
email,
|
|
315
|
+
role: this.role ?? '',
|
|
316
|
+
port: this.port ?? '',
|
|
317
|
+
url: this.url ?? '',
|
|
318
|
+
json: this.json,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await runCommandModule(() => import('../commands/session'));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
299
325
|
export const registeredCommands = {
|
|
300
326
|
init: InitCommand,
|
|
301
327
|
create: CreateCommand,
|
|
@@ -309,6 +335,7 @@ export const registeredCommands = {
|
|
|
309
335
|
explain: ExplainCommand,
|
|
310
336
|
trace: TraceCommand,
|
|
311
337
|
command: CommandCommand,
|
|
338
|
+
session: SessionCommand,
|
|
312
339
|
} as const;
|
|
313
340
|
|
|
314
341
|
export const createCli = (version: string) => {
|
|
@@ -333,6 +360,7 @@ export const createCli = (version: string) => {
|
|
|
333
360
|
clipanion.register(ExplainCommand);
|
|
334
361
|
clipanion.register(TraceCommand);
|
|
335
362
|
clipanion.register(CommandCommand);
|
|
363
|
+
clipanion.register(SessionCommand);
|
|
336
364
|
|
|
337
365
|
return clipanion;
|
|
338
366
|
};
|