@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,343 @@
1
+ /**
2
+ * Helpers shared by the pinner tests in *../\*.test.ts*. They start local
3
+ * servers and proxies, and run child processes.
4
+ * @module
5
+ */
6
+ import { execFile } from "node:child_process";
7
+ import { existsSync } from "node:fs";
8
+ import {
9
+ createServer,
10
+ type IncomingMessage,
11
+ type Server,
12
+ type ServerResponse,
13
+ } from "node:http";
14
+ import {
15
+ type AddressInfo,
16
+ connect,
17
+ createServer as createTcpServer,
18
+ type Server as TcpServer,
19
+ } from "node:net";
20
+ import { dirname, join } from "node:path";
21
+ import process from "node:process";
22
+ import { fileURLToPath } from "node:url";
23
+ import { type ResolvedAddress, UrlError } from "../../url.ts";
24
+ import { detectRuntime } from "../mod.ts";
25
+ import type { ConnectionPinnerOptions } from "../types.ts";
26
+
27
+ export const isBun: boolean = detectRuntime() === "bun";
28
+ export const isNode: boolean = detectRuntime() === "node";
29
+
30
+ /** Pinner options that allow connecting to any address, e.g., loopback. */
31
+ export const allowAll: ConnectionPinnerOptions = { verifyAddress() {} };
32
+
33
+ export const ipv4 = (address: string): ResolvedAddress => ({
34
+ address,
35
+ family: 4,
36
+ });
37
+ export const ipv6 = (address: string): ResolvedAddress => ({
38
+ address,
39
+ family: 6,
40
+ });
41
+
42
+ /**
43
+ * Whether the error is the rejection of an address by `verifyAddress`.
44
+ * The Bun pinner throws the `UrlError` itself, and the Node.js pinner
45
+ * rejects with the `TypeError` of `fetch()`, caused by the `UrlError` that
46
+ * its lookup failed with.
47
+ */
48
+ export function isAddressRejection(error: unknown): boolean {
49
+ return error instanceof UrlError ||
50
+ error instanceof TypeError && error.cause instanceof UrlError;
51
+ }
52
+
53
+ export interface ReceivedRequest {
54
+ readonly method: string;
55
+ readonly url: string;
56
+ readonly host: string | undefined;
57
+ readonly localAddress: string | undefined;
58
+ readonly body: string;
59
+ }
60
+
61
+ function readBody(req: IncomingMessage): Promise<string> {
62
+ return new Promise((resolve, reject) => {
63
+ let body = "";
64
+ req.setEncoding("utf8");
65
+ req.on("data", (chunk: string) => body += chunk);
66
+ req.on("end", () => resolve(body));
67
+ req.on("error", reject);
68
+ });
69
+ }
70
+
71
+ export async function listen(
72
+ server: Server | TcpServer,
73
+ host = "127.0.0.1",
74
+ ): Promise<number> {
75
+ await new Promise<void>((resolve, reject) => {
76
+ server.once("error", reject);
77
+ server.listen(0, host, () => {
78
+ server.off("error", reject);
79
+ resolve();
80
+ });
81
+ });
82
+ return (server.address() as AddressInfo).port;
83
+ }
84
+
85
+ export async function closeServer(server: Server | TcpServer): Promise<void> {
86
+ (server as Server).closeAllConnections?.();
87
+ await new Promise<void>((resolve) => server.close(() => resolve()));
88
+ }
89
+
90
+ function canConnect(host: string, port: number): Promise<boolean> {
91
+ return new Promise((resolve) => {
92
+ const socket = connect({ host, port });
93
+ const done = (result: boolean) => {
94
+ socket.destroy();
95
+ resolve(result);
96
+ };
97
+ socket.setTimeout(2_000, () => done(false));
98
+ socket.once("connect", () => done(true));
99
+ socket.once("error", () => done(false));
100
+ });
101
+ }
102
+
103
+ export interface LoopbackSupport {
104
+ /** Whether `::1` can be connected to. */
105
+ readonly ipv6: boolean;
106
+ /**
107
+ * Whether a server listening on `::` accepts connections to both 127.0.0.1
108
+ * and `::1`.
109
+ */
110
+ readonly dualStack: boolean;
111
+ }
112
+
113
+ let loopbackSupport: Promise<LoopbackSupport> | undefined;
114
+
115
+ /**
116
+ * Checks which loopback addresses the tests can use. Only 127.0.0.1 is
117
+ * assumed: other addresses of 127.0.0.0/8 are not configured on macOS, and
118
+ * IPv6 can be disabled.
119
+ */
120
+ export function getLoopbackSupport(): Promise<LoopbackSupport> {
121
+ loopbackSupport ??= (async () => {
122
+ const server = createTcpServer((socket) => socket.destroy());
123
+ let port: number;
124
+ try {
125
+ port = await listen(server, "::");
126
+ } catch {
127
+ return { ipv6: false, dualStack: false };
128
+ }
129
+ try {
130
+ const [v4, v6] = await Promise.all([
131
+ canConnect("127.0.0.1", port),
132
+ canConnect("::1", port),
133
+ ]);
134
+ return { ipv6: v6, dualStack: v4 && v6 };
135
+ } finally {
136
+ await closeServer(server);
137
+ }
138
+ })();
139
+ return loopbackSupport;
140
+ }
141
+
142
+ /**
143
+ * Starts an HTTP server that records the requests it receives.
144
+ */
145
+ export async function startRecordingServer(
146
+ host = "127.0.0.1",
147
+ respond: (req: IncomingMessage, res: ServerResponse) => void = (_, res) =>
148
+ res.end("pinned"),
149
+ ) {
150
+ const requests: ReceivedRequest[] = [];
151
+ const server = createServer(async (req, res) => {
152
+ const body = await readBody(req);
153
+ requests.push({
154
+ method: req.method ?? "",
155
+ url: req.url ?? "",
156
+ host: req.headers.host,
157
+ localAddress: req.socket.localAddress,
158
+ body,
159
+ });
160
+ respond(req, res);
161
+ });
162
+ const port = await listen(server, host);
163
+ return { server, port, requests };
164
+ }
165
+
166
+ /**
167
+ * Starts a TCP server that only counts the connections it accepts, and drops
168
+ * them right away. It stands for an HTTPS server without a certificate.
169
+ */
170
+ export async function startCountingServer() {
171
+ const counter = { connections: 0 };
172
+ const server = createTcpServer((socket) => {
173
+ counter.connections++;
174
+ socket.destroy();
175
+ });
176
+ const port = await listen(server);
177
+ return { server, port, counter };
178
+ }
179
+
180
+ /**
181
+ * Starts a proxy that records the request targets it is asked for, and never
182
+ * forwards them. It answers a request in absolute form with `via-proxy`,
183
+ * and drops tunnels (`CONNECT`). It parses requests by itself since
184
+ * the `connect` event of `node:http` servers is not available on older Bun
185
+ * versions.
186
+ * @param options.refuseTunnels Whether to refuse tunnels with a `403`
187
+ * response instead of dropping them. Some
188
+ * clients retry dropped tunnels without end
189
+ * (e.g., the `EnvHttpProxyAgent` of Node.js),
190
+ * and some take the response of a refused
191
+ * tunnel for the response of the request
192
+ * (e.g., Bun 1.3.14).
193
+ */
194
+ export async function startRecordingProxy(
195
+ { refuseTunnels = false }: { readonly refuseTunnels?: boolean } = {},
196
+ ) {
197
+ const targets: string[] = [];
198
+ const server = createTcpServer((socket) => {
199
+ let head = "";
200
+ socket.setEncoding("latin1");
201
+ socket.on("error", () => {});
202
+ socket.on("data", (chunk: string) => {
203
+ if (head.includes("\r\n\r\n")) return;
204
+ head += chunk;
205
+ if (!head.includes("\r\n\r\n")) return;
206
+ const [method, target] = head.slice(0, head.indexOf("\r\n")).split(" ");
207
+ targets.push(`${method} ${target}`);
208
+ if (method !== "CONNECT") {
209
+ socket.end(
210
+ "HTTP/1.1 200 OK\r\nContent-Length: 9\r\nConnection: close\r\n" +
211
+ "\r\nvia-proxy",
212
+ );
213
+ } else if (refuseTunnels) {
214
+ socket.end(
215
+ "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n" +
216
+ "Connection: close\r\n\r\n",
217
+ );
218
+ } else {
219
+ socket.destroy();
220
+ }
221
+ });
222
+ });
223
+ const port = await listen(server);
224
+ return { server, port, targets };
225
+ }
226
+
227
+ /**
228
+ * Returns the first of the given paths that exists.
229
+ */
230
+ function findFile(name: string, candidates: readonly string[]): string {
231
+ const found = candidates.find((path) => existsSync(path));
232
+ if (found == null) {
233
+ throw new Error(`Cannot find ${name} in ${candidates.join(", ")}`);
234
+ }
235
+ return found;
236
+ }
237
+
238
+ function getModuleDirectory(moduleUrl: string | undefined): string | null {
239
+ return typeof moduleUrl === "string" && moduleUrl.startsWith("file:")
240
+ ? dirname(fileURLToPath(moduleUrl))
241
+ : null;
242
+ }
243
+
244
+ const TESTDATA = ["src", "pinners", "testdata"] as const;
245
+
246
+ /**
247
+ * Finds a source file in *testdata/* (e.g., a child script that Bun runs
248
+ * directly). When a test runs from the compiled *dist/tests/pinners/*
249
+ * instead of *src/pinners/*, the test data is not compiled with it, so this
250
+ * falls back to the files in the package: first relative to the test module,
251
+ * then relative to the working directory (the package root, where the Bun
252
+ * test script runs), in case `import.meta.url` is unavailable in a CommonJS
253
+ * build.
254
+ * @param name The file name.
255
+ * @param moduleUrl The `import.meta.url` of the test module.
256
+ */
257
+ export function resolveTestdata(
258
+ name: string,
259
+ moduleUrl: string | undefined,
260
+ ): string {
261
+ const candidates: string[] = [];
262
+ const here = getModuleDirectory(moduleUrl);
263
+ if (here != null) {
264
+ candidates.push(
265
+ join(here, TESTDATA[2], name),
266
+ join(here, "..", "..", "..", ...TESTDATA, name),
267
+ );
268
+ }
269
+ candidates.push(join(process.cwd(), ...TESTDATA, name));
270
+ return findFile(name, candidates);
271
+ }
272
+
273
+ /**
274
+ * Finds a child script of *testdata/* compiled into
275
+ * *dist/tests/pinners/testdata/* by *tsdown.config.ts*, for Node.js, which
276
+ * cannot run TypeScript sources without flags before 22.6.0. Node.js tests
277
+ * run only from *dist/tests/* (`cd dist/tests && node --test`), so this looks
278
+ * relative to the test module, then relative to the working directory, in
279
+ * case `import.meta.url` is unavailable in a CommonJS build.
280
+ * @param name The compiled file name, e.g., `node-child.mjs`.
281
+ * @param moduleUrl The `import.meta.url` of the test module.
282
+ */
283
+ export function resolveCompiledTestdata(
284
+ name: string,
285
+ moduleUrl: string | undefined,
286
+ ): string {
287
+ const candidates: string[] = [];
288
+ const here = getModuleDirectory(moduleUrl);
289
+ if (here != null) candidates.push(join(here, TESTDATA[2], name));
290
+ candidates.push(
291
+ join(process.cwd(), ...TESTDATA.slice(1), name),
292
+ join(process.cwd(), "dist", "tests", ...TESTDATA.slice(1), name),
293
+ );
294
+ return findFile(name, candidates);
295
+ }
296
+
297
+ const PROXY_ENV_NAMES = [
298
+ "HTTP_PROXY",
299
+ "HTTPS_PROXY",
300
+ "ALL_PROXY",
301
+ "NO_PROXY",
302
+ ] as const;
303
+
304
+ /**
305
+ * Runs a child script in a new process of the current runtime, without proxy
306
+ * environment variables (and `NODE_USE_ENV_PROXY`) but with the given ones,
307
+ * and parses the last line of its output as JSON.
308
+ * @param script The path of the script.
309
+ * @param args The arguments to the script.
310
+ * @param extraEnv The environment variables to set.
311
+ */
312
+ export function runChild<T>(
313
+ script: string,
314
+ args: readonly string[],
315
+ extraEnv: Record<string, string>,
316
+ ): Promise<T> {
317
+ const env: Record<string, string | undefined> = { ...process.env };
318
+ for (const name of PROXY_ENV_NAMES) {
319
+ delete env[name];
320
+ delete env[name.toLowerCase()];
321
+ }
322
+ delete env.NODE_USE_ENV_PROXY;
323
+ Object.assign(env, extraEnv);
324
+ return new Promise((resolve, reject) => {
325
+ execFile(
326
+ process.execPath,
327
+ [script, ...args],
328
+ { env, timeout: 30_000 },
329
+ (error, stdout, stderr) => {
330
+ if (error != null) {
331
+ reject(new Error(`${script} failed: ${stderr}`, { cause: error }));
332
+ return;
333
+ }
334
+ const line = stdout.trim().split("\n").at(-1) ?? "";
335
+ try {
336
+ resolve(JSON.parse(line));
337
+ } catch (cause) {
338
+ reject(new Error(`Unexpected output: ${stdout}${stderr}`, { cause }));
339
+ }
340
+ },
341
+ );
342
+ });
343
+ }
@@ -0,0 +1,12 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIBzTCCAXOgAwIBAgIUfeiNSVLuqF1qsVMEOsR7YW1pmVgwCgYIKoZIzj0EAwIw
3
+ HjEcMBoGA1UEAwwTcGlubmVkLWhvc3QuaW52YWxpZDAgFw0yNjEwMjAxODI2MDVa
4
+ GA8yMTI2MDkyNjE4MjYwNVowHjEcMBoGA1UEAwwTcGlubmVkLWhvc3QuaW52YWxp
5
+ ZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMiOCZmTSR6q86Up9QUH69Lf6o3G
6
+ /Y2T/NY3h3eE349EmITY/3TtcY18bG7dz+vkDw/umptvZ3fuE2uYmhoeKPWjgYww
7
+ gYkwHQYDVR0OBBYEFLHn+Zc6QAPrvKuTJv7E7ogNvqctMB8GA1UdIwQYMBaAFLHn
8
+ +Zc6QAPrvKuTJv7E7ogNvqctMA8GA1UdEwEB/wQFMAMBAf8wNgYDVR0RBC8wLYIT
9
+ cGlubmVkLWhvc3QuaW52YWxpZIIWYWx0ZXJuYXRlLWhvc3QuaW52YWxpZDAKBggq
10
+ hkjOPQQDAgNIADBFAiB4LmqgIz2JxWyls6cZdcLC5pGS9jgKVTPizpvXg/iE6gIh
11
+ AIYLzlFL9KlC5rc8dbkDWbNSEidECoc6UhUaO6QygAGc
12
+ -----END CERTIFICATE-----
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A child process for the certificate verification regression test of the
3
+ * Bun pinner in *../bun.test.ts*. Bun reads `NODE_EXTRA_CA_CERTS` only at
4
+ * startup, so the test runs this script in a new Bun process with it pointing
5
+ * to *tls-cert.pem*, a self-signed certificate for `pinned-host.invalid` and
6
+ * `alternate-host.invalid`.
7
+ *
8
+ * Usage: `bun tls-child.ts <https-port>`
9
+ *
10
+ * It requests `https://<hostname>:<https-port>/<case>` through a pinner that
11
+ * allows loopback addresses, with every hostname pinned to 127.0.0.1, in this
12
+ * order:
13
+ *
14
+ * 1. `pinned`: `pinned-host.invalid`, which the certificate covers.
15
+ * 2. `unlisted`: `unlisted-host.invalid`, which the certificate does not
16
+ * cover.
17
+ * 3. `unlisted-again`: the same as `unlisted`, so that a connection kept
18
+ * after a failed check would be reused.
19
+ * 4. `alternate`: `alternate-host.invalid`, the second name of
20
+ * the certificate.
21
+ * 5. `signals`: `pinned-host.invalid` with signals both in the request and
22
+ * as the separate argument.
23
+ *
24
+ * It prints the results as a JSON line.
25
+ * @module
26
+ */
27
+ import process from "node:process";
28
+ import { issuePinGrant } from "../../pin-grant.ts";
29
+ import { createBunPinner } from "../bun.ts";
30
+
31
+ type TlsChildResult =
32
+ | { readonly case: string; readonly status: number; readonly body: string }
33
+ | { readonly case: string; readonly error: string };
34
+
35
+ const port = Number(process.argv[2]);
36
+ if (!Number.isInteger(port)) {
37
+ throw new TypeError("Usage: tls-child.ts <https-port>");
38
+ }
39
+
40
+ const pinner = createBunPinner({ verifyAddress() {} });
41
+ const loopback = [{ address: "127.0.0.1", family: 4 }] as const;
42
+
43
+ async function attempt(
44
+ name: string,
45
+ hostname: string,
46
+ withSignals = false,
47
+ ): Promise<TlsChildResult> {
48
+ const url = new URL(`https://${hostname}:${port}/${name}`);
49
+ const grant = issuePinGrant(url, loopback);
50
+ try {
51
+ const response = withSignals
52
+ ? await pinner.fetch(
53
+ grant,
54
+ new Request(url, { signal: new AbortController().signal }),
55
+ new AbortController().signal,
56
+ )
57
+ : await pinner.fetch(grant, new Request(url));
58
+ return { case: name, status: response.status, body: await response.text() };
59
+ } catch (error) {
60
+ const code = (error as { code?: unknown } | null)?.code;
61
+ return {
62
+ case: name,
63
+ error: typeof code === "string" ? code : String(error),
64
+ };
65
+ }
66
+ }
67
+
68
+ const results: TlsChildResult[] = [];
69
+ for (
70
+ const [name, hostname] of [
71
+ ["pinned", "pinned-host.invalid"],
72
+ ["unlisted", "unlisted-host.invalid"],
73
+ ["unlisted-again", "unlisted-host.invalid"],
74
+ ["alternate", "alternate-host.invalid"],
75
+ ] as const
76
+ ) {
77
+ results.push(await attempt(name, hostname));
78
+ }
79
+ results.push(await attempt("signals", "pinned-host.invalid", true));
80
+
81
+ const output = JSON.stringify({
82
+ version: (globalThis as { Bun?: { version: string } }).Bun?.version,
83
+ results,
84
+ });
85
+ await new Promise<void>((resolve) =>
86
+ process.stdout.write(`${output}\n`, () => resolve())
87
+ );
88
+ process.exit(0);
@@ -0,0 +1,5 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/QHV62XOkUjGtf0h
3
+ mxlMDxXRwhp33NjCq9+0lo6Y072hRANCAATIjgmZk0keqvOlKfUFB+vS3+qNxv2N
4
+ k/zWN4d3hN+PRJiE2P907XGNfGxu3c/r5A8P7pqbb2d37hNrmJoaHij1
5
+ -----END PRIVATE KEY-----
@@ -0,0 +1,48 @@
1
+ import type { PinGrant } from "../pin-grant.ts";
2
+ import type { ResolvedAddress } from "../url.ts";
3
+
4
+ /**
5
+ * Connects requests only to the addresses a {@link PinGrant} stands for.
6
+ * Pinners are created by factories and never exported from any entry point.
7
+ */
8
+ export interface ConnectionPinner {
9
+ /**
10
+ * Whether this pinner can pin requests to the given URL. This is decided
11
+ * per protocol.
12
+ */
13
+ supports(url: URL): boolean;
14
+
15
+ /**
16
+ * Sends the request to one of the addresses of the grant. The grant is
17
+ * redeemed for every request, and every address is checked again right
18
+ * before connecting.
19
+ * @param grant The grant for the request's origin.
20
+ * @param request The request to send. Its redirect mode is ignored;
21
+ * redirects are never followed.
22
+ * @param signal An optional signal to abort the request.
23
+ * @returns The response.
24
+ */
25
+ fetch(
26
+ grant: PinGrant,
27
+ request: Request,
28
+ signal?: AbortSignal,
29
+ ): Promise<Response>;
30
+
31
+ /**
32
+ * Releases the resources held for the grant. Called once when the session
33
+ * that owns the grant is closed.
34
+ */
35
+ release(grant: PinGrant): void | Promise<void>;
36
+ }
37
+
38
+ /**
39
+ * Options for the pinner factories.
40
+ */
41
+ export interface ConnectionPinnerOptions {
42
+ /**
43
+ * Checks an address right before connecting to it. The production pinner
44
+ * rejects non-public addresses here.
45
+ * @throws {UrlError} If the address must not be connected to.
46
+ */
47
+ readonly verifyAddress: (address: ResolvedAddress) => void;
48
+ }