@fedify/vocab-runtime 2.4.0-dev.2200 → 2.4.0-dev.2212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/mod.cjs +145 -1
- package/dist/mod.d.cts +45 -1
- package/dist/mod.d.ts +45 -1
- package/dist/mod.js +145 -2
- package/dist/tests/{body-C7VkG5Rz.cjs → body-BG5Ol-IR.cjs} +1 -1
- package/dist/tests/{body-DxnRZZTZ.mjs → body-DbGNUM07.mjs} +1 -1
- package/dist/tests/body.test.cjs +1 -1
- package/dist/tests/body.test.mjs +1 -1
- package/dist/tests/decimal.test.cjs +2 -1
- package/dist/tests/decimal.test.mjs +2 -1
- package/dist/tests/{docloader-DzUk-dX-.cjs → docloader-BC9UAgDx.cjs} +2 -2
- package/dist/tests/{docloader-lhhaq3_J.mjs → docloader-C-c5dtFJ.mjs} +2 -2
- package/dist/tests/docloader.test.cjs +2 -2
- package/dist/tests/docloader.test.mjs +2 -2
- package/dist/tests/portable-media-Cn4uVXaI.cjs +153 -0
- package/dist/tests/portable-media-nj7yXhru.mjs +148 -0
- package/dist/tests/portable-media.test.cjs +222 -0
- package/dist/tests/portable-media.test.d.cts +1 -0
- package/dist/tests/portable-media.test.d.mts +1 -0
- package/dist/tests/portable-media.test.mjs +223 -0
- package/dist/tests/{request-AYfyUo0m.mjs → request-CfuNbRJC.mjs} +1 -1
- package/dist/tests/{request-D5KwMCcr.cjs → request-jic6JEwl.cjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/package.json +1 -1
- package/src/mod.ts +5 -0
- package/src/portable-media.test.ts +293 -0
- package/src/portable-media.ts +264 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { n as createHashlink, t as computeDigestMultibase } from "./digest-COC7xDiQ.mjs";
|
|
2
|
+
import { t as fetchPortableMedia } from "./portable-media-nj7yXhru.mjs";
|
|
3
|
+
import { deepStrictEqual, equal, rejects } from "node:assert/strict";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
//#region src/portable-media.test.ts
|
|
7
|
+
const bytes = new TextEncoder().encode("portable image");
|
|
8
|
+
const digest = "zQmU2Jds4B2J83K7CrYC34CEJdB6vCLyqkM2cgBxYeaNWTA";
|
|
9
|
+
const hashlink = createHashlink(digest);
|
|
10
|
+
const networkTest = (name, run) => test(name, async (t) => {
|
|
11
|
+
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") {
|
|
12
|
+
t.skip("Requires network permission for a local test server.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
await run();
|
|
16
|
+
});
|
|
17
|
+
async function withServers(handlers, run) {
|
|
18
|
+
const servers = handlers.map((handler) => createServer(async (request, reply) => {
|
|
19
|
+
const result = handler(request.url ?? "/");
|
|
20
|
+
if (result == null) return;
|
|
21
|
+
reply.writeHead(result.status, Object.fromEntries(result.headers));
|
|
22
|
+
reply.end(new Uint8Array(await result.arrayBuffer()));
|
|
23
|
+
}));
|
|
24
|
+
try {
|
|
25
|
+
const origins = [];
|
|
26
|
+
for (const server of servers) {
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
server.once("error", reject);
|
|
29
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
30
|
+
});
|
|
31
|
+
const address = server.address();
|
|
32
|
+
if (address == null || typeof address === "string") throw new Error("Server did not bind a TCP port.");
|
|
33
|
+
origins.push(`http://127.0.0.1:${address.port}`);
|
|
34
|
+
}
|
|
35
|
+
await run(origins);
|
|
36
|
+
} finally {
|
|
37
|
+
await Promise.all(servers.map(async (server) => {
|
|
38
|
+
server.closeAllConnections();
|
|
39
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
networkTest("fetchPortableMedia() accepts Link, Image, and Document", async () => {
|
|
44
|
+
const link = {
|
|
45
|
+
href: new URL(hashlink),
|
|
46
|
+
digestMultibase: digest
|
|
47
|
+
};
|
|
48
|
+
const image = {
|
|
49
|
+
url: new URL(hashlink),
|
|
50
|
+
digestMultibase: digest
|
|
51
|
+
};
|
|
52
|
+
const document = { url: {
|
|
53
|
+
href: new URL(hashlink),
|
|
54
|
+
digestMultibase: digest
|
|
55
|
+
} };
|
|
56
|
+
const requested = [];
|
|
57
|
+
await withServers([(path) => {
|
|
58
|
+
requested.push(path);
|
|
59
|
+
return new Response(bytes);
|
|
60
|
+
}], async ([gateway]) => {
|
|
61
|
+
for (const media of [
|
|
62
|
+
link,
|
|
63
|
+
image,
|
|
64
|
+
document
|
|
65
|
+
]) equal(await (await fetchPortableMedia(media, {
|
|
66
|
+
gateways: [gateway],
|
|
67
|
+
allowPrivateAddress: true
|
|
68
|
+
})).text(), "portable image");
|
|
69
|
+
});
|
|
70
|
+
deepStrictEqual(requested, Array(3).fill(`/.well-known/apgateway/${hashlink}`));
|
|
71
|
+
});
|
|
72
|
+
networkTest("fetchPortableMedia() falls back on status and digest mismatch", async () => {
|
|
73
|
+
const requested = [];
|
|
74
|
+
await withServers([
|
|
75
|
+
(path) => {
|
|
76
|
+
requested.push(`first:${path}`);
|
|
77
|
+
return new Response(null, { status: 503 });
|
|
78
|
+
},
|
|
79
|
+
(path) => {
|
|
80
|
+
requested.push(`second:${path}`);
|
|
81
|
+
return new Response("wrong bytes");
|
|
82
|
+
},
|
|
83
|
+
(path) => {
|
|
84
|
+
requested.push(`third:${path}`);
|
|
85
|
+
return new Response(bytes, { headers: {
|
|
86
|
+
"Content-Type": "image/png",
|
|
87
|
+
"Set-Cookie": "secret=1"
|
|
88
|
+
} });
|
|
89
|
+
}
|
|
90
|
+
], async (gateways) => {
|
|
91
|
+
const result = await fetchPortableMedia({
|
|
92
|
+
url: new URL(hashlink),
|
|
93
|
+
digestMultibase: digest
|
|
94
|
+
}, {
|
|
95
|
+
gateways,
|
|
96
|
+
allowPrivateAddress: true
|
|
97
|
+
});
|
|
98
|
+
equal(await result.text(), "portable image");
|
|
99
|
+
equal(result.headers.get("Content-Type"), "image/png");
|
|
100
|
+
equal(result.headers.get("Set-Cookie"), null);
|
|
101
|
+
});
|
|
102
|
+
deepStrictEqual(requested.map((r) => r.split(":")[0]), [
|
|
103
|
+
"first",
|
|
104
|
+
"second",
|
|
105
|
+
"third"
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
networkTest("fetchPortableMedia() verifies direct HTTP media", async () => {
|
|
109
|
+
const wrongDigest = await computeDigestMultibase(/* @__PURE__ */ new Uint8Array());
|
|
110
|
+
await withServers([() => new Response(bytes)], async ([origin]) => {
|
|
111
|
+
equal(await (await fetchPortableMedia(`${origin}/a`, {
|
|
112
|
+
digestMultibase: digest,
|
|
113
|
+
allowPrivateAddress: true
|
|
114
|
+
})).text(), "portable image");
|
|
115
|
+
await rejects(() => fetchPortableMedia(`${origin}/a`, {
|
|
116
|
+
digestMultibase: wrongDigest,
|
|
117
|
+
allowPrivateAddress: true
|
|
118
|
+
}), /digest does not match/);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
test("fetchPortableMedia() rejects invalid references before fetching", async () => {
|
|
122
|
+
const wrongDigest = await computeDigestMultibase(/* @__PURE__ */ new Uint8Array());
|
|
123
|
+
await rejects(() => fetchPortableMedia({ url: new URL(hashlink) }), /requires digestMultibase/);
|
|
124
|
+
await rejects(() => fetchPortableMedia({
|
|
125
|
+
url: new URL(hashlink),
|
|
126
|
+
digestMultibase: digest
|
|
127
|
+
}), /requires at least one gateway/);
|
|
128
|
+
await rejects(() => fetchPortableMedia({
|
|
129
|
+
url: new URL(hashlink),
|
|
130
|
+
digestMultibase: wrongDigest
|
|
131
|
+
}, { gateways: ["https://example.com"] }), /disagree/);
|
|
132
|
+
await rejects(() => fetchPortableMedia({
|
|
133
|
+
url: new URL("hl:invalid"),
|
|
134
|
+
digestMultibase: digest
|
|
135
|
+
}, { gateways: ["https://example.com"] }), TypeError);
|
|
136
|
+
await rejects(() => fetchPortableMedia({
|
|
137
|
+
url: {
|
|
138
|
+
href: new URL(hashlink),
|
|
139
|
+
digestMultibase: wrongDigest
|
|
140
|
+
},
|
|
141
|
+
digestMultibase: digest
|
|
142
|
+
}, { gateways: ["https://example.com"] }), /digests disagree/);
|
|
143
|
+
await rejects(() => fetchPortableMedia("file:///tmp/media", { digestMultibase: digest }), TypeError);
|
|
144
|
+
});
|
|
145
|
+
networkTest("fetchPortableMedia() bounds response bytes", async () => {
|
|
146
|
+
await withServers([() => new Response(bytes)], async ([origin]) => {
|
|
147
|
+
await rejects(() => fetchPortableMedia(`${origin}/media`, {
|
|
148
|
+
digestMultibase: digest,
|
|
149
|
+
maxBytes: bytes.length - 1,
|
|
150
|
+
allowPrivateAddress: true
|
|
151
|
+
}), /exceeds the limit/);
|
|
152
|
+
});
|
|
153
|
+
await rejects(() => fetchPortableMedia("https://example.com/media", {
|
|
154
|
+
digestMultibase: digest,
|
|
155
|
+
maxBytes: 0
|
|
156
|
+
}), RangeError);
|
|
157
|
+
});
|
|
158
|
+
test("fetchPortableMedia() rejects private media and gateways", async () => {
|
|
159
|
+
await rejects(() => fetchPortableMedia("http://127.0.0.1/media", { digestMultibase: digest }), /private address/);
|
|
160
|
+
await rejects(() => fetchPortableMedia({
|
|
161
|
+
url: new URL(hashlink),
|
|
162
|
+
digestMultibase: digest
|
|
163
|
+
}, { gateways: ["http://127.0.0.1"] }), /No gateway returned verified portable media/);
|
|
164
|
+
});
|
|
165
|
+
test("fetchPortableMedia() propagates cancellation", async () => {
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const reason = /* @__PURE__ */ new Error("cancelled");
|
|
168
|
+
controller.abort(reason);
|
|
169
|
+
await rejects(() => fetchPortableMedia({
|
|
170
|
+
url: new URL(hashlink),
|
|
171
|
+
digestMultibase: digest
|
|
172
|
+
}, {
|
|
173
|
+
gateways: ["https://example.com"],
|
|
174
|
+
signal: controller.signal
|
|
175
|
+
}), (error) => error === reason);
|
|
176
|
+
});
|
|
177
|
+
networkTest("fetchPortableMedia() falls back after a gateway timeout", async () => {
|
|
178
|
+
await withServers([() => void 0, () => new Response(bytes)], async (gateways) => {
|
|
179
|
+
equal(await (await fetchPortableMedia({
|
|
180
|
+
url: new URL(hashlink),
|
|
181
|
+
digestMultibase: digest
|
|
182
|
+
}, {
|
|
183
|
+
gateways,
|
|
184
|
+
allowPrivateAddress: true,
|
|
185
|
+
gatewayTimeout: 20,
|
|
186
|
+
timeout: 500
|
|
187
|
+
})).text(), "portable image");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
networkTest("fetchPortableMedia() applies one overall timeout", async () => {
|
|
191
|
+
await withServers([() => void 0, () => new Response(bytes)], async (gateways) => {
|
|
192
|
+
await rejects(() => fetchPortableMedia({
|
|
193
|
+
url: new URL(hashlink),
|
|
194
|
+
digestMultibase: digest
|
|
195
|
+
}, {
|
|
196
|
+
gateways,
|
|
197
|
+
allowPrivateAddress: true,
|
|
198
|
+
gatewayTimeout: 100,
|
|
199
|
+
timeout: 20
|
|
200
|
+
}), (error) => error instanceof DOMException && error.name === "TimeoutError");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
networkTest("fetchPortableMedia() follows a safe redirect", async () => {
|
|
204
|
+
const requested = [];
|
|
205
|
+
await withServers([(path) => {
|
|
206
|
+
requested.push(path);
|
|
207
|
+
return path === "/media" ? new Response(bytes) : new Response(null, {
|
|
208
|
+
status: 302,
|
|
209
|
+
headers: { Location: "/media" }
|
|
210
|
+
});
|
|
211
|
+
}], async ([gateway]) => {
|
|
212
|
+
equal(await (await fetchPortableMedia({
|
|
213
|
+
url: new URL(hashlink),
|
|
214
|
+
digestMultibase: digest
|
|
215
|
+
}, {
|
|
216
|
+
gateways: [gateway],
|
|
217
|
+
allowPrivateAddress: true
|
|
218
|
+
})).text(), "portable image");
|
|
219
|
+
});
|
|
220
|
+
deepStrictEqual(requested, [`/.well-known/apgateway/${hashlink}`, "/media"]);
|
|
221
|
+
});
|
|
222
|
+
//#endregion
|
|
223
|
+
export {};
|
|
@@ -3,7 +3,7 @@ let node_process = require("node:process");
|
|
|
3
3
|
node_process = require_rolldown_runtime.__toESM(node_process, 1);
|
|
4
4
|
//#region deno.json
|
|
5
5
|
var name = "@fedify/vocab-runtime";
|
|
6
|
-
var version = "2.4.0-dev.
|
|
6
|
+
var version = "2.4.0-dev.2212+26ce9858";
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region src/request.ts
|
|
9
9
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
|
|
2
|
-
const require_request = require("./request-
|
|
2
|
+
const require_request = require("./request-jic6JEwl.cjs");
|
|
3
3
|
let node_test = require("node:test");
|
|
4
4
|
let node_process = require("node:process");
|
|
5
5
|
node_process = require_rolldown_runtime.__toESM(node_process, 1);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-
|
|
1
|
+
import { n as createActivityPubRequest, o as version, r as getUserAgent } from "./request-CfuNbRJC.mjs";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { deepStrictEqual } from "node:assert";
|
package/package.json
CHANGED
package/src/mod.ts
CHANGED
|
@@ -47,6 +47,11 @@ export {
|
|
|
47
47
|
verifyHashlink,
|
|
48
48
|
} from "./digest.ts";
|
|
49
49
|
export { LanguageString } from "./langstr.ts";
|
|
50
|
+
export {
|
|
51
|
+
fetchPortableMedia,
|
|
52
|
+
type FetchPortableMediaOptions,
|
|
53
|
+
type PortableMedia,
|
|
54
|
+
} from "./portable-media.ts";
|
|
50
55
|
export type {
|
|
51
56
|
PortableObjectReferrer,
|
|
52
57
|
PortableObjectVerification,
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { deepStrictEqual, equal, rejects } from "node:assert/strict";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import type { Document, Image, Link } from "@fedify/vocab";
|
|
5
|
+
import { computeDigestMultibase, createHashlink } from "./digest.ts";
|
|
6
|
+
import { fetchPortableMedia, type PortableMedia } from "./portable-media.ts";
|
|
7
|
+
|
|
8
|
+
const bytes = new TextEncoder().encode("portable image");
|
|
9
|
+
const digest = "zQmU2Jds4B2J83K7CrYC34CEJdB6vCLyqkM2cgBxYeaNWTA";
|
|
10
|
+
const hashlink = createHashlink(digest);
|
|
11
|
+
|
|
12
|
+
const networkTest = (name: string, run: () => Promise<void>) =>
|
|
13
|
+
test(name, async (t) => {
|
|
14
|
+
if (
|
|
15
|
+
"Deno" in globalThis &&
|
|
16
|
+
(await Deno.permissions.query({ name: "net" })).state !== "granted"
|
|
17
|
+
) {
|
|
18
|
+
t.skip("Requires network permission for a local test server.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await run();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type Handler = (path: string) => Response | undefined;
|
|
25
|
+
|
|
26
|
+
async function withServers(
|
|
27
|
+
handlers: Handler[],
|
|
28
|
+
run: (origins: string[]) => Promise<void>,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const servers = handlers.map((handler) =>
|
|
31
|
+
createServer(async (request, reply) => {
|
|
32
|
+
const result = handler(request.url ?? "/");
|
|
33
|
+
if (result == null) return;
|
|
34
|
+
reply.writeHead(result.status, Object.fromEntries(result.headers));
|
|
35
|
+
reply.end(new Uint8Array(await result.arrayBuffer()));
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
try {
|
|
39
|
+
const origins: string[] = [];
|
|
40
|
+
for (const server of servers) {
|
|
41
|
+
await new Promise<void>((resolve, reject) => {
|
|
42
|
+
server.once("error", reject);
|
|
43
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
44
|
+
});
|
|
45
|
+
const address = server.address();
|
|
46
|
+
if (address == null || typeof address === "string") {
|
|
47
|
+
throw new Error("Server did not bind a TCP port.");
|
|
48
|
+
}
|
|
49
|
+
origins.push(`http://127.0.0.1:${address.port}`);
|
|
50
|
+
}
|
|
51
|
+
await run(origins);
|
|
52
|
+
} finally {
|
|
53
|
+
await Promise.all(servers.map(async (server) => {
|
|
54
|
+
server.closeAllConnections();
|
|
55
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
networkTest(
|
|
61
|
+
"fetchPortableMedia() accepts Link, Image, and Document",
|
|
62
|
+
async () => {
|
|
63
|
+
const accepts = <T extends PortableMedia>() => {};
|
|
64
|
+
accepts<Link>();
|
|
65
|
+
accepts<Image>();
|
|
66
|
+
accepts<Document>();
|
|
67
|
+
const link: PortableMedia = {
|
|
68
|
+
href: new URL(hashlink),
|
|
69
|
+
digestMultibase: digest,
|
|
70
|
+
};
|
|
71
|
+
const image: PortableMedia = {
|
|
72
|
+
url: new URL(hashlink),
|
|
73
|
+
digestMultibase: digest,
|
|
74
|
+
};
|
|
75
|
+
const document: PortableMedia = {
|
|
76
|
+
url: { href: new URL(hashlink), digestMultibase: digest },
|
|
77
|
+
};
|
|
78
|
+
const requested: string[] = [];
|
|
79
|
+
await withServers([(path) => {
|
|
80
|
+
requested.push(path);
|
|
81
|
+
return new Response(bytes);
|
|
82
|
+
}], async ([gateway]) => {
|
|
83
|
+
for (const media of [link, image, document]) {
|
|
84
|
+
equal(
|
|
85
|
+
await (await fetchPortableMedia(media, {
|
|
86
|
+
gateways: [gateway],
|
|
87
|
+
allowPrivateAddress: true,
|
|
88
|
+
})).text(),
|
|
89
|
+
"portable image",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
deepStrictEqual(
|
|
94
|
+
requested,
|
|
95
|
+
Array(3).fill(
|
|
96
|
+
`/.well-known/apgateway/${hashlink}`,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
networkTest(
|
|
103
|
+
"fetchPortableMedia() falls back on status and digest mismatch",
|
|
104
|
+
async () => {
|
|
105
|
+
const requested: string[] = [];
|
|
106
|
+
await withServers([
|
|
107
|
+
(path) => {
|
|
108
|
+
requested.push(`first:${path}`);
|
|
109
|
+
return new Response(null, { status: 503 });
|
|
110
|
+
},
|
|
111
|
+
(path) => {
|
|
112
|
+
requested.push(`second:${path}`);
|
|
113
|
+
return new Response("wrong bytes");
|
|
114
|
+
},
|
|
115
|
+
(path) => {
|
|
116
|
+
requested.push(`third:${path}`);
|
|
117
|
+
return new Response(bytes, {
|
|
118
|
+
headers: { "Content-Type": "image/png", "Set-Cookie": "secret=1" },
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
], async (gateways) => {
|
|
122
|
+
const result = await fetchPortableMedia(
|
|
123
|
+
{ url: new URL(hashlink), digestMultibase: digest },
|
|
124
|
+
{ gateways, allowPrivateAddress: true },
|
|
125
|
+
);
|
|
126
|
+
equal(await result.text(), "portable image");
|
|
127
|
+
equal(result.headers.get("Content-Type"), "image/png");
|
|
128
|
+
equal(result.headers.get("Set-Cookie"), null);
|
|
129
|
+
});
|
|
130
|
+
deepStrictEqual(requested.map((r) => r.split(":")[0]), [
|
|
131
|
+
"first",
|
|
132
|
+
"second",
|
|
133
|
+
"third",
|
|
134
|
+
]);
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
networkTest("fetchPortableMedia() verifies direct HTTP media", async () => {
|
|
139
|
+
const wrongDigest = await computeDigestMultibase(new Uint8Array());
|
|
140
|
+
await withServers([() => new Response(bytes)], async ([origin]) => {
|
|
141
|
+
const result = await fetchPortableMedia(`${origin}/a`, {
|
|
142
|
+
digestMultibase: digest,
|
|
143
|
+
allowPrivateAddress: true,
|
|
144
|
+
});
|
|
145
|
+
equal(await result.text(), "portable image");
|
|
146
|
+
await rejects(() =>
|
|
147
|
+
fetchPortableMedia(`${origin}/a`, {
|
|
148
|
+
digestMultibase: wrongDigest,
|
|
149
|
+
allowPrivateAddress: true,
|
|
150
|
+
}), /digest does not match/);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("fetchPortableMedia() rejects invalid references before fetching", async () => {
|
|
155
|
+
const wrongDigest = await computeDigestMultibase(new Uint8Array());
|
|
156
|
+
await rejects(
|
|
157
|
+
() => fetchPortableMedia({ url: new URL(hashlink) }),
|
|
158
|
+
/requires digestMultibase/,
|
|
159
|
+
);
|
|
160
|
+
await rejects(() =>
|
|
161
|
+
fetchPortableMedia({
|
|
162
|
+
url: new URL(hashlink),
|
|
163
|
+
digestMultibase: digest,
|
|
164
|
+
}), /requires at least one gateway/);
|
|
165
|
+
await rejects(() =>
|
|
166
|
+
fetchPortableMedia({
|
|
167
|
+
url: new URL(hashlink),
|
|
168
|
+
digestMultibase: wrongDigest,
|
|
169
|
+
}, { gateways: ["https://example.com"] }), /disagree/);
|
|
170
|
+
await rejects(() =>
|
|
171
|
+
fetchPortableMedia({
|
|
172
|
+
url: new URL("hl:invalid"),
|
|
173
|
+
digestMultibase: digest,
|
|
174
|
+
}, { gateways: ["https://example.com"] }), TypeError);
|
|
175
|
+
await rejects(() =>
|
|
176
|
+
fetchPortableMedia({
|
|
177
|
+
url: { href: new URL(hashlink), digestMultibase: wrongDigest },
|
|
178
|
+
digestMultibase: digest,
|
|
179
|
+
}, { gateways: ["https://example.com"] }), /digests disagree/);
|
|
180
|
+
await rejects(() =>
|
|
181
|
+
fetchPortableMedia("file:///tmp/media", {
|
|
182
|
+
digestMultibase: digest,
|
|
183
|
+
}), TypeError);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
networkTest("fetchPortableMedia() bounds response bytes", async () => {
|
|
187
|
+
await withServers([() => new Response(bytes)], async ([origin]) => {
|
|
188
|
+
await rejects(() =>
|
|
189
|
+
fetchPortableMedia(`${origin}/media`, {
|
|
190
|
+
digestMultibase: digest,
|
|
191
|
+
maxBytes: bytes.length - 1,
|
|
192
|
+
allowPrivateAddress: true,
|
|
193
|
+
}), /exceeds the limit/);
|
|
194
|
+
});
|
|
195
|
+
await rejects(() =>
|
|
196
|
+
fetchPortableMedia("https://example.com/media", {
|
|
197
|
+
digestMultibase: digest,
|
|
198
|
+
maxBytes: 0,
|
|
199
|
+
}), RangeError);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("fetchPortableMedia() rejects private media and gateways", async () => {
|
|
203
|
+
await rejects(() =>
|
|
204
|
+
fetchPortableMedia("http://127.0.0.1/media", {
|
|
205
|
+
digestMultibase: digest,
|
|
206
|
+
}), /private address/);
|
|
207
|
+
await rejects(
|
|
208
|
+
() =>
|
|
209
|
+
fetchPortableMedia({
|
|
210
|
+
url: new URL(hashlink),
|
|
211
|
+
digestMultibase: digest,
|
|
212
|
+
}, { gateways: ["http://127.0.0.1"] }),
|
|
213
|
+
/No gateway returned verified portable media/,
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("fetchPortableMedia() propagates cancellation", async () => {
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const reason = new Error("cancelled");
|
|
220
|
+
controller.abort(reason);
|
|
221
|
+
await rejects(() =>
|
|
222
|
+
fetchPortableMedia({
|
|
223
|
+
url: new URL(hashlink),
|
|
224
|
+
digestMultibase: digest,
|
|
225
|
+
}, {
|
|
226
|
+
gateways: ["https://example.com"],
|
|
227
|
+
signal: controller.signal,
|
|
228
|
+
}), (error) => error === reason);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
networkTest(
|
|
232
|
+
"fetchPortableMedia() falls back after a gateway timeout",
|
|
233
|
+
async () => {
|
|
234
|
+
await withServers([
|
|
235
|
+
() => undefined,
|
|
236
|
+
() => new Response(bytes),
|
|
237
|
+
], async (gateways) => {
|
|
238
|
+
const response = await fetchPortableMedia({
|
|
239
|
+
url: new URL(hashlink),
|
|
240
|
+
digestMultibase: digest,
|
|
241
|
+
}, {
|
|
242
|
+
gateways,
|
|
243
|
+
allowPrivateAddress: true,
|
|
244
|
+
gatewayTimeout: 20,
|
|
245
|
+
timeout: 500,
|
|
246
|
+
});
|
|
247
|
+
equal(await response.text(), "portable image");
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
networkTest("fetchPortableMedia() applies one overall timeout", async () => {
|
|
253
|
+
await withServers(
|
|
254
|
+
[() => undefined, () => new Response(bytes)],
|
|
255
|
+
async (gateways) => {
|
|
256
|
+
await rejects(
|
|
257
|
+
() =>
|
|
258
|
+
fetchPortableMedia({
|
|
259
|
+
url: new URL(hashlink),
|
|
260
|
+
digestMultibase: digest,
|
|
261
|
+
}, {
|
|
262
|
+
gateways,
|
|
263
|
+
allowPrivateAddress: true,
|
|
264
|
+
gatewayTimeout: 100,
|
|
265
|
+
timeout: 20,
|
|
266
|
+
}),
|
|
267
|
+
(error) =>
|
|
268
|
+
error instanceof DOMException && error.name === "TimeoutError",
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
networkTest("fetchPortableMedia() follows a safe redirect", async () => {
|
|
275
|
+
const requested: string[] = [];
|
|
276
|
+
await withServers([(path) => {
|
|
277
|
+
requested.push(path);
|
|
278
|
+
return path === "/media" ? new Response(bytes) : new Response(null, {
|
|
279
|
+
status: 302,
|
|
280
|
+
headers: { Location: "/media" },
|
|
281
|
+
});
|
|
282
|
+
}], async ([gateway]) => {
|
|
283
|
+
const response = await fetchPortableMedia({
|
|
284
|
+
url: new URL(hashlink),
|
|
285
|
+
digestMultibase: digest,
|
|
286
|
+
}, { gateways: [gateway], allowPrivateAddress: true });
|
|
287
|
+
equal(await response.text(), "portable image");
|
|
288
|
+
});
|
|
289
|
+
deepStrictEqual(requested, [
|
|
290
|
+
`/.well-known/apgateway/${hashlink}`,
|
|
291
|
+
"/media",
|
|
292
|
+
]);
|
|
293
|
+
});
|