@serverkgg/bridge 0.0.8 → 0.0.9

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 (48) hide show
  1. package/package.json +3 -3
  2. package/src/cli/validate.ts +1 -0
  3. package/src/context/bridgeContext.ts +10 -0
  4. package/src/context/index.ts +1 -0
  5. package/src/core/bridgeDownloadError.ts +10 -0
  6. package/src/core/bridgeHold.ts +12 -0
  7. package/src/core/bridgeKind.ts +2 -0
  8. package/src/core/bridgeUserError.test.ts +23 -0
  9. package/src/core/bridgeUserError.ts +12 -0
  10. package/src/core/index.ts +3 -0
  11. package/src/manifest/index.ts +1 -0
  12. package/src/manifest/manifest.test.ts +85 -0
  13. package/src/manifest/manifest.ts +21 -0
  14. package/src/manifest/presence.ts +48 -0
  15. package/src/manifest/primitives.ts +6 -0
  16. package/src/modules/bridge.ts +45 -2
  17. package/src/modules/bridgeDriver.ts +2 -0
  18. package/src/modules/bridgeModule.ts +25 -3
  19. package/src/panel/bridgeDetail.ts +49 -0
  20. package/src/panel/bridgeLayout.ts +1 -0
  21. package/src/panel/bridgePanel.ts +30 -0
  22. package/src/panel/bridgeUploadMode.ts +4 -0
  23. package/src/panel/index.ts +2 -0
  24. package/src/protocol/bridgeEgress.ts +8 -1
  25. package/src/protocol/bridgeEvents.ts +4 -0
  26. package/src/protocol/bridgeFrame.ts +28 -1
  27. package/src/protocol/bridgeLimits.ts +10 -0
  28. package/src/runtime/bridgeActions.ts +7 -0
  29. package/src/runtime/bridgeDriver.test.ts +477 -2
  30. package/src/runtime/bridgeDriver.ts +116 -3
  31. package/src/runtime/bridgeEgress.ts +3 -0
  32. package/src/runtime/bridgeEvents.ts +7 -5
  33. package/src/runtime/bridgeFiles.test.ts +625 -15
  34. package/src/runtime/bridgeFiles.ts +152 -45
  35. package/src/runtime/bridgeInstall.test.ts +118 -0
  36. package/src/runtime/bridgeInstall.ts +55 -0
  37. package/src/runtime/bridgeNet.ts +32 -3
  38. package/src/runtime/bridgePending.test.ts +148 -0
  39. package/src/runtime/bridgePending.ts +43 -0
  40. package/src/runtime/bridgeRegistry.test.ts +134 -0
  41. package/src/runtime/bridgeRegistry.ts +26 -4
  42. package/src/runtime/bridgeServer.test.ts +338 -0
  43. package/src/runtime/bridgeServer.ts +25 -3
  44. package/src/runtime/bridgeSupervisor.ts +18 -0
  45. package/src/runtime/bridgeZip.ts +179 -58
  46. package/src/runtime/serve.ts +64 -4
  47. package/src/wire/panel.test.ts +166 -0
  48. package/src/wire/panel.ts +32 -0
@@ -1,21 +1,28 @@
1
1
  import type { Dirent } from "node:fs";
2
2
  import { chown, mkdir, readdir, rename, rm, writeFile } from "node:fs/promises";
3
- import type { BridgeDownloadOptions, BridgeExtractOptions, BridgeFileEntry, BridgeFiles } from "../context";
4
- import { compileGlob } from "../utils";
3
+ import type {
4
+ BridgeDigestAlgorithm,
5
+ BridgeDownloadOptions,
6
+ BridgeExtractOptions,
7
+ BridgeFileEntry,
8
+ BridgeFiles,
9
+ } from "../context";
10
+ import { BridgeDownloadError } from "../core";
11
+ import { compileGlob, compileGlobs, matchesAny } from "../utils";
5
12
  import { type BridgeEgress, BridgeEgressCallError, BridgeEgressUnavailableError } from "./bridgeEgress";
6
- import type { BridgeLogger } from "./bridgeLog";
13
+ import type { BridgeLeveledLogger } from "./bridgeLog";
7
14
  import { USER_AGENT } from "./bridgeNet";
8
15
  import { BridgePathError, resolveWithin, toSegments } from "./bridgePath";
9
16
  import { isPrivileged } from "./bridgePrivilege";
10
- import { readZip, ZIP_ENTRY_LIMIT, ZIP_TREE_ENTRY_LIMIT } from "./bridgeZip";
17
+ import { openZip, ZIP_ENTRY_LIMIT, ZIP_TREE_ENTRY_LIMIT } from "./bridgeZip";
11
18
 
12
19
  const READ_LIMIT = 4 * 1024 * 1024;
13
20
 
14
- const DOWNLOAD_LIMIT = 512 * 1024 * 1024;
21
+ const DOWNLOAD_LIMIT = 2 * 1024 * 1024 * 1024;
15
22
 
16
23
  const DOWNLOAD_TIMEOUT_MS = 300_000;
17
24
 
18
- const ARCHIVE_LIMIT = 256 * 1024 * 1024;
25
+ const ARCHIVE_LIMIT = 2 * 1024 * 1024 * 1024;
19
26
 
20
27
  const WALK_DEPTH = 12;
21
28
 
@@ -23,18 +30,22 @@ const WALK_LIMIT = 4096;
23
30
 
24
31
  const MILESTONE_STEP = 25;
25
32
 
26
- const DIGEST_LENGTHS: Record<string, number> = {
33
+ const DIGEST_LENGTHS: Record<BridgeDigestAlgorithm, number> = {
27
34
  sha1: 40,
28
35
  sha256: 64,
29
36
  sha512: 128,
30
37
  };
31
38
 
39
+ const isDigestAlgorithm = (value: string): value is BridgeDigestAlgorithm => {
40
+ return Object.hasOwn(DIGEST_LENGTHS, value);
41
+ };
42
+
32
43
  export interface BridgeFilesOptions {
33
44
  root: string;
34
45
  uid: number;
35
46
  gid: number;
36
47
  egress: BridgeEgress;
37
- onLog: BridgeLogger;
48
+ onLog: BridgeLeveledLogger;
38
49
  }
39
50
 
40
51
  interface DownloadSource {
@@ -45,12 +56,12 @@ interface DownloadSource {
45
56
  const parseDigest = (digest: string) => {
46
57
  const [algo, expected] = digest.split(":");
47
58
 
48
- if (!algo || !expected || DIGEST_LENGTHS[algo] !== expected.length) {
59
+ if (!algo || !expected || !isDigestAlgorithm(algo) || DIGEST_LENGTHS[algo] !== expected.length) {
49
60
  throw new BridgePathError("invalid", `digest "${digest}" is not supported`);
50
61
  }
51
62
 
52
63
  return {
53
- algo: algo as "sha1" | "sha256" | "sha512",
64
+ algo,
54
65
  expected,
55
66
  };
56
67
  };
@@ -93,22 +104,87 @@ const directorySize = async (path: string, depth: number): Promise<number> => {
93
104
  return total;
94
105
  };
95
106
 
96
- const selectPrefix = (select: string | undefined) => {
97
- if (select === undefined) {
98
- return null;
107
+ const SUFFIX_PREFIX = "**/";
108
+
109
+ type ArchiveSelect =
110
+ | {
111
+ kind: "entry";
112
+ value: string;
113
+ }
114
+ | {
115
+ kind: "prefix";
116
+ value: string;
117
+ }
118
+ | {
119
+ kind: "suffix";
120
+ value: string;
121
+ };
122
+
123
+ const prefixSelect = (select: string): ArchiveSelect => {
124
+ const segments = toSegments(select);
125
+
126
+ return {
127
+ kind: "prefix",
128
+ value: segments.length === 0 ? "" : `${segments.join("/")}/`,
129
+ };
130
+ };
131
+
132
+ const entrySelect = (select: string): ArchiveSelect => {
133
+ return {
134
+ kind: "entry",
135
+ value: toSegments(select).join("/"),
136
+ };
137
+ };
138
+
139
+ const suffixSelect = (select: string): ArchiveSelect => {
140
+ return {
141
+ kind: "suffix",
142
+ value: toSegments(select.slice(SUFFIX_PREFIX.length)).join("/"),
143
+ };
144
+ };
145
+
146
+ const compileSelect = (select: string): ArchiveSelect => {
147
+ if (select.startsWith(SUFFIX_PREFIX)) {
148
+ return suffixSelect(select);
99
149
  }
100
150
 
101
- const segments = toSegments(select);
151
+ return select.endsWith("/") ? prefixSelect(select) : entrySelect(select);
152
+ };
153
+
154
+ const compileSelects = (select: string | undefined, selects: string[] | undefined) => {
155
+ const compiled: ArchiveSelect[] = [];
102
156
 
103
- return segments.length === 0 ? null : `${segments.join("/")}/`;
157
+ if (select !== undefined) {
158
+ compiled.push(prefixSelect(select));
159
+ }
160
+
161
+ for (const entry of selects ?? []) {
162
+ compiled.push(compileSelect(entry));
163
+ }
164
+
165
+ return compiled;
104
166
  };
105
167
 
106
- const selectedName = (name: string, prefix: string | null) => {
107
- if (prefix === null) {
168
+ const selectedName = (name: string, selects: ArchiveSelect[]) => {
169
+ if (selects.length === 0) {
108
170
  return name;
109
171
  }
110
172
 
111
- return name.startsWith(prefix) ? name.slice(prefix.length) : null;
173
+ for (const select of selects) {
174
+ if (select.kind === "prefix" && name.startsWith(select.value)) {
175
+ return name.slice(select.value.length);
176
+ }
177
+
178
+ if (select.kind === "entry" && name === select.value) {
179
+ return name;
180
+ }
181
+
182
+ if (select.kind === "suffix" && (name === select.value || name.endsWith(`/${select.value}`))) {
183
+ return select.value;
184
+ }
185
+ }
186
+
187
+ return null;
112
188
  };
113
189
 
114
190
  const treeSegments = (name: string) => {
@@ -207,6 +283,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
207
283
  const shared = await options.egress.download(remote.href, {
208
284
  digest: downloadOptions.digest,
209
285
  sizeBytes: downloadOptions.sizeBytes,
286
+ timeoutMs: downloadOptions.timeoutMs,
210
287
  });
211
288
 
212
289
  return {
@@ -215,7 +292,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
215
292
  };
216
293
  } catch (error) {
217
294
  if (error instanceof BridgeEgressCallError) {
218
- throw new BridgePathError("missing", `${remote.host} answered ${error.status ?? error.message}`);
295
+ throw new BridgeDownloadError(error.status, `${remote.host} answered ${error.status ?? error.message}`);
219
296
  }
220
297
 
221
298
  if (!(error instanceof BridgeEgressUnavailableError)) {
@@ -229,11 +306,11 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
229
306
  "user-agent": USER_AGENT,
230
307
  },
231
308
  redirect: "error",
232
- signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
309
+ signal: AbortSignal.timeout(downloadOptions.timeoutMs ?? DOWNLOAD_TIMEOUT_MS),
233
310
  });
234
311
 
235
312
  if (!response.ok || !response.body) {
236
- throw new BridgePathError("missing", `${remote.host} answered ${response.status}`);
313
+ throw new BridgeDownloadError(response.status, `${remote.host} answered ${response.status}`);
237
314
  }
238
315
 
239
316
  const declared = Number(response.headers.get("content-length"));
@@ -333,6 +410,22 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
333
410
  return resolved.isDirectory ? await directorySize(resolved.target, 0) : resolved.sizeBytes;
334
411
  },
335
412
 
413
+ async digest(path, algorithm) {
414
+ const resolved = await resolveWithin(options.root, path);
415
+
416
+ if (!resolved.exists || resolved.isDirectory) {
417
+ throw new BridgePathError("missing", `path "${path}" is not a readable file`);
418
+ }
419
+
420
+ const hasher = new Bun.CryptoHasher(algorithm);
421
+
422
+ for await (const chunk of Bun.file(resolved.target).stream()) {
423
+ hasher.update(chunk);
424
+ }
425
+
426
+ return hasher.digest("hex");
427
+ },
428
+
336
429
  async download(path, url, downloadOptions: BridgeDownloadOptions = {}) {
337
430
  const remote = new URL(url);
338
431
 
@@ -340,12 +433,15 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
340
433
  throw new BridgePathError("invalid", `${remote.host} is not https`);
341
434
  }
342
435
 
343
- const limit = downloadOptions.sizeBytes ?? DOWNLOAD_LIMIT;
436
+ const declared = downloadOptions.sizeBytes;
344
437
 
345
- if (limit > DOWNLOAD_LIMIT) {
346
- throw new BridgePathError("invalid", `download of ${limit} bytes exceeds the limit`);
438
+ if (declared !== undefined && declared > DOWNLOAD_LIMIT) {
439
+ throw new BridgePathError("invalid", `download of ${declared} bytes exceeds the limit`);
347
440
  }
348
441
 
442
+ const verified = downloadOptions.digest !== undefined;
443
+ const limit = verified ? DOWNLOAD_LIMIT : (declared ?? DOWNLOAD_LIMIT);
444
+
349
445
  const source = await openSource(remote, downloadOptions);
350
446
 
351
447
  await ensureParent(path);
@@ -357,11 +453,14 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
357
453
 
358
454
  const hasher = downloadOptions.digest ? new Bun.CryptoHasher(parseDigest(downloadOptions.digest).algo) : null;
359
455
  const writer = Bun.file(staging).writer();
456
+ const quiet = downloadOptions.quiet === true;
360
457
 
361
- options.onLog("downloading", {
362
- file: resolved.relative,
363
- sizeBytes: totalBytes,
364
- });
458
+ if (!quiet) {
459
+ options.onLog("downloading", {
460
+ file: resolved.relative,
461
+ sizeBytes: totalBytes,
462
+ });
463
+ }
365
464
 
366
465
  let readBytes = 0;
367
466
 
@@ -380,7 +479,7 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
380
479
 
381
480
  await writer.flush();
382
481
 
383
- if (totalBytes !== null) {
482
+ if (totalBytes !== null && !quiet) {
384
483
  const reached = Math.floor((readBytes / totalBytes) * 100);
385
484
 
386
485
  if (reached >= milestone + MILESTONE_STEP && reached < 100) {
@@ -396,11 +495,8 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
396
495
 
397
496
  await writer.end();
398
497
 
399
- if (downloadOptions.sizeBytes !== undefined && readBytes !== downloadOptions.sizeBytes) {
400
- throw new BridgePathError(
401
- "invalid",
402
- `${remote.host} returned ${readBytes} bytes, expected ${downloadOptions.sizeBytes}`,
403
- );
498
+ if (!verified && declared !== undefined && readBytes !== declared) {
499
+ throw new BridgePathError("invalid", `${remote.host} returned ${readBytes} bytes, expected ${declared}`);
404
500
  }
405
501
 
406
502
  if (hasher && downloadOptions.digest) {
@@ -410,6 +506,14 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
410
506
  throw new BridgePathError("invalid", `${remote.host} failed digest verification`);
411
507
  }
412
508
  }
509
+
510
+ if (verified && declared !== undefined && readBytes !== declared) {
511
+ options.onLog.warn("the source declared a different size, the checksum passed so we kept the file", {
512
+ file: resolved.relative,
513
+ sizeBytes: readBytes,
514
+ declaredBytes: declared,
515
+ });
516
+ }
413
517
  } catch (error) {
414
518
  writer.end();
415
519
 
@@ -423,10 +527,12 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
423
527
  await rename(staging, resolved.target);
424
528
  await own(resolved.target);
425
529
 
426
- options.onLog("downloaded", {
427
- file: resolved.relative,
428
- sizeBytes: readBytes,
429
- });
530
+ if (!quiet) {
531
+ options.onLog("downloaded", {
532
+ file: resolved.relative,
533
+ sizeBytes: readBytes,
534
+ });
535
+ }
430
536
  },
431
537
 
432
538
  async extract(path, destination, extractOptions: BridgeExtractOptions = {}) {
@@ -442,17 +548,18 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
442
548
 
443
549
  const tree = extractOptions.tree === true;
444
550
  const allowed = extractOptions.extensions?.map((extension) => extension.toLowerCase());
445
- const prefix = selectPrefix(extractOptions.select);
551
+ const selects = compileSelects(extractOptions.select, extractOptions.selects);
552
+ const excluded = compileGlobs(extractOptions.exclude ?? []);
446
553
  const base = toSegments(destination);
447
554
  const target = await ensureDirectory(destination);
448
- const buffer = Buffer.from(await Bun.file(resolved.target).arrayBuffer());
555
+ const archive = await openZip(resolved.target, tree ? ZIP_TREE_ENTRY_LIMIT : ZIP_ENTRY_LIMIT);
449
556
  const directories = new Set<string>();
450
557
  const written: string[] = [];
451
558
 
452
- for (const entry of readZip(buffer, tree ? ZIP_TREE_ENTRY_LIMIT : ZIP_ENTRY_LIMIT)) {
453
- const selected = selectedName(entry.name, prefix);
559
+ for (const entry of archive.entries) {
560
+ const selected = selectedName(entry.name, selects);
454
561
 
455
- if (selected === null) {
562
+ if (selected === null || matchesAny(selected, excluded)) {
456
563
  continue;
457
564
  }
458
565
 
@@ -495,13 +602,13 @@ export const createBridgeFiles = (options: BridgeFilesOptions): BridgeFiles => {
495
602
  ...segments,
496
603
  ].join("/");
497
604
 
498
- await Bun.write(`${target}/${segments.join("/")}`, entry.data);
605
+ await Bun.write(`${target}/${segments.join("/")}`, await archive.read(entry));
499
606
  await own(`${target}/${segments.join("/")}`);
500
607
 
501
608
  written.push(relative);
502
609
  }
503
610
 
504
- if (written.length === 0) {
611
+ if (written.length === 0 && extractOptions.selects === undefined) {
505
612
  throw new BridgePathError("invalid", `archive "${path}" contained no matching files`);
506
613
  }
507
614
 
@@ -0,0 +1,118 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BridgeDesired, BridgeHoldReason, BridgeRuntime } from "../core";
3
+ import { decideInstall, recountHold } from "./bridgeInstall";
4
+
5
+ describe("decideInstall", () => {
6
+ test("starts the game when the install returns nothing and the server wants to run", () => {
7
+ expect(decideInstall(undefined, BridgeDesired.Running)).toEqual({
8
+ hold: null,
9
+ runtime: BridgeRuntime.Starting,
10
+ start: true,
11
+ });
12
+ });
13
+
14
+ test("stays stopped when the install returns nothing and the server wants to stay down", () => {
15
+ expect(decideInstall(undefined, BridgeDesired.Stopped)).toEqual({
16
+ hold: null,
17
+ runtime: BridgeRuntime.Stopped,
18
+ start: false,
19
+ });
20
+ });
21
+
22
+ test("starts the game when the install answers with no hold", () => {
23
+ expect(
24
+ decideInstall(
25
+ {
26
+ hold: null,
27
+ },
28
+ BridgeDesired.Running,
29
+ ),
30
+ ).toEqual({
31
+ hold: null,
32
+ runtime: BridgeRuntime.Starting,
33
+ start: true,
34
+ });
35
+ });
36
+
37
+ test("holds the game stopped when the install answers with a hold", () => {
38
+ const hold = {
39
+ reason: BridgeHoldReason.PendingFiles,
40
+ count: 2,
41
+ };
42
+
43
+ expect(
44
+ decideInstall(
45
+ {
46
+ hold,
47
+ },
48
+ BridgeDesired.Running,
49
+ ),
50
+ ).toEqual({
51
+ hold,
52
+ runtime: BridgeRuntime.Stopped,
53
+ start: false,
54
+ });
55
+ });
56
+
57
+ test("keeps the hold when the server was already stopped", () => {
58
+ const hold = {
59
+ reason: BridgeHoldReason.PendingFiles,
60
+ count: 1,
61
+ };
62
+
63
+ expect(
64
+ decideInstall(
65
+ {
66
+ hold,
67
+ },
68
+ BridgeDesired.Stopped,
69
+ ),
70
+ ).toEqual({
71
+ hold,
72
+ runtime: BridgeRuntime.Stopped,
73
+ start: false,
74
+ });
75
+ });
76
+ });
77
+
78
+ describe("recountHold", () => {
79
+ test("leaves a driver without a hold alone", () => {
80
+ expect(recountHold(null, 3)).toBeNull();
81
+ });
82
+
83
+ test("moves the count of a pending-files hold to what the driver now lists", () => {
84
+ expect(
85
+ recountHold(
86
+ {
87
+ reason: BridgeHoldReason.PendingFiles,
88
+ count: 3,
89
+ },
90
+ 1,
91
+ ),
92
+ ).toEqual({
93
+ reason: BridgeHoldReason.PendingFiles,
94
+ count: 1,
95
+ });
96
+ });
97
+
98
+ test("keeps the same hold when the count did not move", () => {
99
+ const hold = {
100
+ reason: BridgeHoldReason.PendingFiles,
101
+ count: 2,
102
+ };
103
+
104
+ expect(recountHold(hold, 2)).toBe(hold);
105
+ });
106
+
107
+ test("clears the hold when nothing is pending any more", () => {
108
+ expect(
109
+ recountHold(
110
+ {
111
+ reason: BridgeHoldReason.PendingFiles,
112
+ count: 1,
113
+ },
114
+ 0,
115
+ ),
116
+ ).toBeNull();
117
+ });
118
+ });
@@ -0,0 +1,55 @@
1
+ import {
2
+ BridgeDesired,
3
+ BridgeHoldReason,
4
+ type BridgeInstallHold,
5
+ type BridgeInstallOutcome,
6
+ BridgeRuntime,
7
+ } from "../core";
8
+
9
+ export interface BridgeInstallDecision {
10
+ hold: BridgeInstallHold | null;
11
+ runtime: BridgeRuntime;
12
+ start: boolean;
13
+ }
14
+
15
+ export const decideInstall = (
16
+ outcome: BridgeInstallOutcome | undefined,
17
+ desired: BridgeDesired,
18
+ ): BridgeInstallDecision => {
19
+ const hold = outcome ? outcome.hold : null;
20
+
21
+ if (hold) {
22
+ return {
23
+ hold,
24
+ runtime: BridgeRuntime.Stopped,
25
+ start: false,
26
+ };
27
+ }
28
+
29
+ const running = desired === BridgeDesired.Running;
30
+
31
+ return {
32
+ hold: null,
33
+ runtime: running ? BridgeRuntime.Starting : BridgeRuntime.Stopped,
34
+ start: running,
35
+ };
36
+ };
37
+
38
+ export const recountHold = (hold: BridgeInstallHold | null, count: number): BridgeInstallHold | null => {
39
+ if (!hold || hold.reason !== BridgeHoldReason.PendingFiles) {
40
+ return hold;
41
+ }
42
+
43
+ if (count <= 0) {
44
+ return null;
45
+ }
46
+
47
+ if (hold.count === count) {
48
+ return hold;
49
+ }
50
+
51
+ return {
52
+ reason: hold.reason,
53
+ count,
54
+ };
55
+ };
@@ -1,5 +1,6 @@
1
1
  import type { BridgeNet, BridgeRequestOptions } from "../context";
2
2
  import { BridgeNetError } from "../core";
3
+ import { BridgeHttpMethod } from "../protocol";
3
4
  import { type BridgeEgress, BridgeEgressCallError, BridgeEgressUnavailableError } from "./bridgeEgress";
4
5
 
5
6
  const REQUEST_TIMEOUT_MS = 15_000;
@@ -10,6 +11,10 @@ export const USER_AGENT = "serverk-bridge/1.0 (serverk.gg)";
10
11
 
11
12
  const LINE_BREAK = /[\r\n]/;
12
13
 
14
+ const hasHeader = (headers: Record<string, string> | undefined, name: string) => {
15
+ return Object.keys(headers ?? {}).some((key) => key.toLowerCase() === name);
16
+ };
17
+
13
18
  export const safeHeaders = (headers: Record<string, string> = {}) => {
14
19
  const checked: Record<string, string> = {};
15
20
 
@@ -54,12 +59,24 @@ const readCapped = async (response: Response, host: string) => {
54
59
  };
55
60
 
56
61
  export const createBridgeNet = (egress: BridgeEgress): BridgeNet => {
57
- const direct = async (remote: URL, headers: Record<string, string>, timeoutMs: number) => {
62
+ const direct = async (
63
+ remote: URL,
64
+ headers: Record<string, string>,
65
+ timeoutMs: number,
66
+ httpMethod: BridgeHttpMethod,
67
+ body: string | null,
68
+ ) => {
58
69
  const response = await fetch(remote, {
70
+ method: httpMethod,
59
71
  headers: {
60
72
  ...headers,
61
73
  "user-agent": USER_AGENT,
62
74
  },
75
+ ...(body === null
76
+ ? {}
77
+ : {
78
+ body,
79
+ }),
63
80
  redirect: "error",
64
81
  signal: AbortSignal.timeout(timeoutMs),
65
82
  });
@@ -78,11 +95,23 @@ export const createBridgeNet = (egress: BridgeEgress): BridgeNet => {
78
95
  throw new BridgeNetError(null, `${remote.host} is not https`);
79
96
  }
80
97
 
81
- const headers = safeHeaders(options.headers);
98
+ const httpMethod = options.httpMethod ?? BridgeHttpMethod.Get;
99
+ const body = httpMethod === BridgeHttpMethod.Post ? (options.body ?? null) : null;
100
+
101
+ const headers = safeHeaders({
102
+ ...(body !== null && !hasHeader(options.headers, "content-type")
103
+ ? {
104
+ "content-type": "application/json",
105
+ }
106
+ : {}),
107
+ ...options.headers,
108
+ });
82
109
 
83
110
  try {
84
111
  const result = await egress.fetch(url, {
85
112
  ...options,
113
+ httpMethod,
114
+ body: body ?? undefined,
86
115
  headers,
87
116
  });
88
117
 
@@ -97,7 +126,7 @@ export const createBridgeNet = (egress: BridgeEgress): BridgeNet => {
97
126
  }
98
127
  }
99
128
 
100
- return await direct(remote, headers, options.timeoutMs ?? REQUEST_TIMEOUT_MS);
129
+ return await direct(remote, headers, options.timeoutMs ?? REQUEST_TIMEOUT_MS, httpMethod, body);
101
130
  };
102
131
 
103
132
  return {