@dbx-tools/appkit-lakebox 0.0.0-stage → 0.9.131

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.
Files changed (55) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +104 -2
  3. package/index.ts +32 -0
  4. package/lib/index.d.ts +28 -0
  5. package/lib/index.js +26 -0
  6. package/lib/src/_package.d.ts +2 -0
  7. package/lib/src/_package.js +11 -0
  8. package/lib/src/_resource-registry.d.ts +41 -0
  9. package/lib/src/_resource-registry.js +128 -0
  10. package/lib/src/_service-snapshots.d.ts +55 -0
  11. package/lib/src/_service-snapshots.js +65 -0
  12. package/lib/src/config.d.ts +40 -0
  13. package/lib/src/config.js +86 -0
  14. package/lib/src/desktop.d.ts +6 -0
  15. package/lib/src/desktop.js +88 -0
  16. package/lib/src/metrics.d.ts +59 -0
  17. package/lib/src/metrics.js +151 -0
  18. package/lib/src/plugin.d.ts +259 -0
  19. package/lib/src/plugin.js +329 -0
  20. package/lib/src/routes.d.ts +75 -0
  21. package/lib/src/routes.js +441 -0
  22. package/lib/src/runtime.d.ts +154 -0
  23. package/lib/src/runtime.js +760 -0
  24. package/lib/src/services.d.ts +101 -0
  25. package/lib/src/services.js +188 -0
  26. package/lib/src/skills.d.ts +5 -0
  27. package/lib/src/skills.js +21 -0
  28. package/lib/src/user.d.ts +46 -0
  29. package/lib/src/user.js +76 -0
  30. package/lib/src/workspace.d.ts +43 -0
  31. package/lib/src/workspace.js +138 -0
  32. package/lib/tsconfig.tsbuildinfo +1 -0
  33. package/package.json +167 -4
  34. package/scripts/desktop-prerequisites.sh +66 -0
  35. package/scripts/desktop-session.sh +307 -0
  36. package/scripts/lakebox-admin.sh +234 -0
  37. package/scripts/lakebox-service.py +449 -0
  38. package/skills/html-page/SKILL.md +40 -0
  39. package/skills/jupyter-lab/SKILL.md +32 -0
  40. package/skills/lakebox-showcase/SKILL.md +66 -0
  41. package/skills/network-speed-test/SKILL.md +29 -0
  42. package/skills/realtime-whiteboard/SKILL.md +28 -0
  43. package/src/_package.ts +11 -0
  44. package/src/_resource-registry.ts +160 -0
  45. package/src/_service-snapshots.ts +84 -0
  46. package/src/config.ts +128 -0
  47. package/src/desktop.ts +97 -0
  48. package/src/metrics.ts +178 -0
  49. package/src/plugin.ts +446 -0
  50. package/src/routes.ts +524 -0
  51. package/src/runtime.ts +952 -0
  52. package/src/services.ts +239 -0
  53. package/src/skills.ts +23 -0
  54. package/src/user.ts +88 -0
  55. package/src/workspace.ts +185 -0
package/src/routes.ts ADDED
@@ -0,0 +1,524 @@
1
+ import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
2
+ import { createServer, type Server, type Socket } from "node:net";
3
+ import { type Duplex, PassThrough, Readable } from "node:stream";
4
+ import { ACCESS_TOKEN_HEADER, USER_EMAIL_HEADER, USER_ID_HEADER } from "@dbx-tools/appkit";
5
+ import type { SshSession } from "@dbx-tools/lakebox/session";
6
+ import { errorUtils, log } from "@dbx-tools/shared-core";
7
+ import type { LakeboxUser } from "@dbx-tools/shared-lakebox";
8
+ import type { NextFunction, Request, Response } from "express";
9
+ import ProxyModule from "http-proxy-3";
10
+ import type { LakeboxServiceRoute } from "./services.ts";
11
+
12
+ const MAX_PENDING_BYTES = 1024 * 1024;
13
+ const FORWARDING_READY_TIMEOUT_MS = 15_000;
14
+ const logger = log.logger("appkit-lakebox:routes");
15
+
16
+ /** Result of resolving one user's service for a routed request. */
17
+ export type LakeboxRouteResolution =
18
+ | { kind: "ready"; session: SshSession; route: LakeboxServiceRoute }
19
+ | { kind: "loading" }
20
+ | { kind: "failed"; message: string }
21
+ | { kind: "missing" };
22
+
23
+ /** Runtime callbacks that authorize and resolve deterministic routes. */
24
+ export interface LakeboxRouteResolver {
25
+ /** Returns the authenticated Lakebox user for request headers, or `undefined`. */
26
+ user(headers: IncomingHttpHeaders): LakeboxUser | undefined;
27
+ /**
28
+ * Resolves a running service. Returns `loading` after starting the Lakebox
29
+ * or service in the background, and `missing` for an unknown service.
30
+ */
31
+ resolve(user: LakeboxUser, service: string): Promise<LakeboxRouteResolution>;
32
+ /** Reports that a ready service refused a forwarded connection. */
33
+ unavailable(user: LakeboxUser, service: string): void;
34
+ }
35
+
36
+ interface RouteMatch {
37
+ userHash: string;
38
+ service: string;
39
+ mount: string;
40
+ }
41
+
42
+ interface Bridge {
43
+ session: SshSession;
44
+ server: Server;
45
+ target: { host: string; port: number; protocol: string };
46
+ sockets: Set<Socket>;
47
+ }
48
+
49
+ interface PendingResponse {
50
+ user: LakeboxUser;
51
+ service: string;
52
+ head?: string;
53
+ }
54
+
55
+ /**
56
+ * Proxies `<prefix>/<user-hash>/<service>` to that user's service port.
57
+ *
58
+ * The user hash is the only routing key. A request is forwarded only when its
59
+ * authenticated identity hashes to the same value, and the port is derived from
60
+ * the hash and service name, so there is no stored route table. Requests
61
+ * without an identity are rejected with 404, including WebSocket upgrades.
62
+ */
63
+ export class LakeboxRouteRegistry {
64
+ private readonly bridges = new Map<number, Bridge>();
65
+ private readonly upgrades = new Set<Socket>();
66
+ private readonly pending = new WeakMap<IncomingMessage, PendingResponse>();
67
+ private readonly proxy = ProxyModule.createProxyServer({ ws: true, xfwd: true });
68
+ private shuttingDown = false;
69
+
70
+ /** Creates a registry beneath one AppKit route prefix. */
71
+ constructor(
72
+ private readonly prefix: string,
73
+ private readonly resolver: LakeboxRouteResolver,
74
+ private readonly forwardingReadyTimeoutMs = FORWARDING_READY_TIMEOUT_MS,
75
+ ) {
76
+ this.proxy.on("error", (error, request, response) => {
77
+ if (this.shuttingDown && isConnectionClosureError(error)) {
78
+ response.destroy();
79
+ return;
80
+ }
81
+ const pending = this.pending.get(request);
82
+ if (pending && "writeHead" in response && !response.headersSent) {
83
+ this.resolver.unavailable(pending.user, pending.service);
84
+ sendLoadingResponse(request.headers.accept, response);
85
+ return;
86
+ }
87
+ logger.warn("Lakebox route proxy failed", {
88
+ method: request.method,
89
+ path: request.url,
90
+ error,
91
+ });
92
+ if ("writeHead" in response) {
93
+ if (response.headersSent) {
94
+ response.destroy();
95
+ } else {
96
+ response.writeHead(502);
97
+ response.end("Lakebox route unavailable");
98
+ }
99
+ } else if (!response.destroyed) {
100
+ response.destroy();
101
+ }
102
+ });
103
+ this.proxy.on("proxyRes", (proxyResponse, request, response) => {
104
+ const pending = this.pending.get(request);
105
+ if (pending && [502, 503, 504].includes(proxyResponse.statusCode ?? 0)) {
106
+ proxyResponse.resume();
107
+ this.resolver.unavailable(pending.user, pending.service);
108
+ sendLoadingResponse(request.headers.accept, response);
109
+ return;
110
+ }
111
+ if (pending?.head) {
112
+ void injectHtmlHead(proxyResponse, response, pending.head);
113
+ return;
114
+ }
115
+ pipeProxyResponse(proxyResponse, response);
116
+ });
117
+ }
118
+
119
+ /** Returns the route path segment match for a request path. */
120
+ match(path: string): RouteMatch | undefined {
121
+ const root = this.prefix.replace(/\/+$/, "");
122
+ if (!path.startsWith(`${root}/`)) return undefined;
123
+ const [userHash, service] = path.slice(root.length + 1).split("/");
124
+ if (!userHash || !service) return undefined;
125
+ return { userHash, service, mount: `${root}/${userHash}/${service}` };
126
+ }
127
+
128
+ /** Express middleware that authorizes, rewrites, and proxies matching requests. */
129
+ middleware() {
130
+ return async (request: Request, response: Response, next: NextFunction) => {
131
+ const match = this.match(request.path);
132
+ if (!match) return next();
133
+ const user = this.authorize(request.headers, match);
134
+ if (!user) {
135
+ response.statusCode = 404;
136
+ response.end("Not Found");
137
+ return;
138
+ }
139
+ if (request.path === match.mount) {
140
+ response.statusCode = 308;
141
+ response.setHeader("location", routeRedirectUrl(request.url, match.mount));
142
+ response.end();
143
+ return;
144
+ }
145
+ let resolution: LakeboxRouteResolution;
146
+ try {
147
+ resolution = await this.resolver.resolve(user, match.service);
148
+ } catch (error) {
149
+ return next(error);
150
+ }
151
+ if (resolution.kind === "missing") {
152
+ response.statusCode = 404;
153
+ response.end("Not Found");
154
+ return;
155
+ }
156
+ if (resolution.kind === "loading") {
157
+ sendLoadingResponse(request.header("accept"), response);
158
+ return;
159
+ }
160
+ if (resolution.kind === "failed") {
161
+ response.statusCode = 502;
162
+ response.setHeader("content-type", "text/plain; charset=utf-8");
163
+ response.end(`Lakebox service failed: ${resolution.message}`);
164
+ return;
165
+ }
166
+ const bridge = await this.bridge(resolution.session, resolution.route.port);
167
+ const head =
168
+ request.method === "GET" && request.path === `${match.mount}/`
169
+ ? resolution.route.htmlHead
170
+ : undefined;
171
+ if (head) request.headers["accept-encoding"] = "identity";
172
+ this.pending.set(request, { user, service: match.service, ...(head ? { head } : {}) });
173
+ const replayBody = replayParsedJsonBody(request);
174
+ if (resolution.route.stripPrefix) request.url = outboundPath(request.url, match.mount);
175
+ stripIdentityHeaders(request);
176
+ this.proxy.web(request, response, {
177
+ target: bridge.target,
178
+ changeOrigin: true,
179
+ selfHandleResponse: true,
180
+ ...(replayBody ? { buffer: replayBody } : {}),
181
+ });
182
+ };
183
+ }
184
+
185
+ /** Number of open forwarded HTTP and WebSocket connections. */
186
+ activeConnections(): number {
187
+ let count = this.upgrades.size;
188
+ for (const bridge of this.bridges.values()) count += bridge.sockets.size;
189
+ return count;
190
+ }
191
+
192
+ /** Returns whether an upgrade path belongs to the Lakebox route prefix. */
193
+ canHandleUpgrade(path: string): boolean {
194
+ return Boolean(this.match(path));
195
+ }
196
+
197
+ /** Proxies an authorized WebSocket upgrade. Returns false when it was not handled. */
198
+ async handleUpgrade(request: IncomingMessage, socket: Socket, head: Buffer): Promise<boolean> {
199
+ const url = new URL(request.url ?? "/", "http://localhost");
200
+ const match = this.match(url.pathname);
201
+ if (!match) return false;
202
+ const user = this.authorize(request.headers, match);
203
+ if (!user) return false;
204
+ const resolution = await this.resolver.resolve(user, match.service);
205
+ if (resolution.kind !== "ready") return false;
206
+ if (resolution.route.stripPrefix) request.url = outboundPath(request.url, match.mount);
207
+ this.upgrades.add(socket);
208
+ socket.once("close", () => this.upgrades.delete(socket));
209
+ proxyWebSocket(
210
+ request,
211
+ socket,
212
+ head,
213
+ resolution.session,
214
+ resolution.route.port,
215
+ this.forwardingReadyTimeoutMs,
216
+ );
217
+ return true;
218
+ }
219
+
220
+ /** Closes every forwarding bridge, for example after the Lakebox stops. */
221
+ async closeBridges(): Promise<void> {
222
+ this.upgrades.forEach((socket) => socket.destroy());
223
+ this.upgrades.clear();
224
+ const bridges = [...this.bridges.values()];
225
+ this.bridges.clear();
226
+ await Promise.all(bridges.map(closeBridge));
227
+ }
228
+
229
+ /** Suppresses expected proxy errors once shutdown begins. */
230
+ beginShutdown(): void {
231
+ this.shuttingDown = true;
232
+ }
233
+
234
+ /** Closes bridges and the shared proxy. */
235
+ async close(): Promise<void> {
236
+ this.beginShutdown();
237
+ await this.closeBridges();
238
+ this.proxy.close();
239
+ }
240
+
241
+ private authorize(headers: IncomingHttpHeaders, match: RouteMatch): LakeboxUser | undefined {
242
+ const user = this.resolver.user(headers);
243
+ return user?.hash === match.userHash ? user : undefined;
244
+ }
245
+
246
+ private async bridge(session: SshSession, port: number): Promise<Bridge> {
247
+ const existing = this.bridges.get(port);
248
+ if (existing?.session === session) return existing;
249
+ if (existing) {
250
+ this.bridges.delete(port);
251
+ void closeBridge(existing);
252
+ }
253
+ const sockets = new Set<Socket>();
254
+ const server = createServer((socket) => {
255
+ sockets.add(socket);
256
+ socket.once("close", () => sockets.delete(socket));
257
+ bridgeSocket(socket, session.forwardOut("127.0.0.1", port), this.forwardingReadyTimeoutMs);
258
+ });
259
+ const target = await listenTarget(server);
260
+ const bridge = { session, server, target, sockets };
261
+ const raced = this.bridges.get(port);
262
+ if (raced?.session === session) {
263
+ void closeBridge(bridge);
264
+ return raced;
265
+ }
266
+ this.bridges.set(port, bridge);
267
+ return bridge;
268
+ }
269
+ }
270
+
271
+ async function closeBridge(bridge: Bridge): Promise<void> {
272
+ bridge.sockets.forEach((socket) => socket.destroy());
273
+ await closeServer(bridge.server);
274
+ }
275
+
276
+ function stripIdentityHeaders(request: IncomingMessage): void {
277
+ for (const name of [ACCESS_TOKEN_HEADER, "authorization", "cookie"]) delete request.headers[name];
278
+ }
279
+
280
+ function sendLoadingResponse(
281
+ accept: string | string[] | undefined,
282
+ response: Pick<ServerResponse, "statusCode" | "setHeader" | "end">,
283
+ ): void {
284
+ response.statusCode = 202;
285
+ response.setHeader("cache-control", "no-store");
286
+ response.setHeader("retry-after", "2");
287
+ const acceptsHtml = (Array.isArray(accept) ? accept.join(",") : (accept ?? ""))
288
+ .toLowerCase()
289
+ .includes("text/html");
290
+ if (!acceptsHtml) {
291
+ response.setHeader("content-type", "application/json; charset=utf-8");
292
+ response.end(JSON.stringify({ status: "loading", message: "Starting Lakebox service" }));
293
+ return;
294
+ }
295
+ response.setHeader("content-type", "text/html; charset=utf-8");
296
+ response.end(`<!doctype html>
297
+ <html lang="en">
298
+ <head>
299
+ <meta charset="utf-8">
300
+ <meta name="viewport" content="width=device-width,initial-scale=1">
301
+ <meta http-equiv="refresh" content="2">
302
+ <title>Starting Lakebox service</title>
303
+ <style>
304
+ html{color-scheme:light dark;font-family:system-ui,sans-serif}body{min-height:100vh;margin:0;display:grid;place-items:center;background:Canvas;color:CanvasText}.loading{display:flex;align-items:center;gap:.75rem}.spinner{width:1.25rem;height:1.25rem;border:.2rem solid color-mix(in srgb,CanvasText 20%,transparent);border-top-color:CanvasText;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}
305
+ </style>
306
+ </head>
307
+ <body><div class="loading" role="status"><span class="spinner" aria-hidden="true"></span><span>Starting Lakebox service</span></div></body>
308
+ </html>`);
309
+ }
310
+
311
+ function isConnectionClosureError(error: unknown): boolean {
312
+ const code =
313
+ error && typeof error === "object" && "code" in error
314
+ ? String((error as { code?: unknown }).code)
315
+ : undefined;
316
+ return (
317
+ code === "ECONNRESET" ||
318
+ code === "EPIPE" ||
319
+ code === "ERR_STREAM_PREMATURE_CLOSE" ||
320
+ code === "ERR_STREAM_DESTROYED"
321
+ );
322
+ }
323
+
324
+ function replayParsedJsonBody(request: Request): Readable | undefined {
325
+ if (!request.readableEnded || request.body === undefined) return undefined;
326
+ if (!request.header("content-type")?.toLowerCase().includes("json")) return undefined;
327
+ const body = Buffer.from(JSON.stringify(request.body));
328
+ request.headers["content-length"] = String(body.byteLength);
329
+ delete request.headers["transfer-encoding"];
330
+ return Readable.from(body);
331
+ }
332
+
333
+ async function injectHtmlHead(
334
+ proxyResponse: IncomingMessage,
335
+ response: ServerResponse,
336
+ head: string,
337
+ ): Promise<void> {
338
+ try {
339
+ const chunks: Buffer[] = [];
340
+ for await (const chunk of proxyResponse) {
341
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
342
+ }
343
+ const source = Buffer.concat(chunks).toString("utf8");
344
+ const body = source.includes("<head>")
345
+ ? source.replace("<head>", `<head>${head}`)
346
+ : source.includes("</head>")
347
+ ? source.replace("</head>", `${head}</head>`)
348
+ : source;
349
+ for (const [name, value] of Object.entries(proxyResponse.headers)) {
350
+ if (value === undefined || name === "content-length" || name === "transfer-encoding") {
351
+ continue;
352
+ }
353
+ response.setHeader(name, value);
354
+ }
355
+ response.statusCode = proxyResponse.statusCode ?? 200;
356
+ if (proxyResponse.statusMessage) response.statusMessage = proxyResponse.statusMessage;
357
+ response.setHeader("content-length", Buffer.byteLength(body));
358
+ response.end(body);
359
+ } catch (error) {
360
+ logger.warn("Lakebox route HTML injection failed", {
361
+ error: errorUtils.toError(error),
362
+ });
363
+ if (!response.headersSent) response.writeHead(502);
364
+ response.end("Lakebox route unavailable");
365
+ }
366
+ }
367
+
368
+ function pipeProxyResponse(proxyResponse: IncomingMessage, response: ServerResponse): void {
369
+ for (const [name, value] of Object.entries(proxyResponse.headers)) {
370
+ if (value !== undefined) response.setHeader(name, value);
371
+ }
372
+ response.statusCode = proxyResponse.statusCode ?? 200;
373
+ if (proxyResponse.statusMessage) response.statusMessage = proxyResponse.statusMessage;
374
+ proxyResponse.pipe(response);
375
+ }
376
+
377
+ async function closeServer(server: Server): Promise<void> {
378
+ await new Promise<void>((resolve) => server.close(() => resolve()));
379
+ }
380
+
381
+ function routeRedirectUrl(url: string, routePath: string): string {
382
+ const parsed = new URL(url, "http://localhost");
383
+ parsed.pathname = `${routePath}/`;
384
+ return `${parsed.pathname}${parsed.search}`;
385
+ }
386
+
387
+ /**
388
+ * Keep the AppKit request URL on the inbound IncomingMessage so AppKit's
389
+ * request logging and tracing see the public route. Prefix
390
+ * stripping is applied only on the outbound proxy request.
391
+ */
392
+ function outboundPath(requestUrl: string | undefined, stripPrefix?: string): string {
393
+ const url = requestUrl && requestUrl.length > 0 ? requestUrl : "/";
394
+ const queryIndex = url.indexOf("?");
395
+ const pathname = queryIndex >= 0 ? url.slice(0, queryIndex) : url;
396
+ const query = queryIndex >= 0 ? url.slice(queryIndex) : "";
397
+ if (!stripPrefix) return url.startsWith("/") ? url : `/${url}`;
398
+ return `${pathname.slice(stripPrefix.length) || "/"}${query}`;
399
+ }
400
+
401
+ async function listenTarget(
402
+ server: Server,
403
+ ): Promise<{ host: string; port: number; protocol: string }> {
404
+ await new Promise<void>((resolve, reject) => {
405
+ server.once("error", reject);
406
+ server.listen(0, "127.0.0.1", resolve);
407
+ });
408
+ const address = server.address();
409
+ if (!address || typeof address === "string") {
410
+ throw new Error("Failed to bind Lakebox route bridge");
411
+ }
412
+ return { host: "127.0.0.1", port: address.port, protocol: "http:" };
413
+ }
414
+
415
+ function bridgeSocket(
416
+ socket: Socket,
417
+ channelPromise: Promise<Duplex>,
418
+ readyTimeoutMs: number,
419
+ initialData?: Buffer,
420
+ ): void {
421
+ const pending = new PassThrough({ highWaterMark: MAX_PENDING_BYTES });
422
+ let channel: Duplex | undefined;
423
+ let timedOut = false;
424
+ const readyTimeout = setTimeout(() => {
425
+ timedOut = true;
426
+ pending.destroy();
427
+ channel?.destroy();
428
+ if (!socket.destroyed) {
429
+ socket.end("HTTP/1.1 504 Gateway Timeout\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
430
+ }
431
+ }, readyTimeoutMs);
432
+ readyTimeout.unref?.();
433
+ const clearReadyTimeout = () => clearTimeout(readyTimeout);
434
+ if (initialData) pending.write(initialData);
435
+ socket.once("error", () => {
436
+ clearReadyTimeout();
437
+ pending.destroy();
438
+ channel?.destroy();
439
+ });
440
+ socket.pipe(pending);
441
+ socket.once("close", () => {
442
+ clearReadyTimeout();
443
+ pending.destroy();
444
+ channel?.destroy();
445
+ });
446
+
447
+ void channelPromise.then(
448
+ (openedChannel) => {
449
+ if (timedOut || socket.destroyed) {
450
+ pending.destroy();
451
+ openedChannel.destroy();
452
+ return;
453
+ }
454
+ channel = openedChannel;
455
+ clearReadyTimeout();
456
+ let channelEnded = false;
457
+ channel.once("end", () => {
458
+ channelEnded = true;
459
+ });
460
+ channel.once("close", () => {
461
+ if (!channelEnded && !socket.destroyed) socket.destroy();
462
+ });
463
+ channel.once("error", () => socket.destroy());
464
+ channel.pipe(socket);
465
+ pending.pipe(channel);
466
+ },
467
+ (error) => {
468
+ clearReadyTimeout();
469
+ if (!timedOut && !socket.destroyed) {
470
+ logger.warn("Lakebox route forwarding channel failed", {
471
+ error: errorUtils.toError(error),
472
+ });
473
+ socket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
474
+ }
475
+ pending.destroy();
476
+ if (timedOut || socket.destroyed) socket.destroy();
477
+ },
478
+ );
479
+ }
480
+
481
+ function proxyWebSocket(
482
+ request: IncomingMessage,
483
+ socket: Socket,
484
+ head: Buffer,
485
+ session: SshSession,
486
+ port: number,
487
+ readyTimeoutMs: number,
488
+ ): void {
489
+ stripWebsocketIdentityHeaders(request);
490
+ request.headers.origin = `http://127.0.0.1:${port}`;
491
+ request.headers.host = `127.0.0.1:${port}`;
492
+ bridgeSocket(
493
+ socket,
494
+ session.forwardOut("127.0.0.1", port),
495
+ readyTimeoutMs,
496
+ serializeUpgradeRequest(request, head),
497
+ );
498
+ }
499
+
500
+ function stripWebsocketIdentityHeaders(request: IncomingMessage): void {
501
+ for (const name of [
502
+ "authorization",
503
+ "cookie",
504
+ ACCESS_TOKEN_HEADER,
505
+ USER_EMAIL_HEADER,
506
+ USER_ID_HEADER,
507
+ ]) {
508
+ delete request.headers[name];
509
+ }
510
+ }
511
+
512
+ function serializeUpgradeRequest(request: IncomingMessage, head: Buffer): Buffer {
513
+ const lines = [
514
+ `${request.method ?? "GET"} ${request.url ?? "/"} HTTP/${request.httpVersion || "1.1"}`,
515
+ ];
516
+ for (const [name, value] of Object.entries(request.headers)) {
517
+ if (value === undefined) continue;
518
+ for (const item of Array.isArray(value) ? value : [value]) {
519
+ lines.push(`${name}: ${item}`);
520
+ }
521
+ }
522
+ lines.push("", "");
523
+ return Buffer.concat([Buffer.from(lines.join("\r\n")), head]);
524
+ }