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 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
@@ -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
+ });
@@ -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) =>
@@ -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
  };