@fedify/vocab-runtime 2.4.0-pr.936.41 → 2.5.0-dev.2271
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 +4 -2
- package/dist/{tests/docloader-Ck8bcBir.mjs → contexts-BK1CqBR5.cjs} +164 -275
- package/dist/{tests/docloader-S_gQZ2tt.cjs → contexts-DPuJ4UYL.js} +160 -294
- package/dist/{docloader-C_dir7Xb.d.ts → docloader-CYwqh5Df.d.cts} +66 -2
- package/dist/{docloader-D2DTRiyA.d.cts → docloader-CYwqh5Df.d.ts} +66 -2
- package/dist/internal/jsonld-cache.cjs +70 -1
- package/dist/internal/jsonld-cache.d.cts +35 -2
- package/dist/internal/jsonld-cache.d.ts +35 -2
- package/dist/internal/jsonld-cache.js +67 -2
- package/dist/internal/portable-dereference.cjs +721 -0
- package/dist/internal/portable-dereference.d.cts +349 -0
- package/dist/internal/portable-dereference.d.ts +349 -0
- package/dist/internal/portable-dereference.js +702 -0
- package/dist/internal/signed-representation.cjs +378 -0
- package/dist/internal/signed-representation.d.cts +144 -0
- package/dist/internal/signed-representation.d.ts +144 -0
- package/dist/internal/signed-representation.js +371 -0
- package/dist/jsonld.cjs +2 -2
- package/dist/mod.cjs +711 -4527
- package/dist/mod.d.cts +400 -8
- package/dist/mod.d.ts +400 -9
- package/dist/mod.js +692 -4522
- package/dist/portable-DqtfLy_1.d.cts +160 -0
- package/dist/portable-DtWsu2yU.d.ts +160 -0
- package/dist/tests/body-CVR1bABr.mjs +130 -0
- package/dist/tests/body-T_yQWql3.cjs +153 -0
- package/dist/tests/body.test.cjs +89 -0
- package/dist/tests/body.test.d.cts +1 -0
- package/dist/tests/body.test.d.mts +1 -0
- package/dist/tests/body.test.mjs +90 -0
- package/dist/tests/contexts-CIKsin4e.mjs +4512 -0
- package/dist/tests/contexts-DizzBjz4.cjs +4523 -0
- package/dist/tests/decimal.test.cjs +8 -7
- package/dist/tests/decimal.test.mjs +8 -6
- package/dist/tests/digest-3FeH2Y-Q.cjs +176 -0
- package/dist/tests/digest-COC7xDiQ.mjs +141 -0
- package/dist/tests/digest.test.cjs +102 -0
- package/dist/tests/digest.test.d.cts +1 -0
- package/dist/tests/digest.test.d.mts +1 -0
- package/dist/tests/digest.test.mjs +103 -0
- package/dist/tests/docloader-C6yApEts.mjs +373 -0
- package/dist/tests/docloader-CJK2w3l-.cjs +396 -0
- package/dist/tests/docloader.test.cjs +692 -39
- package/dist/tests/docloader.test.mjs +686 -34
- package/dist/tests/internal/multicodec.test.cjs +2 -3
- package/dist/tests/internal/multicodec.test.mjs +2 -2
- package/dist/tests/internal/portable-dereference.test.cjs +172 -0
- package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
- package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
- package/dist/tests/internal/portable-dereference.test.mjs +173 -0
- package/dist/tests/jsonld-cache-BPQmOZWD.mjs +342 -0
- package/dist/tests/jsonld-cache-C07AyNOY.cjs +397 -0
- package/dist/tests/jsonld-cache.test.cjs +101 -298
- package/dist/tests/jsonld-cache.test.mjs +94 -289
- package/dist/tests/{key-_wXwomh_.cjs → key-C-AYkdJJ.cjs} +11 -4
- package/dist/tests/{key-CDGDH_vC.mjs → key-C2Db_TAJ.mjs} +11 -3
- package/dist/tests/key.test.cjs +6 -5
- package/dist/tests/key.test.mjs +6 -4
- package/dist/tests/langstr.test.cjs +4 -4
- package/dist/tests/langstr.test.mjs +2 -2
- package/dist/tests/link.test.cjs +2 -3
- package/dist/tests/link.test.mjs +2 -2
- package/dist/tests/multibase/multibase.test.cjs +8 -9
- package/dist/tests/multibase/multibase.test.mjs +6 -6
- package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-B5Mea7Ip.cjs} +19 -2
- package/dist/tests/{multibase-B4bvakyA.mjs → multibase-BPnF_L4e.mjs} +12 -1
- package/dist/tests/portable-dereference-BNXtgg5U.cjs +278 -0
- package/dist/tests/portable-dereference-DRE5bz-l.mjs +249 -0
- package/dist/tests/portable-media-C4LEXJk4.mjs +148 -0
- package/dist/tests/portable-media-D05JJIJg.cjs +153 -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/portable-workers.test.cjs +36 -0
- package/dist/tests/portable-workers.test.d.cts +2 -0
- package/dist/tests/portable-workers.test.d.mts +2 -0
- package/dist/tests/portable-workers.test.mjs +35 -0
- package/dist/tests/{request-uk51rkhO.cjs → request-C-VZ9TLj.cjs} +10 -4
- package/dist/tests/{request-C8CaGwtt.mjs → request-Cf-A3DU-.mjs} +8 -2
- package/dist/tests/request.test.cjs +7 -4
- package/dist/tests/request.test.mjs +5 -2
- package/dist/tests/signed-representation.test.cjs +588 -0
- package/dist/tests/signed-representation.test.d.cts +1 -0
- package/dist/tests/signed-representation.test.d.mts +1 -0
- package/dist/tests/signed-representation.test.mjs +589 -0
- package/dist/tests/temporal.test.cjs +1 -2
- package/dist/tests/temporal.test.mjs +1 -1
- package/dist/tests/url-BNakuZ8k.cjs +998 -0
- package/dist/tests/url-DMxmp7ZG.mjs +859 -0
- package/dist/tests/url.test.cjs +489 -6
- package/dist/tests/url.test.mjs +489 -5
- package/dist/url-DrGTR8yv.cjs +993 -0
- package/dist/url-Dzyp-NsC.js +860 -0
- package/package.json +27 -4
- package/scripts/test-bun.mjs +17 -0
- package/src/body.test.ts +125 -0
- package/src/body.ts +152 -0
- package/src/contexts/cid-v1.json +114 -0
- package/src/contexts/fep-22cd.json +21 -0
- package/src/contexts/miscellany.json +17 -0
- package/src/contexts.ts +39 -1
- package/src/digest.test.ts +220 -0
- package/src/digest.ts +229 -0
- package/src/docloader.test.ts +918 -27
- package/src/docloader.ts +320 -105
- package/src/internal/jsonld-cache.ts +97 -1
- package/src/internal/portable-dereference.test.ts +254 -0
- package/src/internal/portable-dereference.ts +1000 -0
- package/src/internal/signed-representation.ts +565 -0
- package/src/jsonld-cache.test.ts +89 -0
- package/src/key.test.ts +9 -0
- package/src/key.ts +11 -1
- package/src/mod.ts +28 -0
- package/src/multibase/multibase.test.ts +5 -5
- package/src/portable-media.test.ts +293 -0
- package/src/portable-media.ts +264 -0
- package/src/portable-workers.test.ts +81 -0
- package/src/portable.ts +180 -0
- package/src/preprocessor.ts +7 -0
- package/src/request.test.ts +10 -1
- package/src/request.ts +9 -1
- package/src/signed-representation.test.ts +338 -0
- package/src/url.test.ts +823 -3
- package/src/url.ts +700 -23
- package/tsdown.config.ts +2 -0
- package/dist/tests/url-CsOV_B_P.cjs +0 -482
- package/dist/tests/url-Du7RQQgP.mjs +0 -392
- package/dist/url-DD4F0ULf.cjs +0 -483
- package/dist/url-DGVbSVVi.js +0 -393
- /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
- /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-C4Fl80ae.cjs} +0 -0
- /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-CQ26J_L7.mjs} +0 -0
- /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
- /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
- /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
- /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
- /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { r as getUserAgent } from "./request-Cf-A3DU-.mjs";
|
|
2
|
+
import { r as readBoundedBytes } from "./body-CVR1bABr.mjs";
|
|
3
|
+
import { g as parseGatewayUrl, x as validatePublicUrl } from "./url-DMxmp7ZG.mjs";
|
|
4
|
+
import { a as verifyDigestMultibase, i as parseHashlink, r as parseDigestMultibase } from "./digest-COC7xDiQ.mjs";
|
|
5
|
+
//#region src/portable-media.ts
|
|
6
|
+
function resolveTimeout(value, fallback) {
|
|
7
|
+
if (value === null) return null;
|
|
8
|
+
const result = value ?? fallback;
|
|
9
|
+
if (!Number.isFinite(result) || result <= 0 || result > 2147483647) throw new RangeError("A media timeout must be a positive finite number of milliseconds.");
|
|
10
|
+
return Math.ceil(result);
|
|
11
|
+
}
|
|
12
|
+
async function withTimeout(operation, parent, timeout) {
|
|
13
|
+
parent?.throwIfAborted();
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const onParentAbort = () => controller.abort(parent?.reason);
|
|
16
|
+
parent?.addEventListener("abort", onParentAbort, { once: true });
|
|
17
|
+
let timer;
|
|
18
|
+
if (timeout != null) timer = setTimeout(() => controller.abort(new DOMException(`Media retrieval timed out after ${timeout} ms.`, "TimeoutError")), timeout);
|
|
19
|
+
let onAbort;
|
|
20
|
+
const aborted = new Promise((_, reject) => {
|
|
21
|
+
onAbort = () => reject(controller.signal.reason);
|
|
22
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
return await Promise.race([operation(controller.signal), aborted]);
|
|
26
|
+
} finally {
|
|
27
|
+
if (timer != null) clearTimeout(timer);
|
|
28
|
+
parent?.removeEventListener("abort", onParentAbort);
|
|
29
|
+
if (onAbort != null) controller.signal.removeEventListener("abort", onAbort);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function mediaTarget(media) {
|
|
33
|
+
if (typeof media === "string" || media instanceof URL) return {
|
|
34
|
+
url: media.toString(),
|
|
35
|
+
digestMultibase: void 0
|
|
36
|
+
};
|
|
37
|
+
const target = media.href ?? media.url;
|
|
38
|
+
const url = target instanceof URL ? target.href : target != null && typeof target === "object" && "href" in target ? target.href?.href : void 0;
|
|
39
|
+
if (url == null) throw new TypeError("Portable media has no URL or href.");
|
|
40
|
+
const linkDigest = target != null && !(target instanceof URL) && typeof target === "object" && "digestMultibase" in target ? target.digestMultibase : void 0;
|
|
41
|
+
if (media.digestMultibase != null && linkDigest != null) {
|
|
42
|
+
const outer = parseDigestMultibase(media.digestMultibase).digest;
|
|
43
|
+
const inner = parseDigestMultibase(linkDigest).digest;
|
|
44
|
+
if (outer.some((byte, i) => byte !== inner[i])) throw new TypeError("Media and linked resource digests disagree.");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
url,
|
|
48
|
+
digestMultibase: media.digestMultibase ?? linkDigest
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function fetchVerified(url, digestMultibase, options, signal, maxBytes) {
|
|
52
|
+
const visited = /* @__PURE__ */ new Set();
|
|
53
|
+
for (let redirects = 0; redirects <= 20; redirects++) {
|
|
54
|
+
signal.throwIfAborted();
|
|
55
|
+
if (url.username !== "" || url.password !== "") throw new TypeError("Media URLs must not contain credentials.");
|
|
56
|
+
if (!options.allowPrivateAddress) await validatePublicUrl(url.href);
|
|
57
|
+
else if (url.protocol !== "http:" && url.protocol !== "https:") throw new TypeError("Media URLs must use HTTP(S).");
|
|
58
|
+
if (visited.has(url.href)) throw new TypeError("Media redirect loop.");
|
|
59
|
+
visited.add(url.href);
|
|
60
|
+
const response = await fetch(url, {
|
|
61
|
+
method: "GET",
|
|
62
|
+
headers: {
|
|
63
|
+
Accept: "*/*",
|
|
64
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
65
|
+
},
|
|
66
|
+
redirect: "manual",
|
|
67
|
+
signal
|
|
68
|
+
});
|
|
69
|
+
if ([
|
|
70
|
+
301,
|
|
71
|
+
302,
|
|
72
|
+
303,
|
|
73
|
+
307,
|
|
74
|
+
308
|
|
75
|
+
].includes(response.status)) {
|
|
76
|
+
const location = response.headers.get("Location");
|
|
77
|
+
response.body?.cancel().catch(() => {});
|
|
78
|
+
if (location == null || redirects === 20) throw new TypeError("Invalid or excessive media redirects.");
|
|
79
|
+
url = new URL(location, url);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (response.status !== 200) {
|
|
83
|
+
response.body?.cancel().catch(() => {});
|
|
84
|
+
throw new Error(`Media request failed with HTTP ${response.status}.`);
|
|
85
|
+
}
|
|
86
|
+
const bytes = await readBoundedBytes(response, maxBytes, url);
|
|
87
|
+
signal.throwIfAborted();
|
|
88
|
+
if (!await verifyDigestMultibase(bytes, digestMultibase)) throw new Error("Portable media digest does not match.");
|
|
89
|
+
const headers = new Headers();
|
|
90
|
+
const contentType = response.headers.get("Content-Type");
|
|
91
|
+
if (contentType != null) headers.set("Content-Type", contentType);
|
|
92
|
+
return new Response(bytes, { headers });
|
|
93
|
+
}
|
|
94
|
+
throw new TypeError("Too many media redirects.");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Fetches a portable object's external media and returns it only after its
|
|
98
|
+
* SHA-256 `digestMultibase` has been verified. `hl:` resources are tried at
|
|
99
|
+
* the supplied gateways in order. The result contains verified body bytes;
|
|
100
|
+
* its `Content-Type` is unverified metadata supplied by the server.
|
|
101
|
+
*
|
|
102
|
+
* @param media A Link, Image, Document, or URL with an external resource.
|
|
103
|
+
* @param options Retrieval, integrity, and network policy options.
|
|
104
|
+
* @returns A response containing only verified bytes.
|
|
105
|
+
* @throws {TypeError} If the media reference or digest is invalid.
|
|
106
|
+
* @throws {Error} If no source supplies a matching bounded resource.
|
|
107
|
+
* @since 2.4.0
|
|
108
|
+
*/
|
|
109
|
+
async function fetchPortableMedia(media, options = {}) {
|
|
110
|
+
const { url, digestMultibase: propertyDigest } = mediaTarget(media);
|
|
111
|
+
const digestMultibase = propertyDigest ?? options.digestMultibase;
|
|
112
|
+
if (digestMultibase == null) throw new TypeError("Portable media requires digestMultibase.");
|
|
113
|
+
const expected = parseDigestMultibase(digestMultibase).digest;
|
|
114
|
+
const maxBytes = options.maxBytes ?? 16777216;
|
|
115
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The media size limit must be a positive safe integer.");
|
|
116
|
+
const timeout = resolveTimeout(options.timeout, 1e4);
|
|
117
|
+
const gatewayTimeout = resolveTimeout(options.gatewayTimeout, 3e3);
|
|
118
|
+
let targets;
|
|
119
|
+
let hashlink = false;
|
|
120
|
+
if (/^hl:/i.test(url)) {
|
|
121
|
+
hashlink = true;
|
|
122
|
+
const hashDigest = parseDigestMultibase(parseHashlink(url).digestMultibase).digest;
|
|
123
|
+
if (expected.some((byte, i) => byte !== hashDigest[i])) throw new TypeError("Hashlink and digestMultibase disagree.");
|
|
124
|
+
const gateways = [...new Set([...options.gateways ?? []].map((gateway) => parseGatewayUrl(gateway.toString()).origin))];
|
|
125
|
+
if (gateways.length === 0) throw new TypeError("Hashlink media requires at least one gateway.");
|
|
126
|
+
targets = gateways.map((gateway) => new URL(`/.well-known/apgateway/${url}`, gateway));
|
|
127
|
+
} else {
|
|
128
|
+
const target = new URL(url);
|
|
129
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") throw new TypeError("Portable media URLs must use HTTP(S) or hl:.");
|
|
130
|
+
targets = [target];
|
|
131
|
+
}
|
|
132
|
+
return await withTimeout(async (signal) => {
|
|
133
|
+
let lastError;
|
|
134
|
+
for (const target of targets) {
|
|
135
|
+
signal.throwIfAborted();
|
|
136
|
+
try {
|
|
137
|
+
return await withTimeout((attemptSignal) => fetchVerified(target, digestMultibase, options, attemptSignal, maxBytes), signal, hashlink ? gatewayTimeout : null);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
signal.throwIfAborted();
|
|
140
|
+
if (!hashlink) throw error;
|
|
141
|
+
lastError = error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new Error("No gateway returned verified portable media.", { cause: lastError });
|
|
145
|
+
}, options.signal, timeout);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { fetchPortableMedia as t };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const require_request = require("./request-C-VZ9TLj.cjs");
|
|
2
|
+
const require_body = require("./body-T_yQWql3.cjs");
|
|
3
|
+
const require_url = require("./url-BNakuZ8k.cjs");
|
|
4
|
+
const require_digest = require("./digest-3FeH2Y-Q.cjs");
|
|
5
|
+
//#region src/portable-media.ts
|
|
6
|
+
function resolveTimeout(value, fallback) {
|
|
7
|
+
if (value === null) return null;
|
|
8
|
+
const result = value ?? fallback;
|
|
9
|
+
if (!Number.isFinite(result) || result <= 0 || result > 2147483647) throw new RangeError("A media timeout must be a positive finite number of milliseconds.");
|
|
10
|
+
return Math.ceil(result);
|
|
11
|
+
}
|
|
12
|
+
async function withTimeout(operation, parent, timeout) {
|
|
13
|
+
parent?.throwIfAborted();
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const onParentAbort = () => controller.abort(parent?.reason);
|
|
16
|
+
parent?.addEventListener("abort", onParentAbort, { once: true });
|
|
17
|
+
let timer;
|
|
18
|
+
if (timeout != null) timer = setTimeout(() => controller.abort(new DOMException(`Media retrieval timed out after ${timeout} ms.`, "TimeoutError")), timeout);
|
|
19
|
+
let onAbort;
|
|
20
|
+
const aborted = new Promise((_, reject) => {
|
|
21
|
+
onAbort = () => reject(controller.signal.reason);
|
|
22
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
return await Promise.race([operation(controller.signal), aborted]);
|
|
26
|
+
} finally {
|
|
27
|
+
if (timer != null) clearTimeout(timer);
|
|
28
|
+
parent?.removeEventListener("abort", onParentAbort);
|
|
29
|
+
if (onAbort != null) controller.signal.removeEventListener("abort", onAbort);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function mediaTarget(media) {
|
|
33
|
+
if (typeof media === "string" || media instanceof URL) return {
|
|
34
|
+
url: media.toString(),
|
|
35
|
+
digestMultibase: void 0
|
|
36
|
+
};
|
|
37
|
+
const target = media.href ?? media.url;
|
|
38
|
+
const url = target instanceof URL ? target.href : target != null && typeof target === "object" && "href" in target ? target.href?.href : void 0;
|
|
39
|
+
if (url == null) throw new TypeError("Portable media has no URL or href.");
|
|
40
|
+
const linkDigest = target != null && !(target instanceof URL) && typeof target === "object" && "digestMultibase" in target ? target.digestMultibase : void 0;
|
|
41
|
+
if (media.digestMultibase != null && linkDigest != null) {
|
|
42
|
+
const outer = require_digest.parseDigestMultibase(media.digestMultibase).digest;
|
|
43
|
+
const inner = require_digest.parseDigestMultibase(linkDigest).digest;
|
|
44
|
+
if (outer.some((byte, i) => byte !== inner[i])) throw new TypeError("Media and linked resource digests disagree.");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
url,
|
|
48
|
+
digestMultibase: media.digestMultibase ?? linkDigest
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function fetchVerified(url, digestMultibase, options, signal, maxBytes) {
|
|
52
|
+
const visited = /* @__PURE__ */ new Set();
|
|
53
|
+
for (let redirects = 0; redirects <= 20; redirects++) {
|
|
54
|
+
signal.throwIfAborted();
|
|
55
|
+
if (url.username !== "" || url.password !== "") throw new TypeError("Media URLs must not contain credentials.");
|
|
56
|
+
if (!options.allowPrivateAddress) await require_url.validatePublicUrl(url.href);
|
|
57
|
+
else if (url.protocol !== "http:" && url.protocol !== "https:") throw new TypeError("Media URLs must use HTTP(S).");
|
|
58
|
+
if (visited.has(url.href)) throw new TypeError("Media redirect loop.");
|
|
59
|
+
visited.add(url.href);
|
|
60
|
+
const response = await fetch(url, {
|
|
61
|
+
method: "GET",
|
|
62
|
+
headers: {
|
|
63
|
+
Accept: "*/*",
|
|
64
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : require_request.getUserAgent(options.userAgent)
|
|
65
|
+
},
|
|
66
|
+
redirect: "manual",
|
|
67
|
+
signal
|
|
68
|
+
});
|
|
69
|
+
if ([
|
|
70
|
+
301,
|
|
71
|
+
302,
|
|
72
|
+
303,
|
|
73
|
+
307,
|
|
74
|
+
308
|
|
75
|
+
].includes(response.status)) {
|
|
76
|
+
const location = response.headers.get("Location");
|
|
77
|
+
response.body?.cancel().catch(() => {});
|
|
78
|
+
if (location == null || redirects === 20) throw new TypeError("Invalid or excessive media redirects.");
|
|
79
|
+
url = new URL(location, url);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (response.status !== 200) {
|
|
83
|
+
response.body?.cancel().catch(() => {});
|
|
84
|
+
throw new Error(`Media request failed with HTTP ${response.status}.`);
|
|
85
|
+
}
|
|
86
|
+
const bytes = await require_body.readBoundedBytes(response, maxBytes, url);
|
|
87
|
+
signal.throwIfAborted();
|
|
88
|
+
if (!await require_digest.verifyDigestMultibase(bytes, digestMultibase)) throw new Error("Portable media digest does not match.");
|
|
89
|
+
const headers = new Headers();
|
|
90
|
+
const contentType = response.headers.get("Content-Type");
|
|
91
|
+
if (contentType != null) headers.set("Content-Type", contentType);
|
|
92
|
+
return new Response(bytes, { headers });
|
|
93
|
+
}
|
|
94
|
+
throw new TypeError("Too many media redirects.");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Fetches a portable object's external media and returns it only after its
|
|
98
|
+
* SHA-256 `digestMultibase` has been verified. `hl:` resources are tried at
|
|
99
|
+
* the supplied gateways in order. The result contains verified body bytes;
|
|
100
|
+
* its `Content-Type` is unverified metadata supplied by the server.
|
|
101
|
+
*
|
|
102
|
+
* @param media A Link, Image, Document, or URL with an external resource.
|
|
103
|
+
* @param options Retrieval, integrity, and network policy options.
|
|
104
|
+
* @returns A response containing only verified bytes.
|
|
105
|
+
* @throws {TypeError} If the media reference or digest is invalid.
|
|
106
|
+
* @throws {Error} If no source supplies a matching bounded resource.
|
|
107
|
+
* @since 2.4.0
|
|
108
|
+
*/
|
|
109
|
+
async function fetchPortableMedia(media, options = {}) {
|
|
110
|
+
const { url, digestMultibase: propertyDigest } = mediaTarget(media);
|
|
111
|
+
const digestMultibase = propertyDigest ?? options.digestMultibase;
|
|
112
|
+
if (digestMultibase == null) throw new TypeError("Portable media requires digestMultibase.");
|
|
113
|
+
const expected = require_digest.parseDigestMultibase(digestMultibase).digest;
|
|
114
|
+
const maxBytes = options.maxBytes ?? 16777216;
|
|
115
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The media size limit must be a positive safe integer.");
|
|
116
|
+
const timeout = resolveTimeout(options.timeout, 1e4);
|
|
117
|
+
const gatewayTimeout = resolveTimeout(options.gatewayTimeout, 3e3);
|
|
118
|
+
let targets;
|
|
119
|
+
let hashlink = false;
|
|
120
|
+
if (/^hl:/i.test(url)) {
|
|
121
|
+
hashlink = true;
|
|
122
|
+
const hashDigest = require_digest.parseDigestMultibase(require_digest.parseHashlink(url).digestMultibase).digest;
|
|
123
|
+
if (expected.some((byte, i) => byte !== hashDigest[i])) throw new TypeError("Hashlink and digestMultibase disagree.");
|
|
124
|
+
const gateways = [...new Set([...options.gateways ?? []].map((gateway) => require_url.parseGatewayUrl(gateway.toString()).origin))];
|
|
125
|
+
if (gateways.length === 0) throw new TypeError("Hashlink media requires at least one gateway.");
|
|
126
|
+
targets = gateways.map((gateway) => new URL(`/.well-known/apgateway/${url}`, gateway));
|
|
127
|
+
} else {
|
|
128
|
+
const target = new URL(url);
|
|
129
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") throw new TypeError("Portable media URLs must use HTTP(S) or hl:.");
|
|
130
|
+
targets = [target];
|
|
131
|
+
}
|
|
132
|
+
return await withTimeout(async (signal) => {
|
|
133
|
+
let lastError;
|
|
134
|
+
for (const target of targets) {
|
|
135
|
+
signal.throwIfAborted();
|
|
136
|
+
try {
|
|
137
|
+
return await withTimeout((attemptSignal) => fetchVerified(target, digestMultibase, options, attemptSignal, maxBytes), signal, hashlink ? gatewayTimeout : null);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
signal.throwIfAborted();
|
|
140
|
+
if (!hashlink) throw error;
|
|
141
|
+
lastError = error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new Error("No gateway returned verified portable media.", { cause: lastError });
|
|
145
|
+
}, options.signal, timeout);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
Object.defineProperty(exports, "fetchPortableMedia", {
|
|
149
|
+
enumerable: true,
|
|
150
|
+
get: function() {
|
|
151
|
+
return fetchPortableMedia;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
const require_digest = require("./digest-3FeH2Y-Q.cjs");
|
|
2
|
+
const require_portable_media = require("./portable-media-D05JJIJg.cjs");
|
|
3
|
+
let node_assert_strict = require("node:assert/strict");
|
|
4
|
+
let node_test = require("node:test");
|
|
5
|
+
let node_http = require("node:http");
|
|
6
|
+
//#region src/portable-media.test.ts
|
|
7
|
+
const bytes = new TextEncoder().encode("portable image");
|
|
8
|
+
const digest = "zQmU2Jds4B2J83K7CrYC34CEJdB6vCLyqkM2cgBxYeaNWTA";
|
|
9
|
+
const hashlink = require_digest.createHashlink(digest);
|
|
10
|
+
const networkTest = (name, run) => (0, node_test.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) => (0, node_http.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
|
+
]) (0, node_assert_strict.equal)(await (await require_portable_media.fetchPortableMedia(media, {
|
|
66
|
+
gateways: [gateway],
|
|
67
|
+
allowPrivateAddress: true
|
|
68
|
+
})).text(), "portable image");
|
|
69
|
+
});
|
|
70
|
+
(0, node_assert_strict.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 require_portable_media.fetchPortableMedia({
|
|
92
|
+
url: new URL(hashlink),
|
|
93
|
+
digestMultibase: digest
|
|
94
|
+
}, {
|
|
95
|
+
gateways,
|
|
96
|
+
allowPrivateAddress: true
|
|
97
|
+
});
|
|
98
|
+
(0, node_assert_strict.equal)(await result.text(), "portable image");
|
|
99
|
+
(0, node_assert_strict.equal)(result.headers.get("Content-Type"), "image/png");
|
|
100
|
+
(0, node_assert_strict.equal)(result.headers.get("Set-Cookie"), null);
|
|
101
|
+
});
|
|
102
|
+
(0, node_assert_strict.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 require_digest.computeDigestMultibase(/* @__PURE__ */ new Uint8Array());
|
|
110
|
+
await withServers([() => new Response(bytes)], async ([origin]) => {
|
|
111
|
+
(0, node_assert_strict.equal)(await (await require_portable_media.fetchPortableMedia(`${origin}/a`, {
|
|
112
|
+
digestMultibase: digest,
|
|
113
|
+
allowPrivateAddress: true
|
|
114
|
+
})).text(), "portable image");
|
|
115
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia(`${origin}/a`, {
|
|
116
|
+
digestMultibase: wrongDigest,
|
|
117
|
+
allowPrivateAddress: true
|
|
118
|
+
}), /digest does not match/);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
(0, node_test.test)("fetchPortableMedia() rejects invalid references before fetching", async () => {
|
|
122
|
+
const wrongDigest = await require_digest.computeDigestMultibase(/* @__PURE__ */ new Uint8Array());
|
|
123
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia({ url: new URL(hashlink) }), /requires digestMultibase/);
|
|
124
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia({
|
|
125
|
+
url: new URL(hashlink),
|
|
126
|
+
digestMultibase: digest
|
|
127
|
+
}), /requires at least one gateway/);
|
|
128
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia({
|
|
129
|
+
url: new URL(hashlink),
|
|
130
|
+
digestMultibase: wrongDigest
|
|
131
|
+
}, { gateways: ["https://example.com"] }), /disagree/);
|
|
132
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia({
|
|
133
|
+
url: new URL("hl:invalid"),
|
|
134
|
+
digestMultibase: digest
|
|
135
|
+
}, { gateways: ["https://example.com"] }), TypeError);
|
|
136
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia({
|
|
137
|
+
url: {
|
|
138
|
+
href: new URL(hashlink),
|
|
139
|
+
digestMultibase: wrongDigest
|
|
140
|
+
},
|
|
141
|
+
digestMultibase: digest
|
|
142
|
+
}, { gateways: ["https://example.com"] }), /digests disagree/);
|
|
143
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.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 (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia(`${origin}/media`, {
|
|
148
|
+
digestMultibase: digest,
|
|
149
|
+
maxBytes: bytes.length - 1,
|
|
150
|
+
allowPrivateAddress: true
|
|
151
|
+
}), /exceeds the limit/);
|
|
152
|
+
});
|
|
153
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia("https://example.com/media", {
|
|
154
|
+
digestMultibase: digest,
|
|
155
|
+
maxBytes: 0
|
|
156
|
+
}), RangeError);
|
|
157
|
+
});
|
|
158
|
+
(0, node_test.test)("fetchPortableMedia() rejects private media and gateways", async () => {
|
|
159
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia("http://127.0.0.1/media", { digestMultibase: digest }), /private address/);
|
|
160
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.fetchPortableMedia({
|
|
161
|
+
url: new URL(hashlink),
|
|
162
|
+
digestMultibase: digest
|
|
163
|
+
}, { gateways: ["http://127.0.0.1"] }), /No gateway returned verified portable media/);
|
|
164
|
+
});
|
|
165
|
+
(0, node_test.test)("fetchPortableMedia() propagates cancellation", async () => {
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const reason = /* @__PURE__ */ new Error("cancelled");
|
|
168
|
+
controller.abort(reason);
|
|
169
|
+
await (0, node_assert_strict.rejects)(() => require_portable_media.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
|
+
(0, node_assert_strict.equal)(await (await require_portable_media.fetchPortableMedia({
|
|
180
|
+
url: new URL(hashlink),
|
|
181
|
+
digestMultibase: digest
|
|
182
|
+
}, {
|
|
183
|
+
gateways,
|
|
184
|
+
allowPrivateAddress: true,
|
|
185
|
+
gatewayTimeout: 500,
|
|
186
|
+
timeout: 5e3
|
|
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 (0, node_assert_strict.rejects)(() => require_portable_media.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
|
+
(0, node_assert_strict.equal)(await (await require_portable_media.fetchPortableMedia({
|
|
213
|
+
url: new URL(hashlink),
|
|
214
|
+
digestMultibase: digest
|
|
215
|
+
}, {
|
|
216
|
+
gateways: [gateway],
|
|
217
|
+
allowPrivateAddress: true
|
|
218
|
+
})).text(), "portable image");
|
|
219
|
+
});
|
|
220
|
+
(0, node_assert_strict.deepStrictEqual)(requested, [`/.well-known/apgateway/${hashlink}`, "/media"]);
|
|
221
|
+
});
|
|
222
|
+
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|