@fedify/vocab-runtime 2.4.0-pr.934.40 → 2.4.0

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 (140) hide show
  1. package/deno.json +4 -2
  2. package/dist/{tests/docloader-Xi61Le8w.mjs → contexts-BK1CqBR5.cjs} +164 -269
  3. package/dist/{tests/docloader-BFedvemb.cjs → contexts-DPuJ4UYL.js} +160 -288
  4. package/dist/{docloader-DnUMWHaJ.d.cts → docloader-CYwqh5Df.d.cts} +68 -2
  5. package/dist/{docloader-xRGn1azD.d.ts → docloader-CYwqh5Df.d.ts} +68 -2
  6. package/dist/internal/jsonld-cache.cjs +70 -1
  7. package/dist/internal/jsonld-cache.d.cts +35 -2
  8. package/dist/internal/jsonld-cache.d.ts +35 -2
  9. package/dist/internal/jsonld-cache.js +67 -2
  10. package/dist/internal/portable-dereference.cjs +721 -0
  11. package/dist/internal/portable-dereference.d.cts +349 -0
  12. package/dist/internal/portable-dereference.d.ts +349 -0
  13. package/dist/internal/portable-dereference.js +702 -0
  14. package/dist/internal/signed-representation.cjs +378 -0
  15. package/dist/internal/signed-representation.d.cts +144 -0
  16. package/dist/internal/signed-representation.d.ts +144 -0
  17. package/dist/internal/signed-representation.js +371 -0
  18. package/dist/jsonld.cjs +2 -2
  19. package/dist/mod.cjs +715 -4523
  20. package/dist/mod.d.cts +406 -6
  21. package/dist/mod.d.ts +406 -7
  22. package/dist/mod.js +694 -4518
  23. package/dist/portable-DqtfLy_1.d.cts +160 -0
  24. package/dist/portable-DtWsu2yU.d.ts +160 -0
  25. package/dist/tests/body-CYiu9CO-.mjs +130 -0
  26. package/dist/tests/body-CkEwROsn.cjs +153 -0
  27. package/dist/tests/body.test.cjs +89 -0
  28. package/dist/tests/body.test.d.cts +1 -0
  29. package/dist/tests/body.test.d.mts +1 -0
  30. package/dist/tests/body.test.mjs +90 -0
  31. package/dist/tests/contexts-CIKsin4e.mjs +4512 -0
  32. package/dist/tests/contexts-DizzBjz4.cjs +4523 -0
  33. package/dist/tests/decimal.test.cjs +8 -7
  34. package/dist/tests/decimal.test.mjs +8 -6
  35. package/dist/tests/digest-3FeH2Y-Q.cjs +176 -0
  36. package/dist/tests/digest-COC7xDiQ.mjs +141 -0
  37. package/dist/tests/digest.test.cjs +102 -0
  38. package/dist/tests/digest.test.d.cts +1 -0
  39. package/dist/tests/digest.test.d.mts +1 -0
  40. package/dist/tests/digest.test.mjs +103 -0
  41. package/dist/tests/docloader-C3YFl6y4.cjs +396 -0
  42. package/dist/tests/docloader-CvmS4sVq.mjs +373 -0
  43. package/dist/tests/docloader.test.cjs +692 -39
  44. package/dist/tests/docloader.test.mjs +686 -34
  45. package/dist/tests/internal/multicodec.test.cjs +2 -3
  46. package/dist/tests/internal/multicodec.test.mjs +2 -2
  47. package/dist/tests/internal/portable-dereference.test.cjs +172 -0
  48. package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
  49. package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
  50. package/dist/tests/internal/portable-dereference.test.mjs +173 -0
  51. package/dist/tests/jsonld-cache-BPQmOZWD.mjs +342 -0
  52. package/dist/tests/jsonld-cache-C07AyNOY.cjs +397 -0
  53. package/dist/tests/jsonld-cache.test.cjs +101 -298
  54. package/dist/tests/jsonld-cache.test.mjs +94 -289
  55. package/dist/tests/{key-_wXwomh_.cjs → key-C-AYkdJJ.cjs} +11 -4
  56. package/dist/tests/{key-CDGDH_vC.mjs → key-C2Db_TAJ.mjs} +11 -3
  57. package/dist/tests/key.test.cjs +6 -5
  58. package/dist/tests/key.test.mjs +6 -4
  59. package/dist/tests/langstr.test.cjs +4 -4
  60. package/dist/tests/langstr.test.mjs +2 -2
  61. package/dist/tests/link.test.cjs +2 -3
  62. package/dist/tests/link.test.mjs +2 -2
  63. package/dist/tests/multibase/multibase.test.cjs +8 -9
  64. package/dist/tests/multibase/multibase.test.mjs +6 -6
  65. package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-B5Mea7Ip.cjs} +19 -2
  66. package/dist/tests/{multibase-B4bvakyA.mjs → multibase-BPnF_L4e.mjs} +12 -1
  67. package/dist/tests/portable-dereference-BNXtgg5U.cjs +278 -0
  68. package/dist/tests/portable-dereference-DRE5bz-l.mjs +249 -0
  69. package/dist/tests/portable-media-BLKllU56.cjs +153 -0
  70. package/dist/tests/portable-media-DRSGBORB.mjs +148 -0
  71. package/dist/tests/portable-media.test.cjs +222 -0
  72. package/dist/tests/portable-media.test.d.cts +1 -0
  73. package/dist/tests/portable-media.test.d.mts +1 -0
  74. package/dist/tests/portable-media.test.mjs +223 -0
  75. package/dist/tests/portable-workers.test.cjs +36 -0
  76. package/dist/tests/portable-workers.test.d.cts +2 -0
  77. package/dist/tests/portable-workers.test.d.mts +2 -0
  78. package/dist/tests/portable-workers.test.mjs +35 -0
  79. package/dist/tests/{request-cCPgOxYG.cjs → request-8vPWtV1-.cjs} +10 -4
  80. package/dist/tests/{request-BOS-hNaf.mjs → request-DRaOaTqD.mjs} +8 -2
  81. package/dist/tests/request.test.cjs +7 -4
  82. package/dist/tests/request.test.mjs +5 -2
  83. package/dist/tests/signed-representation.test.cjs +588 -0
  84. package/dist/tests/signed-representation.test.d.cts +1 -0
  85. package/dist/tests/signed-representation.test.d.mts +1 -0
  86. package/dist/tests/signed-representation.test.mjs +589 -0
  87. package/dist/tests/temporal.test.cjs +1 -2
  88. package/dist/tests/temporal.test.mjs +1 -1
  89. package/dist/tests/url-BNakuZ8k.cjs +998 -0
  90. package/dist/tests/url-DMxmp7ZG.mjs +859 -0
  91. package/dist/tests/url.test.cjs +503 -3
  92. package/dist/tests/url.test.mjs +503 -2
  93. package/dist/url-DrGTR8yv.cjs +993 -0
  94. package/dist/url-Dzyp-NsC.js +860 -0
  95. package/package.json +27 -4
  96. package/scripts/test-bun.mjs +17 -0
  97. package/src/body.test.ts +125 -0
  98. package/src/body.ts +152 -0
  99. package/src/contexts/cid-v1.json +114 -0
  100. package/src/contexts/fep-22cd.json +21 -0
  101. package/src/contexts/fep-ef61.json +10 -0
  102. package/src/contexts/miscellany.json +17 -0
  103. package/src/contexts/security-data-integrity-v1.json +0 -4
  104. package/src/contexts.ts +40 -0
  105. package/src/digest.test.ts +220 -0
  106. package/src/digest.ts +229 -0
  107. package/src/docloader.test.ts +918 -27
  108. package/src/docloader.ts +322 -105
  109. package/src/internal/jsonld-cache.ts +97 -1
  110. package/src/internal/portable-dereference.test.ts +254 -0
  111. package/src/internal/portable-dereference.ts +1000 -0
  112. package/src/internal/signed-representation.ts +565 -0
  113. package/src/jsonld-cache.test.ts +89 -0
  114. package/src/key.test.ts +9 -0
  115. package/src/key.ts +11 -1
  116. package/src/mod.ts +30 -0
  117. package/src/multibase/multibase.test.ts +5 -5
  118. package/src/portable-media.test.ts +293 -0
  119. package/src/portable-media.ts +264 -0
  120. package/src/portable-workers.test.ts +81 -0
  121. package/src/portable.ts +180 -0
  122. package/src/preprocessor.ts +7 -0
  123. package/src/request.test.ts +10 -1
  124. package/src/request.ts +9 -1
  125. package/src/signed-representation.test.ts +338 -0
  126. package/src/url.test.ts +844 -1
  127. package/src/url.ts +717 -17
  128. package/tsdown.config.ts +2 -0
  129. package/dist/tests/url-BvjYQdxL.cjs +0 -456
  130. package/dist/tests/url-a2D8NAgh.mjs +0 -378
  131. package/dist/url-Ck3dGEwH.cjs +0 -457
  132. package/dist/url-m1YxGNZ0.js +0 -379
  133. /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
  134. /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-C4Fl80ae.cjs} +0 -0
  135. /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-CQ26J_L7.mjs} +0 -0
  136. /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
  137. /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
  138. /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
  139. /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
  140. /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
@@ -0,0 +1,153 @@
1
+ const require_request = require("./request-8vPWtV1-.cjs");
2
+ const require_body = require("./body-CkEwROsn.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,148 @@
1
+ import { r as getUserAgent } from "./request-DRaOaTqD.mjs";
2
+ import { r as readBoundedBytes } from "./body-CYiu9CO-.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,222 @@
1
+ const require_digest = require("./digest-3FeH2Y-Q.cjs");
2
+ const require_portable_media = require("./portable-media-BLKllU56.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 { };