@xfey/tutti 0.1.33 → 0.1.35
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 -3
- package/dist/server-shell/cli/args.d.ts +6 -0
- package/dist/server-shell/cli/args.js +30 -0
- package/dist/server-shell/cli/cli.js +118 -6
- package/dist/server-shell/cli/errors.d.ts +1 -1
- package/dist/server-shell/cli/host-runtime-endpoint.d.ts +1 -0
- package/dist/server-shell/cli/host-runtime-endpoint.js +10 -5
- package/dist/server-shell/cli/launch.js +1 -0
- package/dist/server-shell/cli/machine-local.d.ts +3 -0
- package/dist/server-shell/cli/machine-local.js +24 -3
- package/dist/server-shell/cli/machine-project-inspector.d.ts +1 -0
- package/dist/server-shell/cli/machine-project-inspector.js +1 -0
- package/dist/server-shell/cli/managed-host.d.ts +1 -0
- package/dist/server-shell/cli/managed-host.js +6 -4
- package/dist/server-shell/cli/runtime-commands.d.ts +3 -2
- package/dist/server-shell/cli/runtime-commands.js +9 -1
- package/dist/server-shell/local-console/folder-picker.d.ts +0 -1
- package/dist/server-shell/local-console/folder-picker.js +2 -33
- package/dist/server-shell/local-console/invocation-context.d.ts +21 -0
- package/dist/server-shell/local-console/invocation-context.js +56 -0
- package/dist/server-shell/local-console/lifecycle-lock.d.ts +13 -0
- package/dist/server-shell/local-console/lifecycle-lock.js +135 -0
- package/dist/server-shell/local-console/managed-console.d.ts +38 -0
- package/dist/server-shell/local-console/managed-console.js +254 -36
- package/dist/server-shell/local-console/project-service.d.ts +16 -9
- package/dist/server-shell/local-console/project-service.js +152 -58
- package/dist/server-shell/local-console/runtime-endpoint.d.ts +15 -1
- package/dist/server-shell/local-console/runtime-endpoint.js +21 -5
- package/dist/server-shell/local-console/server.d.ts +12 -0
- package/dist/server-shell/local-console/server.js +152 -45
- package/dist/server-shell/local-console/session.d.ts +10 -15
- package/dist/server-shell/local-console/session.js +69 -17
- package/package.json +1 -1
- package/web/assets/index-DPuaTngs.js +29 -0
- package/web/assets/{index-BcZpeSaX.css → index-Dmbchapb.css} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-DKf4jz_E.js +0 -29
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import fastify from "fastify";
|
|
4
5
|
import { resolveTuttiHome } from "../../providers/openai/index.js";
|
|
6
|
+
import { readCliVersion } from "../cli/version.js";
|
|
5
7
|
import { registerHostWebStaticRoutes, resolvePackagedWebStaticRoot } from "../http/static-web.js";
|
|
6
|
-
import {
|
|
8
|
+
import { detectLocalFolderPicker, pickLocalFolder } from "./folder-picker.js";
|
|
9
|
+
import { LOCAL_CONSOLE_INVOCATION_ENV_KEYS, createLocalConsoleServiceEnvironment, } from "./invocation-context.js";
|
|
7
10
|
import { LocalConsoleProjectError, LocalConsoleProjectService, } from "./project-service.js";
|
|
8
11
|
import { LOCAL_CONSOLE_SESSION_COOKIE, LocalConsoleSessionRegistry, localConsoleSessionCookie, parseCookieHeader, } from "./session.js";
|
|
9
12
|
import { createLocalConsoleSecret, deleteLocalConsoleRuntimeEndpoint, writeLocalConsoleRuntimeEndpoint, } from "./runtime-endpoint.js";
|
|
@@ -59,12 +62,13 @@ function requireLoopbackHost(request) {
|
|
|
59
62
|
}
|
|
60
63
|
export function createLocalConsoleServer(options) {
|
|
61
64
|
const app = fastify({ logger: false, bodyLimit: 64 * 1024 });
|
|
65
|
+
const tuttiHome = options.tuttiHome ?? resolveTuttiHome(options.env?.TUTTI_HOME, options.cwd);
|
|
62
66
|
const sessions = new LocalConsoleSessionRegistry({
|
|
63
67
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
64
68
|
});
|
|
65
69
|
const projects = new LocalConsoleProjectService({
|
|
66
|
-
|
|
67
|
-
...(options.env === undefined ? {} : {
|
|
70
|
+
tuttiHome,
|
|
71
|
+
...(options.env === undefined ? {} : { serviceEnvironment: options.env }),
|
|
68
72
|
});
|
|
69
73
|
app.addHook("onRequest", (request, reply, done) => {
|
|
70
74
|
try {
|
|
@@ -101,10 +105,11 @@ export function createLocalConsoleServer(options) {
|
|
|
101
105
|
if (!tokenEquals(bearerToken(request.headers.authorization), options.controlToken)) {
|
|
102
106
|
throw new LocalConsoleHttpError(401, "unauthorized", "Local Console control token is invalid.");
|
|
103
107
|
}
|
|
108
|
+
options.onActivity?.();
|
|
104
109
|
}
|
|
105
110
|
function requireSession(request, csrf = false) {
|
|
106
111
|
const cookie = parseCookieHeader(headerValue(request.headers.cookie));
|
|
107
|
-
const session = sessions.readSession(cookie[LOCAL_CONSOLE_SESSION_COOKIE]);
|
|
112
|
+
const session = sessions.readSession(cookie[LOCAL_CONSOLE_SESSION_COOKIE], headerValue(request.headers["x-local-console-context"]));
|
|
108
113
|
if (session === null) {
|
|
109
114
|
throw new LocalConsoleHttpError(401, "session_required", "Open Tutti from the CLI again.");
|
|
110
115
|
}
|
|
@@ -115,22 +120,65 @@ export function createLocalConsoleServer(options) {
|
|
|
115
120
|
throw new LocalConsoleHttpError(403, "csrf_rejected", "Local Console request was rejected.");
|
|
116
121
|
}
|
|
117
122
|
}
|
|
123
|
+
options.onActivity?.();
|
|
118
124
|
return session;
|
|
119
125
|
}
|
|
120
|
-
app.get(`${LOCAL_CONSOLE_API_BASE}/health`, () => ({
|
|
126
|
+
app.get(`${LOCAL_CONSOLE_API_BASE}/health`, () => ({
|
|
127
|
+
status: "ready",
|
|
128
|
+
protocol_version: 2,
|
|
129
|
+
...(options.runtime === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: {
|
|
132
|
+
instance_id: options.runtime.instanceId,
|
|
133
|
+
version: options.runtime.version,
|
|
134
|
+
pid: options.runtime.pid,
|
|
135
|
+
}),
|
|
136
|
+
}));
|
|
137
|
+
app.get(`${LOCAL_CONSOLE_API_BASE}/control/status`, (request) => {
|
|
138
|
+
requireControl(request);
|
|
139
|
+
return {
|
|
140
|
+
status: "running",
|
|
141
|
+
protocol_version: 2,
|
|
142
|
+
...(options.runtime === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: {
|
|
145
|
+
instance_id: options.runtime.instanceId,
|
|
146
|
+
version: options.runtime.version,
|
|
147
|
+
entrypoint: options.runtime.entrypoint,
|
|
148
|
+
started_at: options.runtime.startedAt,
|
|
149
|
+
pid: options.runtime.pid,
|
|
150
|
+
}),
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
app.post(`${LOCAL_CONSOLE_API_BASE}/control/shutdown`, (request) => {
|
|
154
|
+
requireControl(request);
|
|
155
|
+
setImmediate(() => options.onShutdown?.());
|
|
156
|
+
return { status: "stopping" };
|
|
157
|
+
});
|
|
121
158
|
app.post(`${LOCAL_CONSOLE_API_BASE}/access-tokens`, {
|
|
122
159
|
schema: {
|
|
123
160
|
body: {
|
|
124
161
|
type: "object",
|
|
125
162
|
required: ["current_directory"],
|
|
126
163
|
additionalProperties: false,
|
|
127
|
-
properties: {
|
|
164
|
+
properties: {
|
|
165
|
+
current_directory: { type: "string", minLength: 1, maxLength: 4096 },
|
|
166
|
+
environment: {
|
|
167
|
+
type: "object",
|
|
168
|
+
additionalProperties: false,
|
|
169
|
+
properties: Object.fromEntries(LOCAL_CONSOLE_INVOCATION_ENV_KEYS.map((key) => [
|
|
170
|
+
key,
|
|
171
|
+
{ type: "string", minLength: 1, maxLength: 4096 },
|
|
172
|
+
])),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
128
175
|
},
|
|
129
176
|
},
|
|
130
177
|
}, (request) => {
|
|
131
178
|
requireControl(request);
|
|
132
179
|
const access = sessions.issueAccessToken({
|
|
133
180
|
currentDirectory: resolve(request.body.current_directory),
|
|
181
|
+
environment: request.body.environment ?? {},
|
|
134
182
|
});
|
|
135
183
|
return { access_token: access.token, expires_at: access.expires_at };
|
|
136
184
|
});
|
|
@@ -145,51 +193,41 @@ export function createLocalConsoleServer(options) {
|
|
|
145
193
|
},
|
|
146
194
|
}, (request, reply) => {
|
|
147
195
|
requireSameOrigin(request);
|
|
148
|
-
const
|
|
196
|
+
const cookie = parseCookieHeader(headerValue(request.headers.cookie));
|
|
197
|
+
const session = sessions.exchangeAccessToken(request.body.access_token, cookie[LOCAL_CONSOLE_SESSION_COOKIE]);
|
|
149
198
|
if (session === null) {
|
|
150
199
|
throw new LocalConsoleHttpError(401, "access_token_invalid", "This Local Console access link has expired. Run `tutti` again.");
|
|
151
200
|
}
|
|
152
|
-
|
|
153
|
-
|
|
201
|
+
if (session.sessionToken !== undefined) {
|
|
202
|
+
void reply.header("set-cookie", localConsoleSessionCookie(session.sessionToken, session.expires_at));
|
|
203
|
+
}
|
|
204
|
+
options.onActivity?.();
|
|
205
|
+
return {
|
|
206
|
+
csrf_token: session.csrfToken,
|
|
207
|
+
context_token: session.contextToken,
|
|
208
|
+
expires_at: session.expires_at,
|
|
209
|
+
};
|
|
154
210
|
});
|
|
155
211
|
app.get(`${LOCAL_CONSOLE_API_BASE}/bootstrap`, (request) => {
|
|
156
212
|
const session = requireSession(request);
|
|
157
|
-
const picker = detectLocalFolderPicker(process.platform, options.env ?? process.env);
|
|
158
213
|
return {
|
|
159
214
|
csrf_token: session.csrfToken,
|
|
160
|
-
current_directory: session.currentDirectory,
|
|
215
|
+
current_directory: session.context.currentDirectory,
|
|
161
216
|
platform: process.platform,
|
|
162
|
-
folder_picker:
|
|
217
|
+
folder_picker: detectLocalFolderPicker(process.platform, session.context.environment).kind,
|
|
163
218
|
};
|
|
164
219
|
});
|
|
165
220
|
app.get(`${LOCAL_CONSOLE_API_BASE}/projects`, async (request) => {
|
|
221
|
+
const session = requireSession(request);
|
|
222
|
+
return { projects: await projects.listProjects(session.context) };
|
|
223
|
+
});
|
|
224
|
+
app.get(`${LOCAL_CONSOLE_API_BASE}/heartbeat`, (request) => {
|
|
166
225
|
requireSession(request);
|
|
167
|
-
return {
|
|
226
|
+
return { status: "ok" };
|
|
168
227
|
});
|
|
169
228
|
app.post(`${LOCAL_CONSOLE_API_BASE}/folders/pick`, async (request) => {
|
|
170
|
-
requireSession(request, true);
|
|
171
|
-
return await pickLocalFolder({ env:
|
|
172
|
-
});
|
|
173
|
-
app.post(`${LOCAL_CONSOLE_API_BASE}/folders/create`, {
|
|
174
|
-
schema: {
|
|
175
|
-
body: {
|
|
176
|
-
type: "object",
|
|
177
|
-
required: ["parent_path", "name"],
|
|
178
|
-
additionalProperties: false,
|
|
179
|
-
properties: {
|
|
180
|
-
parent_path: { type: "string", minLength: 1 },
|
|
181
|
-
name: { type: "string", minLength: 1, maxLength: 160 },
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
}, (request) => {
|
|
186
|
-
requireSession(request, true);
|
|
187
|
-
try {
|
|
188
|
-
return { path: createLocalProjectFolder(request.body.parent_path, request.body.name) };
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
throw new LocalConsoleHttpError(422, "project_folder_create_failed", error instanceof Error ? error.message : "Project folder could not be created.");
|
|
192
|
-
}
|
|
229
|
+
const session = requireSession(request, true);
|
|
230
|
+
return await pickLocalFolder({ env: session.context.environment });
|
|
193
231
|
});
|
|
194
232
|
app.post(`${LOCAL_CONSOLE_API_BASE}/providers/models`, {
|
|
195
233
|
schema: {
|
|
@@ -232,19 +270,19 @@ export function createLocalConsoleServer(options) {
|
|
|
232
270
|
},
|
|
233
271
|
},
|
|
234
272
|
}, async (request) => {
|
|
235
|
-
requireSession(request, true);
|
|
273
|
+
const session = requireSession(request, true);
|
|
236
274
|
return await projects.launchProject({
|
|
237
275
|
workspacePath: request.body.workspace_path,
|
|
238
276
|
...(request.body.provider === undefined ? {} : { provider: request.body.provider }),
|
|
239
|
-
});
|
|
277
|
+
}, session.context);
|
|
240
278
|
});
|
|
241
279
|
app.post(`${LOCAL_CONSOLE_API_BASE}/projects/:projectId/invite`, async (request) => {
|
|
242
|
-
requireSession(request, true);
|
|
243
|
-
return await projects.refreshInvite(request.params.projectId);
|
|
280
|
+
const session = requireSession(request, true);
|
|
281
|
+
return await projects.refreshInvite(request.params.projectId, session.context);
|
|
244
282
|
});
|
|
245
283
|
app.post(`${LOCAL_CONSOLE_API_BASE}/projects/:projectId/stop`, async (request) => {
|
|
246
|
-
requireSession(request, true);
|
|
247
|
-
return await projects.stopProject(request.params.projectId);
|
|
284
|
+
const session = requireSession(request, true);
|
|
285
|
+
return await projects.stopProject(request.params.projectId, session.context);
|
|
248
286
|
});
|
|
249
287
|
const webStaticRoot = options.webStaticRoot === false
|
|
250
288
|
? undefined
|
|
@@ -261,24 +299,64 @@ function listeningPort(app) {
|
|
|
261
299
|
}
|
|
262
300
|
return address.port;
|
|
263
301
|
}
|
|
302
|
+
function entrypointIdentity(path) {
|
|
303
|
+
try {
|
|
304
|
+
const stats = statSync(path);
|
|
305
|
+
return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}`;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
264
311
|
export async function runLocalConsoleProcess(options = {}) {
|
|
265
312
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
266
313
|
const env = options.env ?? process.env;
|
|
267
314
|
const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
|
|
315
|
+
const serviceEnvironment = createLocalConsoleServiceEnvironment({ env, tuttiHome });
|
|
268
316
|
const controlToken = createLocalConsoleSecret();
|
|
317
|
+
const instanceId = createLocalConsoleSecret();
|
|
318
|
+
const now = options.now ?? (() => new Date());
|
|
319
|
+
const startedAt = now().toISOString();
|
|
320
|
+
const version = readCliVersion();
|
|
321
|
+
const entrypointArgument = process.argv[1];
|
|
322
|
+
if (entrypointArgument === undefined || entrypointArgument.trim() === "") {
|
|
323
|
+
throw new Error("Cannot locate the Tutti CLI entrypoint.");
|
|
324
|
+
}
|
|
325
|
+
const entrypoint = resolve(entrypointArgument);
|
|
326
|
+
const initialEntrypointIdentity = entrypointIdentity(entrypoint);
|
|
327
|
+
if (initialEntrypointIdentity === null) {
|
|
328
|
+
throw new Error("The Tutti CLI entrypoint is unavailable.");
|
|
329
|
+
}
|
|
330
|
+
let lastActivityAt = now().getTime();
|
|
331
|
+
let requestClose = () => undefined;
|
|
269
332
|
const app = createLocalConsoleServer({
|
|
270
333
|
controlToken,
|
|
271
334
|
cwd,
|
|
272
|
-
|
|
335
|
+
tuttiHome,
|
|
336
|
+
env: serviceEnvironment,
|
|
337
|
+
runtime: {
|
|
338
|
+
instanceId,
|
|
339
|
+
version,
|
|
340
|
+
entrypoint,
|
|
341
|
+
startedAt,
|
|
342
|
+
pid: process.pid,
|
|
343
|
+
},
|
|
344
|
+
onActivity: () => {
|
|
345
|
+
lastActivityAt = now().getTime();
|
|
346
|
+
},
|
|
347
|
+
onShutdown: () => requestClose(),
|
|
273
348
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
274
349
|
});
|
|
275
350
|
await app.listen({ host: "127.0.0.1", port: 0 });
|
|
276
351
|
writeLocalConsoleRuntimeEndpoint({
|
|
277
352
|
tuttiHome,
|
|
353
|
+
instanceId,
|
|
278
354
|
pid: process.pid,
|
|
279
355
|
baseUrl: `http://127.0.0.1:${listeningPort(app)}`,
|
|
280
356
|
controlToken,
|
|
281
|
-
|
|
357
|
+
version,
|
|
358
|
+
entrypoint,
|
|
359
|
+
now,
|
|
282
360
|
});
|
|
283
361
|
let closing = false;
|
|
284
362
|
const close = async () => {
|
|
@@ -290,12 +368,41 @@ export async function runLocalConsoleProcess(options = {}) {
|
|
|
290
368
|
tuttiHome,
|
|
291
369
|
expectedControlToken: controlToken,
|
|
292
370
|
});
|
|
293
|
-
|
|
371
|
+
const forceClose = setTimeout(() => app.server.closeAllConnections(), 5_000);
|
|
372
|
+
forceClose.unref();
|
|
373
|
+
try {
|
|
374
|
+
await app.close();
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
clearTimeout(forceClose);
|
|
378
|
+
}
|
|
294
379
|
};
|
|
380
|
+
requestClose = () => void close();
|
|
381
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? 60 * 60 * 1_000;
|
|
382
|
+
const idleCheck = setInterval(() => {
|
|
383
|
+
let installedVersion = null;
|
|
384
|
+
try {
|
|
385
|
+
installedVersion = readCliVersion();
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// Package removal is handled as a runtime identity change.
|
|
389
|
+
}
|
|
390
|
+
if (!existsSync(entrypoint) ||
|
|
391
|
+
entrypointIdentity(entrypoint) !== initialEntrypointIdentity ||
|
|
392
|
+
installedVersion !== version) {
|
|
393
|
+
void close();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (now().getTime() - lastActivityAt >= idleTimeoutMs) {
|
|
397
|
+
void close();
|
|
398
|
+
}
|
|
399
|
+
}, Math.min(60_000, Math.max(1_000, Math.floor(idleTimeoutMs / 4))));
|
|
400
|
+
idleCheck.unref();
|
|
295
401
|
process.once("SIGINT", () => void close());
|
|
296
402
|
process.once("SIGTERM", () => void close());
|
|
297
403
|
await new Promise((resolveClosed) => {
|
|
298
404
|
app.server.once("close", resolveClosed);
|
|
299
405
|
});
|
|
406
|
+
clearInterval(idleCheck);
|
|
300
407
|
}
|
|
301
408
|
//# sourceMappingURL=server.js.map
|
|
@@ -1,33 +1,28 @@
|
|
|
1
|
+
import type { LocalConsoleInvocationContext } from "./invocation-context.js";
|
|
1
2
|
export declare const LOCAL_CONSOLE_SESSION_COOKIE = "tutti_local_console_session";
|
|
2
|
-
type ExpiringValue = {
|
|
3
|
-
hash: string;
|
|
4
|
-
expiresAt: number;
|
|
5
|
-
};
|
|
6
|
-
type LocalConsoleSession = ExpiringValue & {
|
|
7
|
-
csrfToken: string;
|
|
8
|
-
currentDirectory: string;
|
|
9
|
-
};
|
|
10
3
|
export type IssuedLocalConsoleAccessToken = {
|
|
11
4
|
token: string;
|
|
12
5
|
expires_at: string;
|
|
13
6
|
};
|
|
14
7
|
export type ExchangedLocalConsoleSession = {
|
|
15
|
-
sessionToken
|
|
8
|
+
sessionToken?: string;
|
|
9
|
+
contextToken: string;
|
|
16
10
|
csrfToken: string;
|
|
17
11
|
expires_at: string;
|
|
18
12
|
};
|
|
13
|
+
export type ReadLocalConsoleSession = {
|
|
14
|
+
csrfToken: string;
|
|
15
|
+
context: LocalConsoleInvocationContext;
|
|
16
|
+
};
|
|
19
17
|
export declare class LocalConsoleSessionRegistry {
|
|
20
18
|
#private;
|
|
21
19
|
constructor(options?: {
|
|
22
20
|
now?: () => Date;
|
|
23
21
|
});
|
|
24
|
-
issueAccessToken(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
exchangeAccessToken(token: string): ExchangedLocalConsoleSession | null;
|
|
28
|
-
readSession(token: string | undefined): LocalConsoleSession | null;
|
|
22
|
+
issueAccessToken(context: LocalConsoleInvocationContext): IssuedLocalConsoleAccessToken;
|
|
23
|
+
exchangeAccessToken(token: string, existingSessionToken?: string): ExchangedLocalConsoleSession | null;
|
|
24
|
+
readSession(token: string | undefined, contextToken?: string): ReadLocalConsoleSession | null;
|
|
29
25
|
}
|
|
30
26
|
export declare function parseCookieHeader(header: string | undefined): Record<string, string>;
|
|
31
27
|
export declare function localConsoleSessionCookie(token: string, expiresAt: string): string;
|
|
32
|
-
export {};
|
|
33
28
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -16,22 +16,24 @@ function secretEquals(value, expectedHash) {
|
|
|
16
16
|
export class LocalConsoleSessionRegistry {
|
|
17
17
|
#accessTokens = new Map();
|
|
18
18
|
#sessions = new Map();
|
|
19
|
+
#contexts = new Map();
|
|
19
20
|
#now;
|
|
21
|
+
#browserSessionToken;
|
|
20
22
|
constructor(options = {}) {
|
|
21
23
|
this.#now = options.now ?? (() => new Date());
|
|
22
24
|
}
|
|
23
|
-
issueAccessToken(
|
|
25
|
+
issueAccessToken(context) {
|
|
24
26
|
this.#purgeExpired();
|
|
25
27
|
const token = createSecret();
|
|
26
28
|
const expiresAt = this.#now().getTime() + ACCESS_TOKEN_TTL_MS;
|
|
27
29
|
this.#accessTokens.set(hashSecret(token), {
|
|
28
30
|
hash: hashSecret(token),
|
|
29
31
|
expiresAt,
|
|
30
|
-
|
|
32
|
+
context,
|
|
31
33
|
});
|
|
32
34
|
return { token, expires_at: new Date(expiresAt).toISOString() };
|
|
33
35
|
}
|
|
34
|
-
exchangeAccessToken(token) {
|
|
36
|
+
exchangeAccessToken(token, existingSessionToken) {
|
|
35
37
|
this.#purgeExpired();
|
|
36
38
|
const accessTokenHash = hashSecret(token);
|
|
37
39
|
const accessToken = this.#accessTokens.get(accessTokenHash);
|
|
@@ -41,25 +43,58 @@ export class LocalConsoleSessionRegistry {
|
|
|
41
43
|
return null;
|
|
42
44
|
}
|
|
43
45
|
this.#accessTokens.delete(accessTokenHash);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
let sessionToken;
|
|
47
|
+
let sessionHash = "";
|
|
48
|
+
let session;
|
|
49
|
+
if (existingSessionToken !== undefined) {
|
|
50
|
+
sessionHash = hashSecret(existingSessionToken);
|
|
51
|
+
const existingSession = this.#sessions.get(sessionHash);
|
|
52
|
+
if (existingSession !== undefined &&
|
|
53
|
+
existingSession.expiresAt > this.#now().getTime() &&
|
|
54
|
+
secretEquals(existingSessionToken, existingSession.hash)) {
|
|
55
|
+
session = existingSession;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (session === undefined && this.#browserSessionToken !== undefined) {
|
|
59
|
+
const sharedSessionHash = hashSecret(this.#browserSessionToken);
|
|
60
|
+
const sharedSession = this.#sessions.get(sharedSessionHash);
|
|
61
|
+
if (sharedSession !== undefined &&
|
|
62
|
+
sharedSession.expiresAt > this.#now().getTime() &&
|
|
63
|
+
secretEquals(this.#browserSessionToken, sharedSession.hash)) {
|
|
64
|
+
sessionToken = this.#browserSessionToken;
|
|
65
|
+
sessionHash = sharedSessionHash;
|
|
66
|
+
session = sharedSession;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
53
69
|
if (session === undefined) {
|
|
54
|
-
|
|
70
|
+
sessionToken = createSecret();
|
|
71
|
+
this.#browserSessionToken = sessionToken;
|
|
72
|
+
sessionHash = hashSecret(sessionToken);
|
|
73
|
+
session = {
|
|
74
|
+
hash: sessionHash,
|
|
75
|
+
csrfToken: createSecret(),
|
|
76
|
+
expiresAt: this.#now().getTime() + SESSION_TTL_MS,
|
|
77
|
+
defaultContextHash: "",
|
|
78
|
+
};
|
|
79
|
+
this.#sessions.set(sessionHash, session);
|
|
55
80
|
}
|
|
81
|
+
const contextToken = createSecret();
|
|
82
|
+
const contextHash = hashSecret(contextToken);
|
|
83
|
+
session.defaultContextHash = contextHash;
|
|
84
|
+
this.#contexts.set(contextHash, {
|
|
85
|
+
hash: contextHash,
|
|
86
|
+
expiresAt: session.expiresAt,
|
|
87
|
+
sessionHash,
|
|
88
|
+
context: accessToken.context,
|
|
89
|
+
});
|
|
56
90
|
return {
|
|
57
|
-
sessionToken,
|
|
91
|
+
...(sessionToken === undefined ? {} : { sessionToken }),
|
|
92
|
+
contextToken,
|
|
58
93
|
csrfToken: session.csrfToken,
|
|
59
|
-
expires_at: new Date(expiresAt).toISOString(),
|
|
94
|
+
expires_at: new Date(session.expiresAt).toISOString(),
|
|
60
95
|
};
|
|
61
96
|
}
|
|
62
|
-
readSession(token) {
|
|
97
|
+
readSession(token, contextToken) {
|
|
63
98
|
this.#purgeExpired();
|
|
64
99
|
if (token === undefined || token.trim() === "") {
|
|
65
100
|
return null;
|
|
@@ -70,7 +105,15 @@ export class LocalConsoleSessionRegistry {
|
|
|
70
105
|
!secretEquals(token, session.hash)) {
|
|
71
106
|
return null;
|
|
72
107
|
}
|
|
73
|
-
|
|
108
|
+
const contextHash = contextToken === undefined ? session.defaultContextHash : hashSecret(contextToken);
|
|
109
|
+
const context = this.#contexts.get(contextHash);
|
|
110
|
+
if (context === undefined ||
|
|
111
|
+
context.sessionHash !== hashSecret(token) ||
|
|
112
|
+
context.expiresAt <= this.#now().getTime() ||
|
|
113
|
+
(contextToken !== undefined && !secretEquals(contextToken, context.hash))) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return { csrfToken: session.csrfToken, context: context.context };
|
|
74
117
|
}
|
|
75
118
|
#purgeExpired() {
|
|
76
119
|
const now = this.#now().getTime();
|
|
@@ -84,6 +127,15 @@ export class LocalConsoleSessionRegistry {
|
|
|
84
127
|
this.#sessions.delete(key);
|
|
85
128
|
}
|
|
86
129
|
}
|
|
130
|
+
if (this.#browserSessionToken !== undefined &&
|
|
131
|
+
!this.#sessions.has(hashSecret(this.#browserSessionToken))) {
|
|
132
|
+
this.#browserSessionToken = undefined;
|
|
133
|
+
}
|
|
134
|
+
for (const [key, value] of this.#contexts) {
|
|
135
|
+
if (value.expiresAt <= now || !this.#sessions.has(value.sessionHash)) {
|
|
136
|
+
this.#contexts.delete(key);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
87
139
|
}
|
|
88
140
|
}
|
|
89
141
|
export function parseCookieHeader(header) {
|