@fedify/vocab-runtime 2.4.0-dev.1964 → 2.4.0-dev.1995

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 (42) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +176 -152
  3. package/dist/mod.js +176 -152
  4. package/dist/tests/body-B0an0K5j.cjs +94 -0
  5. package/dist/tests/body-DTu3lmhg.mjs +77 -0
  6. package/dist/tests/body.test.cjs +89 -0
  7. package/dist/tests/body.test.d.cts +1 -0
  8. package/dist/tests/body.test.d.mts +1 -0
  9. package/dist/tests/body.test.mjs +90 -0
  10. package/dist/tests/decimal.test.cjs +2 -2
  11. package/dist/tests/decimal.test.mjs +2 -2
  12. package/dist/tests/digest.test.cjs +1 -1
  13. package/dist/tests/digest.test.mjs +1 -1
  14. package/dist/tests/{docloader-B3auVdRI.cjs → docloader-BiGP4BSX.cjs} +33 -82
  15. package/dist/tests/{docloader-BexVAGvr.mjs → docloader-DmFInJaI.mjs} +33 -82
  16. package/dist/tests/docloader.test.cjs +84 -5
  17. package/dist/tests/docloader.test.mjs +84 -5
  18. package/dist/tests/internal/multicodec.test.cjs +1 -1
  19. package/dist/tests/internal/multicodec.test.mjs +1 -1
  20. package/dist/tests/jsonld-cache.test.cjs +1 -1
  21. package/dist/tests/jsonld-cache.test.mjs +1 -1
  22. package/dist/tests/key.test.cjs +1 -1
  23. package/dist/tests/key.test.mjs +1 -1
  24. package/dist/tests/langstr.test.cjs +1 -1
  25. package/dist/tests/langstr.test.mjs +1 -1
  26. package/dist/tests/link.test.cjs +1 -1
  27. package/dist/tests/link.test.mjs +1 -1
  28. package/dist/tests/multibase/multibase.test.cjs +1 -1
  29. package/dist/tests/multibase/multibase.test.mjs +1 -1
  30. package/dist/tests/{request-C8BUtPj4.mjs → request-BPFUWEsX.mjs} +1 -1
  31. package/dist/tests/{request-CRO9h2AO.cjs → request-CAmmk2G7.cjs} +1 -1
  32. package/dist/tests/request.test.cjs +2 -2
  33. package/dist/tests/request.test.mjs +2 -2
  34. package/dist/tests/temporal.test.cjs +1 -1
  35. package/dist/tests/temporal.test.mjs +1 -1
  36. package/dist/tests/url.test.cjs +1 -1
  37. package/dist/tests/url.test.mjs +1 -1
  38. package/package.json +1 -1
  39. package/src/body.test.ts +125 -0
  40. package/src/body.ts +95 -0
  41. package/src/docloader.test.ts +128 -3
  42. package/src/docloader.ts +52 -93
package/src/docloader.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
2
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
3
3
  import metadata from "../deno.json" with { type: "json" };
4
+ import { BodyTooLargeError, MAX_BODY_SIZE, readBoundedText } from "./body.ts";
4
5
  import preloadedContexts from "./contexts.ts";
5
6
  import { HttpHeaderLink } from "./link.ts";
6
7
  import {
@@ -121,51 +122,6 @@ function createResponseMetadata(response: Response): Response {
121
122
  });
122
123
  }
123
124
 
124
- async function cancelResponseBody(response: Response): Promise<void> {
125
- if (response.body != null) {
126
- await response.body.cancel();
127
- }
128
- }
129
-
130
- async function readBoundedText(
131
- response: Response,
132
- maxBytes: number,
133
- ): Promise<{ text: string; size: number; tooLarge: boolean }> {
134
- const contentLength = response.headers.get("Content-Length");
135
- if (contentLength != null) {
136
- const size = Number(contentLength);
137
- if (size > maxBytes) {
138
- await cancelResponseBody(response);
139
- return { text: "", size, tooLarge: true };
140
- }
141
- }
142
-
143
- if (response.body == null) return { text: "", size: 0, tooLarge: false };
144
-
145
- const reader = response.body.getReader();
146
- const decoder = new TextDecoder();
147
- let text = "";
148
- let size = 0;
149
- try {
150
- while (true) {
151
- const result = await reader.read();
152
- if (result.done) break;
153
- const chunkSize = result.value.byteLength;
154
- if (size + chunkSize > maxBytes) {
155
- size += chunkSize;
156
- await reader.cancel();
157
- return { text: "", size, tooLarge: true };
158
- }
159
- size += chunkSize;
160
- text += decoder.decode(result.value, { stream: true });
161
- }
162
- text += decoder.decode();
163
- return { text, size, tooLarge: false };
164
- } finally {
165
- reader.releaseLock();
166
- }
167
- }
168
-
169
125
  /**
170
126
  * Gets a {@link RemoteDocument} from the given response.
171
127
  * @param url The URL of the document to load.
@@ -255,11 +211,14 @@ export async function getRemoteDocument(
255
211
  contentType?.startsWith("application/xhtml+xml;"))
256
212
  ) {
257
213
  const errorResponse = createResponseMetadata(response);
258
- const html = await readBoundedText(response, MAX_HTML_SIZE);
259
- if (html.tooLarge) {
214
+ let html: string;
215
+ try {
216
+ html = await readBoundedText(response, MAX_HTML_SIZE, documentUrl);
217
+ } catch (error) {
218
+ if (!(error instanceof BodyTooLargeError)) throw error;
260
219
  logger.warn(
261
220
  "HTML response too large, skipping alternate link discovery: {url}",
262
- { url: documentUrl, size: html.size },
221
+ { url: documentUrl, maxBytes: MAX_HTML_SIZE },
263
222
  );
264
223
  throw new FetchError(
265
224
  documentUrl,
@@ -267,58 +226,58 @@ export async function getRemoteDocument(
267
226
  `(Content-Type: ${contentType})`,
268
227
  errorResponse,
269
228
  );
270
- } else {
271
- // Safe regex patterns without nested quantifiers to prevent ReDoS
272
- // (CVE-2025-68475)
273
- // Step 1: Extract <a ...> or <link ...> tags
274
- const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
275
- // Step 2: Parse attributes
276
- const attrPattern =
277
- /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
278
-
279
- let tagMatch: RegExpExecArray | null;
280
- while ((tagMatch = tagPattern.exec(html.text)) !== null) {
281
- const tagContent = tagMatch[2];
282
- let attrMatch: RegExpExecArray | null;
283
- const attribs: Record<string, string> = {};
229
+ }
230
+ // Safe regex patterns without nested quantifiers to prevent ReDoS
231
+ // (CVE-2025-68475)
232
+ // Step 1: Extract <a ...> or <link ...> tags
233
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
234
+ // Step 2: Parse attributes
235
+ const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
284
236
 
285
- // Reset regex state for attribute parsing
286
- attrPattern.lastIndex = 0;
287
- while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
288
- const key = attrMatch[1].toLowerCase();
289
- const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
290
- attribs[key] = value;
291
- }
237
+ let tagMatch: RegExpExecArray | null;
238
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
239
+ const tagContent = tagMatch[2];
240
+ let attrMatch: RegExpExecArray | null;
241
+ const attribs: Record<string, string> = {};
292
242
 
293
- if (
294
- attribs.rel === "alternate" && "type" in attribs && (
295
- attribs.type === "application/activity+json" ||
296
- attribs.type === "application/ld+json" ||
297
- attribs.type.startsWith("application/ld+json;")
298
- ) && "href" in attribs &&
299
- new URL(attribs.href, docUrl).href !== docUrl.href
300
- ) {
301
- logger.debug(
302
- "Found alternate document: {alternateUrl} from {url}",
303
- { alternateUrl: attribs.href, url: documentUrl },
304
- );
305
- return await fetch(new URL(attribs.href, docUrl).href);
306
- }
243
+ // Reset regex state for attribute parsing
244
+ attrPattern.lastIndex = 0;
245
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
246
+ const key = attrMatch[1].toLowerCase();
247
+ const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
248
+ attribs[key] = value;
307
249
  }
308
- try {
309
- document = JSON.parse(html.text);
310
- } catch (error) {
311
- if (!(error instanceof SyntaxError)) throw error;
312
- throw new FetchError(
313
- documentUrl,
314
- `HTML document has no ActivityPub alternate link ` +
315
- `(Content-Type: ${contentType})`,
316
- errorResponse,
250
+
251
+ if (
252
+ attribs.rel === "alternate" && "type" in attribs && (
253
+ attribs.type === "application/activity+json" ||
254
+ attribs.type === "application/ld+json" ||
255
+ attribs.type.startsWith("application/ld+json;")
256
+ ) && "href" in attribs &&
257
+ new URL(attribs.href, docUrl).href !== docUrl.href
258
+ ) {
259
+ logger.debug(
260
+ "Found alternate document: {alternateUrl} from {url}",
261
+ { alternateUrl: attribs.href, url: documentUrl },
317
262
  );
263
+ return await fetch(new URL(attribs.href, docUrl).href);
318
264
  }
319
265
  }
266
+ try {
267
+ document = JSON.parse(html);
268
+ } catch (error) {
269
+ if (!(error instanceof SyntaxError)) throw error;
270
+ throw new FetchError(
271
+ documentUrl,
272
+ `HTML document has no ActivityPub alternate link ` +
273
+ `(Content-Type: ${contentType})`,
274
+ errorResponse,
275
+ );
276
+ }
320
277
  } else {
321
- document = await response.json();
278
+ document = JSON.parse(
279
+ await readBoundedText(response, MAX_BODY_SIZE, documentUrl),
280
+ );
322
281
  }
323
282
  logger.debug(
324
283
  "Fetched document: {status} {url} {headers}",