@fedify/vocab-runtime 2.4.5-dev.2462 → 2.4.5

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 (120) hide show
  1. package/deno.json +5 -2
  2. package/dist/{docloader-BMHljyYb.d.cts → docloader-DOWjnNCz.d.cts} +4 -8
  3. package/dist/{docloader-BMHljyYb.d.ts → docloader-DOWjnNCz.d.ts} +4 -8
  4. package/dist/internal/jsonld-cache.cjs +1 -1
  5. package/dist/internal/jsonld-cache.d.cts +1 -1
  6. package/dist/internal/jsonld-cache.d.ts +1 -1
  7. package/dist/internal/jsonld-cache.js +1 -1
  8. package/dist/internal/portable-dereference.cjs +1 -1
  9. package/dist/internal/portable-dereference.d.cts +2 -2
  10. package/dist/internal/portable-dereference.d.ts +2 -2
  11. package/dist/internal/portable-dereference.js +1 -1
  12. package/dist/internal.cjs +6 -0
  13. package/dist/internal.d.cts +108 -0
  14. package/dist/internal.d.ts +108 -0
  15. package/dist/internal.js +3 -0
  16. package/dist/mod.cjs +104 -38
  17. package/dist/mod.d.cts +3 -336
  18. package/dist/mod.d.ts +4 -336
  19. package/dist/mod.js +104 -38
  20. package/dist/{portable-6Aj-7dZJ.d.cts → portable-CTnZWX7J.d.cts} +1 -1
  21. package/dist/{portable-nkTiqH0w.d.ts → portable-Cq-rhQgY.d.ts} +1 -1
  22. package/dist/public-fetch-Dvfvawei.cjs +527 -0
  23. package/dist/public-fetch-ziZ39L5I.js +510 -0
  24. package/dist/tests/{body-Dlnsb-QX.cjs → body-BO79kuv1.cjs} +1 -1
  25. package/dist/tests/{body-BXrFIcFa.mjs → body-CUzCsDZ4.mjs} +1 -1
  26. package/dist/tests/body.test.cjs +1 -1
  27. package/dist/tests/body.test.mjs +1 -1
  28. package/dist/tests/decimal.test.cjs +3 -3
  29. package/dist/tests/decimal.test.mjs +3 -3
  30. package/dist/tests/{docloader-c9BQWaF1.cjs → docloader-B79_YgeY.cjs} +111 -39
  31. package/dist/tests/{docloader-DsuF8YDf.mjs → docloader-C5ZHGEfO.mjs} +106 -40
  32. package/dist/tests/docloader.test.cjs +531 -1293
  33. package/dist/tests/docloader.test.mjs +472 -1252
  34. package/dist/tests/esm-Ds-eHTfY.mjs +1229 -0
  35. package/dist/tests/esm-Q2tdh5f3.cjs +1215 -0
  36. package/dist/tests/internal/portable-dereference.test.cjs +3 -3
  37. package/dist/tests/internal/portable-dereference.test.mjs +3 -3
  38. package/dist/tests/{jsonld-cache-BEXiiM-V.cjs → jsonld-cache-DKIWKxEX.cjs} +1 -1
  39. package/dist/tests/{jsonld-cache-DFnWsLuZ.mjs → jsonld-cache-KH40z9_7.mjs} +1 -1
  40. package/dist/tests/jsonld-cache.test.cjs +2 -2
  41. package/dist/tests/jsonld-cache.test.mjs +2 -2
  42. package/dist/tests/node-BvS3AhZ2.cjs +89 -0
  43. package/dist/tests/node-Cg4pRuYy.mjs +84 -0
  44. package/dist/tests/pin-grant-CPrpk1l4.cjs +70 -0
  45. package/dist/tests/pin-grant-H5XhM1Xs.mjs +53 -0
  46. package/dist/tests/pin-grant.test.cjs +87 -0
  47. package/dist/tests/pin-grant.test.d.cts +1 -0
  48. package/dist/tests/pin-grant.test.d.mts +1 -0
  49. package/dist/tests/pin-grant.test.mjs +88 -0
  50. package/dist/tests/pinners/bun.test.cjs +166 -0
  51. package/dist/tests/pinners/bun.test.d.cts +1 -0
  52. package/dist/tests/pinners/bun.test.d.mts +1 -0
  53. package/dist/tests/pinners/bun.test.mjs +167 -0
  54. package/dist/tests/pinners/node.test.cjs +269 -0
  55. package/dist/tests/pinners/node.test.d.cts +1 -0
  56. package/dist/tests/pinners/node.test.d.mts +1 -0
  57. package/dist/tests/pinners/node.test.mjs +268 -0
  58. package/dist/tests/pinners/pinners.test.cjs +196 -0
  59. package/dist/tests/pinners/pinners.test.d.cts +1 -0
  60. package/dist/tests/pinners/pinners.test.d.mts +1 -0
  61. package/dist/tests/pinners/pinners.test.mjs +197 -0
  62. package/dist/tests/pinners/testdata/node-child.cjs +135 -0
  63. package/dist/tests/pinners/testdata/node-child.d.cts +1 -0
  64. package/dist/tests/pinners/testdata/node-child.d.mts +1 -0
  65. package/dist/tests/pinners/testdata/node-child.mjs +134 -0
  66. package/dist/tests/pinners-CJi9ZkYs.cjs +220 -0
  67. package/dist/tests/pinners-zSofbiy2.mjs +203 -0
  68. package/dist/tests/{portable-dereference-DmTYDy2p.mjs → portable-dereference-BHRE5Pkm.mjs} +2 -2
  69. package/dist/tests/{portable-dereference-qTLMqe2_.cjs → portable-dereference-mRHrC7PI.cjs} +2 -2
  70. package/dist/tests/{portable-media-M7idvaUq.mjs → portable-media-BpVpe1gj.mjs} +3 -3
  71. package/dist/tests/{portable-media-CS4czG6X.cjs → portable-media-C3yY0W5E.cjs} +3 -3
  72. package/dist/tests/portable-media.test.cjs +1 -1
  73. package/dist/tests/portable-media.test.mjs +1 -1
  74. package/dist/tests/portable-workers.test.cjs +2 -2
  75. package/dist/tests/portable-workers.test.mjs +2 -2
  76. package/dist/tests/public-fetch-DNkPoks1.mjs +170 -0
  77. package/dist/tests/public-fetch-U822jbKB.cjs +186 -0
  78. package/dist/tests/public-fetch.test.cjs +1073 -0
  79. package/dist/tests/public-fetch.test.d.cts +1 -0
  80. package/dist/tests/public-fetch.test.d.mts +1 -0
  81. package/dist/tests/public-fetch.test.mjs +1074 -0
  82. package/dist/tests/{request-C336eT6A.mjs → request-3AP-HBJ6.mjs} +1 -1
  83. package/dist/tests/{request-CgTgsBUA.cjs → request-BhopJTZd.cjs} +1 -1
  84. package/dist/tests/request.test.cjs +1 -1
  85. package/dist/tests/request.test.mjs +1 -1
  86. package/dist/tests/support-BmzI3dRJ.cjs +363 -0
  87. package/dist/tests/support-CiHDVJgh.mjs +272 -0
  88. package/dist/tests/{url-BNakuZ8k.cjs → url-Bdj-rTcC.cjs} +88 -12
  89. package/dist/tests/{url-DMxmp7ZG.mjs → url-COj5Q8mb.mjs} +65 -13
  90. package/dist/tests/url.test.cjs +4 -1
  91. package/dist/tests/url.test.mjs +4 -1
  92. package/dist/url-4EE56sNP.d.cts +352 -0
  93. package/dist/url-4EE56sNP.d.ts +352 -0
  94. package/dist/{url-DrGTR8yv.cjs → url-DIKZnaJv.cjs} +88 -12
  95. package/dist/{url-Dzyp-NsC.js → url-mWveR5GA.js} +65 -13
  96. package/package.json +14 -3
  97. package/src/docloader.test.ts +696 -48
  98. package/src/docloader.ts +186 -75
  99. package/src/internal.ts +13 -0
  100. package/src/pin-grant.test.ts +118 -0
  101. package/src/pin-grant.ts +99 -0
  102. package/src/pinners/bun.test.ts +278 -0
  103. package/src/pinners/bun.ts +223 -0
  104. package/src/pinners/mod.ts +59 -0
  105. package/src/pinners/node.test.ts +415 -0
  106. package/src/pinners/node.ts +144 -0
  107. package/src/pinners/pinners.test.ts +316 -0
  108. package/src/pinners/testdata/node-child.ts +147 -0
  109. package/src/pinners/testdata/proxy-child.ts +82 -0
  110. package/src/pinners/testdata/support.ts +343 -0
  111. package/src/pinners/testdata/tls-cert.pem +12 -0
  112. package/src/pinners/testdata/tls-child.ts +88 -0
  113. package/src/pinners/testdata/tls-key.pem +5 -0
  114. package/src/pinners/types.ts +48 -0
  115. package/src/public-fetch.test.ts +1286 -0
  116. package/src/public-fetch.ts +338 -0
  117. package/src/testdata/public-fetch-deno-worker.ts +74 -0
  118. package/src/url.test.ts +11 -0
  119. package/src/url.ts +113 -22
  120. package/tsdown.config.ts +7 -2
@@ -0,0 +1,316 @@
1
+ import { test } from "@fedify/fixture";
2
+ import { deepStrictEqual, ok, rejects } from "node:assert";
3
+ import { createServer } from "node:http";
4
+ import { createServer as createTcpServer } from "node:net";
5
+ import { issuePinGrant, type PinGrant, revokePinGrant } from "../pin-grant.ts";
6
+ import { assertPublicAddress, UrlError } from "../url.ts";
7
+ import { createBunPinner } from "./bun.ts";
8
+ import { createNodePinner } from "./node.ts";
9
+ import {
10
+ allowAll,
11
+ closeServer,
12
+ getLoopbackSupport,
13
+ ipv4,
14
+ ipv6,
15
+ isAddressRejection,
16
+ isBun,
17
+ isNode,
18
+ listen,
19
+ startRecordingServer,
20
+ } from "./testdata/support.ts";
21
+ import type { ConnectionPinner, ConnectionPinnerOptions } from "./types.ts";
22
+
23
+ interface PinnerUnderTest {
24
+ readonly name: string;
25
+ /** Whether real connections can be tested on the current runtime. */
26
+ readonly connects: boolean;
27
+ /**
28
+ * Whether addresses are checked before the request is handed over to
29
+ * the runtime's `fetch()`, so that the checks can be tested on any runtime.
30
+ */
31
+ readonly checksBeforeFetch: boolean;
32
+ readonly create: (
33
+ options: ConnectionPinnerOptions,
34
+ ) => Promise<ConnectionPinner>;
35
+ }
36
+
37
+ /**
38
+ * The pinners of all runtimes. The behaviors they share are tested here, and
39
+ * the ones specific to each runtime in *bun.test.ts* and *node.test.ts*.
40
+ */
41
+ const pinners: readonly PinnerUnderTest[] = [
42
+ {
43
+ name: "createBunPinner()",
44
+ connects: isBun,
45
+ checksBeforeFetch: true,
46
+ create: (options) => Promise.resolve(createBunPinner(options)),
47
+ },
48
+ {
49
+ name: "createNodePinner()",
50
+ connects: isNode,
51
+ // The addresses are checked in the lookup of the undici agent:
52
+ checksBeforeFetch: false,
53
+ create: async (options) =>
54
+ createNodePinner({ ...options, undici: await import("undici") }),
55
+ },
56
+ ];
57
+
58
+ for (const { name, connects, checksBeforeFetch, create } of pinners) {
59
+ test(`${name} supports only HTTP and HTTPS`, async () => {
60
+ const pinner = await create(allowAll);
61
+ ok(pinner.supports(new URL("http://pinned-host.invalid/")));
62
+ ok(pinner.supports(new URL("https://pinned-host.invalid/")));
63
+ ok(!pinner.supports(new URL("ftp://pinned-host.invalid/")));
64
+ ok(!pinner.supports(new URL("ws://pinned-host.invalid/")));
65
+ });
66
+
67
+ test(`${name} checks addresses right before connecting`, {
68
+ ignore: !connects && !checksBeforeFetch,
69
+ }, async () => {
70
+ const { server, port, requests } = await startRecordingServer();
71
+ try {
72
+ // The production check, with a grant for a private address that
73
+ // openPublicFetch() would never issue:
74
+ const pinner = await create({ verifyAddress: assertPublicAddress });
75
+ const url = new URL(`http://pinned-host.invalid:${port}/`);
76
+ const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
77
+ for (const method of ["GET", "POST"]) {
78
+ await rejects(
79
+ () => pinner.fetch(grant, new Request(url, { method })),
80
+ isAddressRejection,
81
+ );
82
+ }
83
+ deepStrictEqual(requests, []);
84
+ } finally {
85
+ await closeServer(server);
86
+ }
87
+ });
88
+
89
+ test(`${name} does not fall back past a failed check`, {
90
+ ignore: !connects && !checksBeforeFetch,
91
+ }, async () => {
92
+ const { server, port, requests } = await startRecordingServer();
93
+ try {
94
+ const checked: string[] = [];
95
+ const pinner = await create({
96
+ verifyAddress({ address }) {
97
+ checked.push(address);
98
+ if (address !== "127.0.0.1") {
99
+ throw new UrlError(`Invalid or private address: ${address}`);
100
+ }
101
+ },
102
+ });
103
+ // The second address would reach the server, but the first one fails
104
+ // the check, and is never connected to (192.0.2.0/24 is TEST-NET-1):
105
+ const url = new URL(`http://pinned-host.invalid:${port}/`);
106
+ const grant = issuePinGrant(url, [ipv4("192.0.2.1"), ipv4("127.0.0.1")]);
107
+ await rejects(
108
+ () => pinner.fetch(grant, new Request(url)),
109
+ isAddressRejection,
110
+ );
111
+ deepStrictEqual(checked, ["192.0.2.1"]);
112
+ deepStrictEqual(requests, []);
113
+ } finally {
114
+ await closeServer(server);
115
+ }
116
+ });
117
+
118
+ test(`${name} redeems the grant for every request`, async () => {
119
+ const { server, port, requests } = await startRecordingServer();
120
+ try {
121
+ const pinner = await create(allowAll);
122
+ const url = new URL(`http://pinned-host.invalid:${port}/`);
123
+ const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
124
+ // Other origins:
125
+ for (
126
+ const other of [
127
+ `http://other-host.invalid:${port}/`,
128
+ `https://pinned-host.invalid:${port}/`,
129
+ `http://pinned-host.invalid:${port + 1}/`,
130
+ ]
131
+ ) {
132
+ await rejects(() => pinner.fetch(grant, new Request(other)), TypeError);
133
+ }
134
+ // Forged grants:
135
+ for (const forged of [{}, Object.freeze(Object.create(null))]) {
136
+ await rejects(
137
+ () => pinner.fetch(forged as PinGrant, new Request(url)),
138
+ TypeError,
139
+ );
140
+ }
141
+ // Revoked grants:
142
+ revokePinGrant(grant);
143
+ await rejects(() => pinner.fetch(grant, new Request(url)), TypeError);
144
+ deepStrictEqual(requests, []);
145
+ } finally {
146
+ await closeServer(server);
147
+ }
148
+ });
149
+
150
+ test(`${name} connects to the pinned address`, {
151
+ ignore: !connects,
152
+ }, async () => {
153
+ const { server, port, requests } = await startRecordingServer();
154
+ try {
155
+ const pinner = await create(allowAll);
156
+ // The .invalid TLD never resolves, so a response proves that the
157
+ // connection went to the pinned address without a DNS lookup:
158
+ const url = new URL(`http://pinned-host.invalid:${port}/path?query`);
159
+ const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
160
+ const response = await pinner.fetch(
161
+ grant,
162
+ new Request(url, { headers: { "X-Test": "header" } }),
163
+ );
164
+ deepStrictEqual(response.status, 200);
165
+ deepStrictEqual(await response.text(), "pinned");
166
+ const post = await pinner.fetch(
167
+ grant,
168
+ new Request(url, { method: "POST", body: "payload" }),
169
+ );
170
+ deepStrictEqual(await post.text(), "pinned");
171
+ deepStrictEqual(
172
+ requests.map(({ method, url, host, body }) => ({
173
+ method,
174
+ url,
175
+ host,
176
+ body,
177
+ })),
178
+ [
179
+ {
180
+ method: "GET",
181
+ url: "/path?query",
182
+ host: `pinned-host.invalid:${port}`,
183
+ body: "",
184
+ },
185
+ {
186
+ method: "POST",
187
+ url: "/path?query",
188
+ host: `pinned-host.invalid:${port}`,
189
+ body: "payload",
190
+ },
191
+ ],
192
+ );
193
+ } finally {
194
+ await closeServer(server);
195
+ }
196
+ });
197
+
198
+ test(`${name} does not follow redirects`, {
199
+ ignore: !connects,
200
+ }, async () => {
201
+ const { server, port, requests } = await startRecordingServer(
202
+ "127.0.0.1",
203
+ (_, res) => {
204
+ res.writeHead(302, { Location: "/target" });
205
+ res.end();
206
+ },
207
+ );
208
+ try {
209
+ const pinner = await create(allowAll);
210
+ const url = new URL(`http://pinned-host.invalid:${port}/`);
211
+ const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
212
+ const response = await pinner.fetch(
213
+ grant,
214
+ new Request(url, { redirect: "follow" }),
215
+ );
216
+ deepStrictEqual(response.status, 302);
217
+ deepStrictEqual(response.headers.get("Location"), "/target");
218
+ deepStrictEqual(requests.length, 1);
219
+ } finally {
220
+ await closeServer(server);
221
+ }
222
+ });
223
+
224
+ test(`${name} aborts requests on either signal`, {
225
+ ignore: !connects,
226
+ }, async () => {
227
+ // The server never responds, so only an abort ends the request:
228
+ let notifyReceived = () => {};
229
+ const server = createServer(() => notifyReceived());
230
+ const port = await listen(server);
231
+ try {
232
+ const pinner = await create(allowAll);
233
+ const url = new URL(`http://pinned-host.invalid:${port}/`);
234
+ const grant = issuePinGrant(url, [ipv4("127.0.0.1")]);
235
+ for (const which of ["request", "separate"] as const) {
236
+ const requestController = new AbortController();
237
+ const separateController = new AbortController();
238
+ const received = new Promise<void>((resolve) =>
239
+ notifyReceived = resolve
240
+ );
241
+ const response = pinner.fetch(
242
+ grant,
243
+ new Request(url, { signal: requestController.signal }),
244
+ separateController.signal,
245
+ );
246
+ await received;
247
+ (which === "request" ? requestController : separateController).abort();
248
+ await rejects(response, { name: "AbortError" }, which);
249
+ }
250
+ } finally {
251
+ await closeServer(server);
252
+ }
253
+ });
254
+
255
+ test(`${name} does not deliver a POST request twice`, {
256
+ ignore: !connects,
257
+ }, async () => {
258
+ // Skipped where a server cannot listen on both loopback addresses:
259
+ if (!(await getLoopbackSupport()).dualStack) return;
260
+ // The server receives the whole request, and then drops the connection
261
+ // without responding, as if it crashed right after processing it.
262
+ // It parses requests by itself since the `node:http` server of older Bun
263
+ // versions (e.g., 1.2.0) responds instead of dropping the connection:
264
+ let received = 0;
265
+ const server = createTcpServer((socket) => {
266
+ let data = "";
267
+ socket.setEncoding("latin1");
268
+ socket.on("error", () => {});
269
+ socket.on("data", (chunk: string) => {
270
+ data += chunk;
271
+ const headEnd = data.indexOf("\r\n\r\n");
272
+ if (headEnd < 0) return;
273
+ const length = /^content-length:\s*(\d+)/im.exec(
274
+ data.slice(0, headEnd),
275
+ )?.[1];
276
+ const complete = length == null
277
+ ? data.endsWith("\r\n0\r\n\r\n")
278
+ : data.length - headEnd - 4 >= Number(length);
279
+ if (!complete) return;
280
+ received++;
281
+ socket.destroy();
282
+ });
283
+ });
284
+ const port = await listen(server, "::");
285
+ try {
286
+ const pinner = await create(allowAll);
287
+ const url = new URL(`http://pinned-host.invalid:${port}/inbox`);
288
+ const grant = issuePinGrant(url, [ipv4("127.0.0.1"), ipv6("::1")]);
289
+ await rejects(() =>
290
+ pinner.fetch(
291
+ grant,
292
+ new Request(url, { method: "POST", body: "activity" }),
293
+ )
294
+ );
295
+ deepStrictEqual(received, 1);
296
+ // Control: the second address reaches the same server, so falling back
297
+ // would have delivered the activity twice. (Whether a GET request
298
+ // falls back here depends on the runtime: Bun 1.4 raises this
299
+ // ECONNRESET as a TypeError, but Bun 1.2 does not.) The first grant is
300
+ // released, since the Node.js pinner connects to the addresses of all
301
+ // live grants for the hostname:
302
+ revokePinGrant(grant);
303
+ await pinner.release(grant);
304
+ const second = issuePinGrant(url, [ipv6("::1")]);
305
+ await rejects(() =>
306
+ pinner.fetch(
307
+ second,
308
+ new Request(url, { method: "POST", body: "activity" }),
309
+ )
310
+ );
311
+ deepStrictEqual(received, 2);
312
+ } finally {
313
+ await closeServer(server);
314
+ }
315
+ });
316
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * A child process for the regression tests of the Node.js pinner in
3
+ * *../node.test.ts* that depend on the environment at startup: Node.js reads
4
+ * proxy environment variables (with `NODE_USE_ENV_PROXY=1`) and
5
+ * `NODE_EXTRA_CA_CERTS` only at startup.
6
+ *
7
+ * Node.js cannot run TypeScript sources without flags before 22.6.0, so
8
+ * *../../../tsdown.config.ts* compiles this script into
9
+ * *dist/tests/pinners/testdata/node-child.mjs*, which the tests run. It has
10
+ * no top-level `await`, so that it can be compiled to CommonJS as well.
11
+ *
12
+ * Usage:
13
+ *
14
+ * - `node node-child.mjs proxy <http-port> <https-port>`: requests
15
+ * `http://pinned-host.invalid:<http-port>/pinned` and
16
+ * `https://pinned-host.invalid:<https-port>/pinned` through a pinner
17
+ * that allows loopback addresses, with both pinned to 127.0.0.1, and then
18
+ * `http://127.0.0.1:<http-port>/control` with the global `fetch()` as
19
+ * a control.
20
+ * - `node node-child.mjs tls <https-port>`: requests
21
+ * `https://<hostname>:<https-port>/<case>` through a pinner that allows
22
+ * loopback addresses, with every hostname pinned to 127.0.0.1, in the same
23
+ * order as *tls-child.ts* does for Bun.
24
+ *
25
+ * It prints the results as a JSON line.
26
+ * @module
27
+ */
28
+ import process from "node:process";
29
+ import { issuePinGrant } from "../../pin-grant.ts";
30
+ import { createNodePinner } from "../node.ts";
31
+ import type { ConnectionPinner } from "../types.ts";
32
+
33
+ type Result =
34
+ | { readonly status: number; readonly body: string }
35
+ | { readonly error: string };
36
+
37
+ const loopback = [{ address: "127.0.0.1", family: 4 }] as const;
38
+
39
+ /**
40
+ * Describes an error by its code, or by the code of its cause, since
41
+ * the `fetch()` of Node.js rejects with a `TypeError` caused by the actual
42
+ * error.
43
+ */
44
+ function describeError(error: unknown): string {
45
+ for (let e = error; typeof e === "object" && e !== null;) {
46
+ const { code, cause } = e as { code?: unknown; cause?: unknown };
47
+ if (typeof code === "string") return code;
48
+ e = cause;
49
+ }
50
+ return String(error);
51
+ }
52
+
53
+ async function attempt(fn: () => Promise<Response>): Promise<Result> {
54
+ try {
55
+ const response = await fn();
56
+ return { status: response.status, body: await response.text() };
57
+ } catch (error) {
58
+ return { error: describeError(error) };
59
+ }
60
+ }
61
+
62
+ function fetchPinned(
63
+ pinner: ConnectionPinner,
64
+ url: URL,
65
+ withSignals = false,
66
+ ): Promise<Result> {
67
+ const grant = issuePinGrant(url, loopback);
68
+ return attempt(() =>
69
+ withSignals
70
+ ? pinner.fetch(
71
+ grant,
72
+ new Request(url, { signal: new AbortController().signal }),
73
+ new AbortController().signal,
74
+ )
75
+ : pinner.fetch(grant, new Request(url))
76
+ );
77
+ }
78
+
79
+ async function proxy(
80
+ pinner: ConnectionPinner,
81
+ httpPort: number,
82
+ httpsPort: number,
83
+ ): Promise<unknown> {
84
+ const http = await fetchPinned(
85
+ pinner,
86
+ new URL(`http://pinned-host.invalid:${httpPort}/pinned`),
87
+ );
88
+ const https = await fetchPinned(
89
+ pinner,
90
+ new URL(`https://pinned-host.invalid:${httpsPort}/pinned`),
91
+ );
92
+ // Without a dispatcher, so that it shows whether the proxy is in effect:
93
+ const control = await attempt(() =>
94
+ fetch(`http://127.0.0.1:${httpPort}/control`, {
95
+ redirect: "manual",
96
+ signal: AbortSignal.timeout(10_000),
97
+ })
98
+ );
99
+ return { version: process.version, http, https, control };
100
+ }
101
+
102
+ async function tls(pinner: ConnectionPinner, port: number): Promise<unknown> {
103
+ const results: ({ readonly case: string } & Result)[] = [];
104
+ for (
105
+ const [name, hostname, withSignals] of [
106
+ ["pinned", "pinned-host.invalid", false],
107
+ ["unlisted", "unlisted-host.invalid", false],
108
+ ["unlisted-again", "unlisted-host.invalid", false],
109
+ ["alternate", "alternate-host.invalid", false],
110
+ ["signals", "pinned-host.invalid", true],
111
+ ] as const
112
+ ) {
113
+ const url = new URL(`https://${hostname}:${port}/${name}`);
114
+ results.push({
115
+ case: name,
116
+ ...await fetchPinned(pinner, url, withSignals),
117
+ });
118
+ }
119
+ return { version: process.version, results };
120
+ }
121
+
122
+ async function main(): Promise<unknown> {
123
+ const [mode, ...ports] = process.argv.slice(2);
124
+ const [first, second] = ports.map(Number);
125
+ const pinner = createNodePinner({
126
+ verifyAddress() {},
127
+ undici: await import("undici"),
128
+ });
129
+ if (mode === "proxy" && Number.isInteger(first) && Number.isInteger(second)) {
130
+ return await proxy(pinner, first, second);
131
+ } else if (mode === "tls" && Number.isInteger(first)) {
132
+ return await tls(pinner, first);
133
+ }
134
+ throw new TypeError(
135
+ "Usage: node-child.mjs proxy <http-port> <https-port> | " +
136
+ "node-child.mjs tls <https-port>",
137
+ );
138
+ }
139
+
140
+ main().then(
141
+ (output) =>
142
+ process.stdout.write(`${JSON.stringify(output)}\n`, () => process.exit(0)),
143
+ (error) => {
144
+ console.error(error);
145
+ process.exit(1);
146
+ },
147
+ );
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A child process for the environment proxy regression test of the Bun
3
+ * pinner in *../bun.test.ts*. Bun reads proxy environment variables only at
4
+ * startup in older versions, so the test runs this script in a new Bun
5
+ * process with the variables set.
6
+ *
7
+ * Usage: `bun proxy-child.ts <http-port> <https-port>`
8
+ *
9
+ * It requests `http://pinned-host.invalid:<http-port>/pinned` and
10
+ * `https://pinned-host.invalid:<https-port>/pinned` through a pinner that
11
+ * allows loopback addresses, with both pinned to 127.0.0.1, and then
12
+ * `http://127.0.0.1:<http-port>/control` with the global `fetch()` as a
13
+ * control. It prints the results as a JSON line.
14
+ * @module
15
+ */
16
+ import { configure, type LogRecord } from "@logtape/logtape";
17
+ import process from "node:process";
18
+ import { issuePinGrant } from "../../pin-grant.ts";
19
+ import { createBunPinner } from "../bun.ts";
20
+
21
+ type Result =
22
+ | { readonly status: number; readonly body: string }
23
+ | { readonly error: string };
24
+
25
+ async function attempt(fn: () => Promise<Response>): Promise<Result> {
26
+ try {
27
+ const response = await fn();
28
+ return { status: response.status, body: await response.text() };
29
+ } catch (error) {
30
+ const code = (error as { code?: unknown } | null)?.code;
31
+ return { error: typeof code === "string" ? code : String(error) };
32
+ }
33
+ }
34
+
35
+ const [httpPort, httpsPort] = process.argv.slice(2).map(Number);
36
+ if (!Number.isInteger(httpPort) || !Number.isInteger(httpsPort)) {
37
+ throw new TypeError("Usage: proxy-child.ts <http-port> <https-port>");
38
+ }
39
+
40
+ const warnings: LogRecord[] = [];
41
+ await configure({
42
+ sinks: { buffer: (record) => warnings.push(record) },
43
+ loggers: [
44
+ {
45
+ category: ["fedify", "runtime", "fetch"],
46
+ sinks: ["buffer"],
47
+ lowestLevel: "warning",
48
+ },
49
+ { category: ["logtape", "meta"], sinks: [] },
50
+ ],
51
+ });
52
+
53
+ const pinner = createBunPinner({ verifyAddress() {} });
54
+ const loopback = [{ address: "127.0.0.1", family: 4 }] as const;
55
+
56
+ async function fetchPinned(url: URL): Promise<Result> {
57
+ const grant = issuePinGrant(url, loopback);
58
+ return await attempt(() => pinner.fetch(grant, new Request(url)));
59
+ }
60
+
61
+ const http = await fetchPinned(
62
+ new URL(`http://pinned-host.invalid:${httpPort}/pinned`),
63
+ );
64
+ const https = await fetchPinned(
65
+ new URL(`https://pinned-host.invalid:${httpsPort}/pinned`),
66
+ );
67
+ // Without proxy: false, so that it shows the proxy is in effect:
68
+ const control = await attempt(() =>
69
+ fetch(`http://127.0.0.1:${httpPort}/control`, { redirect: "manual" })
70
+ );
71
+
72
+ const output = JSON.stringify({
73
+ version: (globalThis as { Bun?: { version: string } }).Bun?.version,
74
+ http,
75
+ https,
76
+ control,
77
+ warnings: warnings.map((record) => record.rawMessage),
78
+ });
79
+ await new Promise<void>((resolve) =>
80
+ process.stdout.write(`${output}\n`, () => resolve())
81
+ );
82
+ process.exit(0);