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.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +143 -0
  3. package/package.json +159 -0
  4. package/sdk-current-contract.json +27 -0
  5. package/sdk-route-manifest.json +67 -0
  6. package/src/admin.js +9 -0
  7. package/src/better-auth.js +14 -0
  8. package/src/config.js +121 -0
  9. package/src/database-codec.js +845 -0
  10. package/src/database-types.js +237 -0
  11. package/src/database-view.js +422 -0
  12. package/src/database.js +420 -0
  13. package/src/discovery.js +374 -0
  14. package/src/http.js +887 -0
  15. package/src/index.js +79 -0
  16. package/src/local/authentication.js +47 -0
  17. package/src/local/better-auth.js +517 -0
  18. package/src/local/cli.js +51 -0
  19. package/src/local/context.js +23 -0
  20. package/src/local/environment.js +109 -0
  21. package/src/local/index.js +204 -0
  22. package/src/local/router.js +999 -0
  23. package/src/local/server.js +664 -0
  24. package/src/local/store.js +530 -0
  25. package/src/local/test-environment.js +74 -0
  26. package/src/management-contracts.js +72 -0
  27. package/src/management.js +12 -0
  28. package/src/native-origin.js +75 -0
  29. package/src/postgres.js +494 -0
  30. package/src/react/core.js +743 -0
  31. package/src/react/index.js +99 -0
  32. package/src/result.js +251 -0
  33. package/src/schema.js +366 -0
  34. package/src/sql.js +996 -0
  35. package/src/svelte/index.js +129 -0
  36. package/src/tanstack/index.js +511 -0
  37. package/src/ts-auth-discovery.js +190 -0
  38. package/src/ts-auth.js +3497 -0
  39. package/types/admin.d.ts +6 -0
  40. package/types/better-auth.d.ts +8 -0
  41. package/types/config.d.ts +58 -0
  42. package/types/database-codec.d.ts +111 -0
  43. package/types/database-types.d.ts +213 -0
  44. package/types/database-view.d.ts +183 -0
  45. package/types/database.d.ts +98 -0
  46. package/types/discovery.d.ts +114 -0
  47. package/types/http.d.ts +46 -0
  48. package/types/index.d.ts +52 -0
  49. package/types/local/authentication.d.ts +11 -0
  50. package/types/local/better-auth.d.ts +33 -0
  51. package/types/local/cli.d.ts +2 -0
  52. package/types/local/context.d.ts +14 -0
  53. package/types/local/environment.d.ts +23 -0
  54. package/types/local/index.d.ts +94 -0
  55. package/types/local/router.d.ts +66 -0
  56. package/types/local/server.d.ts +54 -0
  57. package/types/local/store.d.ts +106 -0
  58. package/types/local/test-environment.d.ts +25 -0
  59. package/types/management-contracts.d.ts +44 -0
  60. package/types/management.d.ts +6 -0
  61. package/types/native-origin.d.ts +23 -0
  62. package/types/postgres.d.ts +123 -0
  63. package/types/react/core.d.ts +366 -0
  64. package/types/react/index.d.ts +54 -0
  65. package/types/result.d.ts +161 -0
  66. package/types/schema.d.ts +145 -0
  67. package/types/sql.d.ts +288 -0
  68. package/types/svelte/index.d.ts +81 -0
  69. package/types/tanstack/index.d.ts +165 -0
  70. package/types/ts-auth-discovery.d.ts +11 -0
  71. 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 {}