terrascale 0.3.0
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/LICENSE +202 -0
- package/README.md +143 -0
- package/package.json +159 -0
- package/sdk-current-contract.json +27 -0
- package/sdk-route-manifest.json +67 -0
- package/src/admin.js +9 -0
- package/src/better-auth.js +14 -0
- package/src/config.js +121 -0
- package/src/database-codec.js +845 -0
- package/src/database-types.js +237 -0
- package/src/database-view.js +422 -0
- package/src/database.js +420 -0
- package/src/discovery.js +374 -0
- package/src/http.js +887 -0
- package/src/index.js +79 -0
- package/src/local/authentication.js +47 -0
- package/src/local/better-auth.js +517 -0
- package/src/local/cli.js +51 -0
- package/src/local/context.js +23 -0
- package/src/local/environment.js +109 -0
- package/src/local/index.js +204 -0
- package/src/local/router.js +999 -0
- package/src/local/server.js +664 -0
- package/src/local/store.js +530 -0
- package/src/local/test-environment.js +74 -0
- package/src/management-contracts.js +72 -0
- package/src/management.js +12 -0
- package/src/native-origin.js +75 -0
- package/src/postgres.js +494 -0
- package/src/react/core.js +743 -0
- package/src/react/index.js +99 -0
- package/src/result.js +251 -0
- package/src/schema.js +366 -0
- package/src/sql.js +996 -0
- package/src/svelte/index.js +129 -0
- package/src/tanstack/index.js +511 -0
- package/src/ts-auth-discovery.js +190 -0
- package/src/ts-auth.js +3497 -0
- package/types/admin.d.ts +6 -0
- package/types/better-auth.d.ts +8 -0
- package/types/config.d.ts +58 -0
- package/types/database-codec.d.ts +111 -0
- package/types/database-types.d.ts +213 -0
- package/types/database-view.d.ts +183 -0
- package/types/database.d.ts +98 -0
- package/types/discovery.d.ts +114 -0
- package/types/http.d.ts +46 -0
- package/types/index.d.ts +52 -0
- package/types/local/authentication.d.ts +11 -0
- package/types/local/better-auth.d.ts +33 -0
- package/types/local/cli.d.ts +2 -0
- package/types/local/context.d.ts +14 -0
- package/types/local/environment.d.ts +23 -0
- package/types/local/index.d.ts +94 -0
- package/types/local/router.d.ts +66 -0
- package/types/local/server.d.ts +54 -0
- package/types/local/store.d.ts +106 -0
- package/types/local/test-environment.d.ts +25 -0
- package/types/management-contracts.d.ts +44 -0
- package/types/management.d.ts +6 -0
- package/types/native-origin.d.ts +23 -0
- package/types/postgres.d.ts +123 -0
- package/types/react/core.d.ts +366 -0
- package/types/react/index.d.ts +54 -0
- package/types/result.d.ts +161 -0
- package/types/schema.d.ts +145 -0
- package/types/sql.d.ts +288 -0
- package/types/svelte/index.d.ts +81 -0
- package/types/tanstack/index.d.ts +165 -0
- package/types/ts-auth-discovery.d.ts +11 -0
- package/types/ts-auth.d.ts +1826 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
/** @import { IncomingMessage, ServerResponse } from "node:http" */
|
|
4
|
+
|
|
5
|
+
const MAX_REQUEST_BODY_BYTES = 4 * 1024 * 1024;
|
|
6
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
7
|
+
const DEFAULT_PORT = 0;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The local HTTP endpoint exposed by a running server.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {Readonly<{
|
|
13
|
+
* readonly origin: string;
|
|
14
|
+
* readonly host: string;
|
|
15
|
+
* readonly port: number;
|
|
16
|
+
* }>} LocalServerAddress
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @typedef {(request: Request) => Promise<Response>} LocalRequestHandler */
|
|
20
|
+
/** @typedef {Readonly<{ host?: string; port?: number }>} StartOptions */
|
|
21
|
+
/** @typedef {boolean | Readonly<{ allowRemote?: boolean }>} ConstructorOptions */
|
|
22
|
+
/** @typedef {ReturnType<typeof createServer>} ServerLike */
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A small Node-only bridge for handlers that speak the Fetch API.
|
|
26
|
+
*
|
|
27
|
+
* The handler is deliberately the sole routing authority. In particular, this
|
|
28
|
+
* adapter does not reserve a health route (or any other private route).
|
|
29
|
+
*/
|
|
30
|
+
export class TerraScaleLocalHttpServer {
|
|
31
|
+
/** @type {LocalRequestHandler} */
|
|
32
|
+
#handler;
|
|
33
|
+
/** @type {boolean} */
|
|
34
|
+
#allowRemote;
|
|
35
|
+
/** @type {unknown} */
|
|
36
|
+
#server;
|
|
37
|
+
/** @type {LocalServerAddress | undefined} */
|
|
38
|
+
#address;
|
|
39
|
+
/** @type {Promise<LocalServerAddress> | undefined} */
|
|
40
|
+
#startPromise;
|
|
41
|
+
/** @type {Promise<void> | undefined} */
|
|
42
|
+
#closePromise;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {LocalRequestHandler} handler
|
|
46
|
+
* @param {ConstructorOptions} [options]
|
|
47
|
+
*/
|
|
48
|
+
constructor(handler, options = false) {
|
|
49
|
+
if (typeof handler !== "function") {
|
|
50
|
+
throw new TypeError(
|
|
51
|
+
"TerraScaleLocalHttpServer requires a request handler.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const allowRemote =
|
|
56
|
+
typeof options === "boolean"
|
|
57
|
+
? options
|
|
58
|
+
: options !== null &&
|
|
59
|
+
typeof options === "object" &&
|
|
60
|
+
(options.allowRemote === undefined ||
|
|
61
|
+
typeof options.allowRemote === "boolean")
|
|
62
|
+
? (options.allowRemote ?? false)
|
|
63
|
+
: undefined;
|
|
64
|
+
if (typeof allowRemote !== "boolean") {
|
|
65
|
+
throw new TypeError("allowRemote must be a boolean.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.#handler = handler;
|
|
69
|
+
this.#allowRemote = allowRemote;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @returns {LocalServerAddress | undefined} */
|
|
73
|
+
get address() {
|
|
74
|
+
return this.#address;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {StartOptions} [options]
|
|
79
|
+
* @returns {Promise<LocalServerAddress>}
|
|
80
|
+
*/
|
|
81
|
+
start(options = {}) {
|
|
82
|
+
if (this.#address !== undefined) {
|
|
83
|
+
return Promise.resolve(this.#address);
|
|
84
|
+
}
|
|
85
|
+
if (this.#startPromise !== undefined) {
|
|
86
|
+
return this.#startPromise;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const host = normalizeHost(options.host ?? DEFAULT_HOST);
|
|
90
|
+
const port = normalizePort(options.port ?? DEFAULT_PORT);
|
|
91
|
+
if (!this.#allowRemote && !isLoopbackHost(host)) {
|
|
92
|
+
return Promise.reject(
|
|
93
|
+
new TypeError(
|
|
94
|
+
`Refusing to bind local HTTP server to non-loopback host "${host}" without allowRemote.`,
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @type {Promise<LocalServerAddress>} */
|
|
100
|
+
const startPromise = new Promise((resolve, reject) => {
|
|
101
|
+
const server = createServer((request, response) => {
|
|
102
|
+
void handleRequest(request, response, this.#handler, host).catch(() => {
|
|
103
|
+
// handleRequest owns the response and turns handler failures into a
|
|
104
|
+
// response where possible. This final guard prevents a rejected
|
|
105
|
+
// callback promise from becoming an unhandled rejection if the peer
|
|
106
|
+
// has already disconnected.
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
this.#server = server;
|
|
110
|
+
|
|
111
|
+
/** @param {Error} error */
|
|
112
|
+
const onError = (error) => {
|
|
113
|
+
server.off("listening", onListening);
|
|
114
|
+
this.#server = undefined;
|
|
115
|
+
reject(error);
|
|
116
|
+
};
|
|
117
|
+
const onListening = () => {
|
|
118
|
+
server.off("error", onError);
|
|
119
|
+
const bound = server.address();
|
|
120
|
+
if (bound === null || typeof bound === "string") {
|
|
121
|
+
this.#server = undefined;
|
|
122
|
+
reject(new Error("Local HTTP server did not expose a TCP address."));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const address = makeAddress(host, bound.port);
|
|
127
|
+
this.#address = address;
|
|
128
|
+
resolve(address);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
server.once("error", onError);
|
|
132
|
+
server.once("listening", onListening);
|
|
133
|
+
server.listen({ host, port });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this.#startPromise = startPromise;
|
|
137
|
+
void startPromise.catch(() => {
|
|
138
|
+
if (this.#startPromise === startPromise) {
|
|
139
|
+
this.#startPromise = undefined;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return startPromise;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @returns {Promise<void>} */
|
|
146
|
+
async close() {
|
|
147
|
+
if (this.#closePromise !== undefined) {
|
|
148
|
+
return this.#closePromise;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const closePromise = (async () => {
|
|
152
|
+
const startPromise = this.#startPromise;
|
|
153
|
+
if (startPromise !== undefined) {
|
|
154
|
+
try {
|
|
155
|
+
await startPromise;
|
|
156
|
+
} catch {
|
|
157
|
+
// A failed listen has already released its server reference.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const server = /** @type {ServerLike | undefined} */ (this.#server);
|
|
162
|
+
if (server === undefined) {
|
|
163
|
+
this.#address = undefined;
|
|
164
|
+
this.#startPromise = undefined;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await closeNodeServer(server);
|
|
169
|
+
this.#server = undefined;
|
|
170
|
+
this.#address = undefined;
|
|
171
|
+
this.#startPromise = undefined;
|
|
172
|
+
})();
|
|
173
|
+
|
|
174
|
+
this.#closePromise = closePromise;
|
|
175
|
+
try {
|
|
176
|
+
await closePromise;
|
|
177
|
+
} finally {
|
|
178
|
+
if (this.#closePromise === closePromise) {
|
|
179
|
+
this.#closePromise = undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {string} host
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
function normalizeHost(host) {
|
|
190
|
+
if (typeof host !== "string") {
|
|
191
|
+
throw new TypeError("host must be a string.");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const trimmed = host.trim();
|
|
195
|
+
if (trimmed.length === 0) {
|
|
196
|
+
throw new TypeError("host cannot be empty.");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// URL notation commonly includes brackets around IPv6 literals, while
|
|
200
|
+
// net.Server.listen expects the unbracketed form.
|
|
201
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
202
|
+
const unbracketed = trimmed.slice(1, -1);
|
|
203
|
+
if (unbracketed.length === 0) {
|
|
204
|
+
throw new TypeError("host cannot be empty.");
|
|
205
|
+
}
|
|
206
|
+
return unbracketed;
|
|
207
|
+
}
|
|
208
|
+
return trimmed;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {number} port
|
|
213
|
+
* @returns {number}
|
|
214
|
+
*/
|
|
215
|
+
function normalizePort(port) {
|
|
216
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
217
|
+
throw new TypeError("port must be an integer between 0 and 65535.");
|
|
218
|
+
}
|
|
219
|
+
return port;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {string} host
|
|
224
|
+
* @returns {boolean}
|
|
225
|
+
*/
|
|
226
|
+
function isLoopbackHost(host) {
|
|
227
|
+
const normalized = host.toLowerCase().replace(/\.$/u, "");
|
|
228
|
+
if (normalized === "localhost") {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const family = isIP(normalized);
|
|
233
|
+
if (family === 4) {
|
|
234
|
+
const octets = normalized.split(".").map(Number);
|
|
235
|
+
return (
|
|
236
|
+
octets.length === 4 &&
|
|
237
|
+
octets[0] === 127 &&
|
|
238
|
+
octets.every((octet) => octet >= 0 && octet <= 255)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (family === 6) {
|
|
242
|
+
return normalized === "::1";
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {string} host
|
|
249
|
+
* @param {number} port
|
|
250
|
+
* @returns {LocalServerAddress}
|
|
251
|
+
*/
|
|
252
|
+
function makeAddress(host, port) {
|
|
253
|
+
const urlHost =
|
|
254
|
+
host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
255
|
+
const origin = `http://${urlHost}:${port}`;
|
|
256
|
+
return { origin, host, port };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {IncomingMessage} incoming
|
|
261
|
+
* @param {ServerResponse} outgoing
|
|
262
|
+
* @param {LocalRequestHandler} handler
|
|
263
|
+
* @param {string} fallbackHost
|
|
264
|
+
* @returns {Promise<void>}
|
|
265
|
+
*/
|
|
266
|
+
async function handleRequest(incoming, outgoing, handler, fallbackHost) {
|
|
267
|
+
const abortController = new AbortController();
|
|
268
|
+
const abort = () => {
|
|
269
|
+
if (!abortController.signal.aborted) {
|
|
270
|
+
abortController.abort();
|
|
271
|
+
}
|
|
272
|
+
if (!outgoing.writableEnded && !outgoing.destroyed) {
|
|
273
|
+
outgoing.destroy();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const onIncomingAborted = () => abort();
|
|
277
|
+
const onIncomingError = () => abort();
|
|
278
|
+
const onIncomingClose = () => {
|
|
279
|
+
if (!incoming.complete) {
|
|
280
|
+
abort();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const onOutgoingClose = () => {
|
|
284
|
+
if (!outgoing.writableEnded) {
|
|
285
|
+
abortController.abort();
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
incoming.once("aborted", onIncomingAborted);
|
|
290
|
+
incoming.once("error", onIncomingError);
|
|
291
|
+
incoming.once("close", onIncomingClose);
|
|
292
|
+
outgoing.once("close", onOutgoingClose);
|
|
293
|
+
outgoing.once("error", onIncomingError);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const body = await readRequestBody(incoming, abortController.signal);
|
|
297
|
+
if (abortController.signal.aborted) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const request = makeRequest(
|
|
302
|
+
incoming,
|
|
303
|
+
body,
|
|
304
|
+
abortController.signal,
|
|
305
|
+
fallbackHost,
|
|
306
|
+
);
|
|
307
|
+
const response = await awaitAbortable(
|
|
308
|
+
Promise.resolve().then(() => handler(request)),
|
|
309
|
+
abortController.signal,
|
|
310
|
+
);
|
|
311
|
+
if (abortController.signal.aborted || outgoing.destroyed) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await writeResponse(
|
|
316
|
+
outgoing,
|
|
317
|
+
response,
|
|
318
|
+
incoming.method ?? "GET",
|
|
319
|
+
abortController.signal,
|
|
320
|
+
);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (abortController.signal.aborted || outgoing.destroyed) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const status =
|
|
327
|
+
error instanceof RequestBodyTooLargeError
|
|
328
|
+
? 413
|
|
329
|
+
: error instanceof BadRequestError
|
|
330
|
+
? 400
|
|
331
|
+
: 500;
|
|
332
|
+
const message =
|
|
333
|
+
status === 413
|
|
334
|
+
? "Request body exceeds the 4 MiB limit."
|
|
335
|
+
: status === 400
|
|
336
|
+
? "Malformed request."
|
|
337
|
+
: "Internal Server Error";
|
|
338
|
+
sendErrorResponse(outgoing, status, message);
|
|
339
|
+
} finally {
|
|
340
|
+
incoming.off("aborted", onIncomingAborted);
|
|
341
|
+
incoming.off("error", onIncomingError);
|
|
342
|
+
incoming.off("close", onIncomingClose);
|
|
343
|
+
outgoing.off("close", onOutgoingClose);
|
|
344
|
+
outgoing.off("error", onIncomingError);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {IncomingMessage} incoming
|
|
350
|
+
* @param {Uint8Array} body
|
|
351
|
+
* @param {AbortSignal} signal
|
|
352
|
+
* @param {string} fallbackHost
|
|
353
|
+
* @returns {Request}
|
|
354
|
+
*/
|
|
355
|
+
function makeRequest(incoming, body, signal, fallbackHost) {
|
|
356
|
+
const headers = new Headers();
|
|
357
|
+
try {
|
|
358
|
+
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
359
|
+
if (value === undefined) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (Array.isArray(value)) {
|
|
363
|
+
for (const item of value) {
|
|
364
|
+
headers.append(name, item);
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
headers.append(name, value);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
throw new BadRequestError();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const authority = incoming.headers.host ?? formatHostForUrl(fallbackHost);
|
|
375
|
+
/** @type {URL} */
|
|
376
|
+
let url;
|
|
377
|
+
try {
|
|
378
|
+
url = new URL(incoming.url ?? "/", `http://${authority}`);
|
|
379
|
+
} catch {
|
|
380
|
+
throw new BadRequestError();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const method = incoming.method ?? "GET";
|
|
384
|
+
/** @type {RequestInit} */
|
|
385
|
+
const init = { method, headers, signal };
|
|
386
|
+
if (method !== "GET" && method !== "HEAD" && body.byteLength > 0) {
|
|
387
|
+
// Node's Buffer/Uint8Array is a valid fetch body at runtime. The DOM
|
|
388
|
+
// library's BodyInit declaration is narrower on some TypeScript versions.
|
|
389
|
+
init.body = /** @type {BodyInit} */ (/** @type {unknown} */ (body));
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
return new Request(url, init);
|
|
393
|
+
} catch {
|
|
394
|
+
throw new BadRequestError();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @param {string} host
|
|
400
|
+
* @returns {string}
|
|
401
|
+
*/
|
|
402
|
+
function formatHostForUrl(host) {
|
|
403
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {IncomingMessage} incoming
|
|
408
|
+
* @param {AbortSignal} signal
|
|
409
|
+
* @returns {Promise<Uint8Array>}
|
|
410
|
+
*/
|
|
411
|
+
async function readRequestBody(incoming, signal) {
|
|
412
|
+
const contentLength = incoming.headers["content-length"];
|
|
413
|
+
const declaredLength =
|
|
414
|
+
typeof contentLength === "string" ? Number(contentLength) : undefined;
|
|
415
|
+
if (
|
|
416
|
+
declaredLength !== undefined &&
|
|
417
|
+
Number.isFinite(declaredLength) &&
|
|
418
|
+
declaredLength > MAX_REQUEST_BODY_BYTES
|
|
419
|
+
) {
|
|
420
|
+
incoming.resume();
|
|
421
|
+
throw new RequestBodyTooLargeError();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
/** @type {Uint8Array[]} */
|
|
426
|
+
const chunks = [];
|
|
427
|
+
let size = 0;
|
|
428
|
+
let settled = false;
|
|
429
|
+
|
|
430
|
+
const cleanup = () => {
|
|
431
|
+
incoming.off("data", onData);
|
|
432
|
+
incoming.off("end", onEnd);
|
|
433
|
+
incoming.off("error", onError);
|
|
434
|
+
incoming.off("aborted", onAborted);
|
|
435
|
+
signal.removeEventListener("abort", onSignalAbort);
|
|
436
|
+
};
|
|
437
|
+
/** @param {Error} [error] */
|
|
438
|
+
const finish = (error) => {
|
|
439
|
+
if (settled) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
settled = true;
|
|
443
|
+
cleanup();
|
|
444
|
+
if (error === undefined) {
|
|
445
|
+
const output = new Uint8Array(size);
|
|
446
|
+
let offset = 0;
|
|
447
|
+
for (const chunk of chunks) {
|
|
448
|
+
output.set(chunk, offset);
|
|
449
|
+
offset += chunk.byteLength;
|
|
450
|
+
}
|
|
451
|
+
resolve(output);
|
|
452
|
+
} else {
|
|
453
|
+
reject(error);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
/** @param {Uint8Array | string} chunk */
|
|
457
|
+
const onData = (chunk) => {
|
|
458
|
+
const bytes =
|
|
459
|
+
typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
|
|
460
|
+
if (size + bytes.byteLength > MAX_REQUEST_BODY_BYTES) {
|
|
461
|
+
incoming.resume();
|
|
462
|
+
finish(new RequestBodyTooLargeError());
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
chunks.push(bytes);
|
|
466
|
+
size += bytes.byteLength;
|
|
467
|
+
};
|
|
468
|
+
const onEnd = () => finish();
|
|
469
|
+
/** @param {Error} error */
|
|
470
|
+
const onError = (error) => finish(error);
|
|
471
|
+
const onAborted = () => finish(new Error("Request aborted."));
|
|
472
|
+
const onSignalAbort = () => finish(new Error("Request aborted."));
|
|
473
|
+
|
|
474
|
+
incoming.on("data", onData);
|
|
475
|
+
incoming.once("end", onEnd);
|
|
476
|
+
incoming.once("error", onError);
|
|
477
|
+
incoming.once("aborted", onAborted);
|
|
478
|
+
signal.addEventListener("abort", onSignalAbort, { once: true });
|
|
479
|
+
if (signal.aborted) {
|
|
480
|
+
finish(new Error("Request aborted."));
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* @param {ServerResponse} outgoing
|
|
487
|
+
* @param {Response} response
|
|
488
|
+
* @param {string} method
|
|
489
|
+
* @param {AbortSignal} signal
|
|
490
|
+
* @returns {Promise<void>}
|
|
491
|
+
*/
|
|
492
|
+
async function writeResponse(outgoing, response, method, signal) {
|
|
493
|
+
for (const [name, value] of response.headers) {
|
|
494
|
+
outgoing.setHeader(name, value);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const getSetCookie = /** @type {Headers & { getSetCookie?: () => string[] }} */ (
|
|
498
|
+
response.headers
|
|
499
|
+
).getSetCookie;
|
|
500
|
+
if (getSetCookie !== undefined) {
|
|
501
|
+
const cookies = getSetCookie.call(response.headers);
|
|
502
|
+
if (cookies.length > 0) {
|
|
503
|
+
outgoing.setHeader("set-cookie", cookies);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
outgoing.writeHead(response.status);
|
|
507
|
+
|
|
508
|
+
if (
|
|
509
|
+
method === "HEAD" ||
|
|
510
|
+
response.body === null ||
|
|
511
|
+
response.status === 204 ||
|
|
512
|
+
response.status === 205 ||
|
|
513
|
+
response.status === 304
|
|
514
|
+
) {
|
|
515
|
+
outgoing.end();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const reader = response.body.getReader();
|
|
520
|
+
let completed = false;
|
|
521
|
+
try {
|
|
522
|
+
while (true) {
|
|
523
|
+
const result = await awaitAbortable(reader.read(), signal);
|
|
524
|
+
if (result.done) {
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
await writeChunk(outgoing, result.value, signal);
|
|
528
|
+
}
|
|
529
|
+
outgoing.end();
|
|
530
|
+
completed = true;
|
|
531
|
+
} finally {
|
|
532
|
+
if (!completed) {
|
|
533
|
+
try {
|
|
534
|
+
await reader.cancel();
|
|
535
|
+
} catch {
|
|
536
|
+
// The body may already have observed the same disconnect.
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
reader.releaseLock();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* @param {ServerResponse} outgoing
|
|
545
|
+
* @param {Uint8Array} chunk
|
|
546
|
+
* @param {AbortSignal} signal
|
|
547
|
+
* @returns {Promise<void>}
|
|
548
|
+
*/
|
|
549
|
+
function writeChunk(outgoing, chunk, signal) {
|
|
550
|
+
if (signal.aborted || outgoing.destroyed) {
|
|
551
|
+
return Promise.reject(new Error("Response aborted."));
|
|
552
|
+
}
|
|
553
|
+
if (outgoing.write(chunk)) {
|
|
554
|
+
return Promise.resolve();
|
|
555
|
+
}
|
|
556
|
+
return new Promise((resolve, reject) => {
|
|
557
|
+
const cleanup = () => {
|
|
558
|
+
outgoing.off("drain", onDrain);
|
|
559
|
+
outgoing.off("close", onClose);
|
|
560
|
+
outgoing.off("error", onError);
|
|
561
|
+
signal.removeEventListener("abort", onAbort);
|
|
562
|
+
};
|
|
563
|
+
const onDrain = () => {
|
|
564
|
+
cleanup();
|
|
565
|
+
resolve();
|
|
566
|
+
};
|
|
567
|
+
const onClose = () => {
|
|
568
|
+
cleanup();
|
|
569
|
+
reject(new Error("Response closed."));
|
|
570
|
+
};
|
|
571
|
+
/** @param {Error} error */
|
|
572
|
+
const onError = (error) => {
|
|
573
|
+
cleanup();
|
|
574
|
+
reject(error);
|
|
575
|
+
};
|
|
576
|
+
const onAbort = () => {
|
|
577
|
+
cleanup();
|
|
578
|
+
reject(new Error("Response aborted."));
|
|
579
|
+
};
|
|
580
|
+
outgoing.once("drain", onDrain);
|
|
581
|
+
outgoing.once("close", onClose);
|
|
582
|
+
outgoing.once("error", onError);
|
|
583
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
584
|
+
if (signal.aborted) {
|
|
585
|
+
onAbort();
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* @template T
|
|
592
|
+
* @param {Promise<T>} promise
|
|
593
|
+
* @param {AbortSignal} signal
|
|
594
|
+
* @returns {Promise<T>}
|
|
595
|
+
*/
|
|
596
|
+
async function awaitAbortable(promise, signal) {
|
|
597
|
+
if (signal.aborted) {
|
|
598
|
+
throw new Error("Operation aborted.");
|
|
599
|
+
}
|
|
600
|
+
return new Promise((resolve, reject) => {
|
|
601
|
+
const onAbort = () => {
|
|
602
|
+
cleanup();
|
|
603
|
+
reject(new Error("Operation aborted."));
|
|
604
|
+
};
|
|
605
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
606
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
607
|
+
promise.then(
|
|
608
|
+
(value) => {
|
|
609
|
+
cleanup();
|
|
610
|
+
resolve(value);
|
|
611
|
+
},
|
|
612
|
+
/** @param {unknown} error */
|
|
613
|
+
(error) => {
|
|
614
|
+
cleanup();
|
|
615
|
+
reject(error);
|
|
616
|
+
},
|
|
617
|
+
);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* @param {ServerResponse} outgoing
|
|
623
|
+
* @param {number} status
|
|
624
|
+
* @param {string} message
|
|
625
|
+
* @returns {void}
|
|
626
|
+
*/
|
|
627
|
+
function sendErrorResponse(outgoing, status, message) {
|
|
628
|
+
if (outgoing.destroyed || outgoing.headersSent) {
|
|
629
|
+
if (!outgoing.destroyed && !outgoing.writableEnded) {
|
|
630
|
+
outgoing.destroy();
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
outgoing.statusCode = status;
|
|
636
|
+
outgoing.setHeader("content-type", "text/plain; charset=utf-8");
|
|
637
|
+
outgoing.setHeader("content-length", Buffer.byteLength(message));
|
|
638
|
+
outgoing.end(message);
|
|
639
|
+
} catch {
|
|
640
|
+
outgoing.destroy();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* @param {ServerLike} server
|
|
646
|
+
* @returns {Promise<void>}
|
|
647
|
+
*/
|
|
648
|
+
function closeNodeServer(server) {
|
|
649
|
+
return new Promise((resolve, reject) => {
|
|
650
|
+
server.close((error) => {
|
|
651
|
+
if (
|
|
652
|
+
error !== undefined &&
|
|
653
|
+
/** @type {NodeJS.ErrnoException} */ (error).code !== "ERR_SERVER_NOT_RUNNING"
|
|
654
|
+
) {
|
|
655
|
+
reject(error);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
resolve();
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
class RequestBodyTooLargeError extends Error {}
|
|
664
|
+
class BadRequestError extends Error {}
|