@fedify/vocab-runtime 2.0.0-dev.1908 → 2.0.0-dev.196

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 (46) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -1
  3. package/deno.json +7 -1
  4. package/dist/chunk-DWy1uDak.cjs +39 -0
  5. package/dist/docloader.test.cjs +5851 -0
  6. package/dist/docloader.test.d.cts +1 -0
  7. package/dist/docloader.test.d.ts +1 -0
  8. package/dist/docloader.test.js +5877 -0
  9. package/dist/key.test.cjs +272 -0
  10. package/dist/key.test.d.cts +1 -0
  11. package/dist/key.test.d.ts +1 -0
  12. package/dist/key.test.js +271 -0
  13. package/dist/langstr.test.cjs +51 -0
  14. package/dist/langstr.test.d.cts +1 -0
  15. package/dist/langstr.test.d.ts +1 -0
  16. package/dist/langstr.test.js +50 -0
  17. package/dist/link-CdFPEo9O.cjs +189 -0
  18. package/dist/link-Ck2yj4dH.js +183 -0
  19. package/dist/link.test.cjs +56 -0
  20. package/dist/link.test.d.cts +1 -0
  21. package/dist/link.test.d.ts +1 -0
  22. package/dist/link.test.js +55 -0
  23. package/dist/mod.cjs +102 -63
  24. package/dist/mod.js +102 -63
  25. package/dist/multibase/multibase.test.cjs +346 -0
  26. package/dist/multibase/multibase.test.d.cts +1 -0
  27. package/dist/multibase/multibase.test.d.ts +1 -0
  28. package/dist/multibase/multibase.test.js +345 -0
  29. package/dist/multibase-BFbBiaPE.cjs +347 -0
  30. package/dist/multibase-DStmqni9.js +311 -0
  31. package/dist/request-BPQb2VYj.cjs +138 -0
  32. package/dist/request-SuYiIZUu.js +108 -0
  33. package/dist/request.test.cjs +44 -0
  34. package/dist/request.test.d.cts +1 -0
  35. package/dist/request.test.d.ts +1 -0
  36. package/dist/request.test.js +43 -0
  37. package/dist/url-C5Vs9nYh.cjs +93 -0
  38. package/dist/url-fW_DHbih.js +63 -0
  39. package/dist/url.test.cjs +37 -0
  40. package/dist/url.test.d.cts +1 -0
  41. package/dist/url.test.d.ts +1 -0
  42. package/dist/url.test.js +36 -0
  43. package/package.json +4 -3
  44. package/src/docloader.test.ts +29 -1
  45. package/src/docloader.ts +101 -45
  46. package/tsdown.config.ts +18 -7
package/src/docloader.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { getLogger } from "@logtape/logtape";
2
+ import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
3
+ import metadata from "../deno.json" with { type: "json" };
2
4
  import preloadedContexts from "./contexts.ts";
3
5
  import { HttpHeaderLink } from "./link.ts";
4
6
  import {
@@ -189,37 +191,55 @@ export async function getRemoteDocument(
189
191
  contentType === "application/xhtml+xml" ||
190
192
  contentType?.startsWith("application/xhtml+xml;"))
191
193
  ) {
192
- const p =
193
- /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig;
194
- const p2 = /\s+([a-z][a-z:_-]*)=("([^"]*)"|'([^']*)'|([^\s>]+))/ig;
194
+ // Security: Limit HTML response size to mitigate ReDoS attacks
195
+ const MAX_HTML_SIZE = 1024 * 1024; // 1MB
195
196
  const html = await response.text();
196
- let m: RegExpExecArray | null;
197
- const rawAttribs: string[] = [];
198
- while ((m = p.exec(html)) !== null) rawAttribs.push(m[2]);
199
- for (const rawAttrs of rawAttribs) {
200
- let m2: RegExpExecArray | null;
201
- const attribs: Record<string, string> = {};
202
- while ((m2 = p2.exec(rawAttrs)) !== null) {
203
- const key = m2[1].toLowerCase();
204
- const value = m2[3] ?? m2[4] ?? m2[5] ?? "";
205
- attribs[key] = value;
206
- }
207
- if (
208
- attribs.rel === "alternate" && "type" in attribs && (
209
- attribs.type === "application/activity+json" ||
210
- attribs.type === "application/ld+json" ||
211
- attribs.type.startsWith("application/ld+json;")
212
- ) && "href" in attribs &&
213
- new URL(attribs.href, docUrl).href !== docUrl.href
214
- ) {
215
- logger.debug(
216
- "Found alternate document: {alternateUrl} from {url}",
217
- { alternateUrl: attribs.href, url: documentUrl },
218
- );
219
- return await fetch(new URL(attribs.href, docUrl).href);
197
+ if (html.length > MAX_HTML_SIZE) {
198
+ logger.warn(
199
+ "HTML response too large, skipping alternate link discovery: {url}",
200
+ { url: documentUrl, size: html.length },
201
+ );
202
+ document = JSON.parse(html);
203
+ } else {
204
+ // Safe regex patterns without nested quantifiers to prevent ReDoS
205
+ // (CVE-2025-68475)
206
+ // Step 1: Extract <a ...> or <link ...> tags
207
+ const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
208
+ // Step 2: Parse attributes
209
+ const attrPattern =
210
+ /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
211
+
212
+ let tagMatch: RegExpExecArray | null;
213
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
214
+ const tagContent = tagMatch[2];
215
+ let attrMatch: RegExpExecArray | null;
216
+ const attribs: Record<string, string> = {};
217
+
218
+ // Reset regex state for attribute parsing
219
+ attrPattern.lastIndex = 0;
220
+ while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
221
+ const key = attrMatch[1].toLowerCase();
222
+ const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
223
+ attribs[key] = value;
224
+ }
225
+
226
+ if (
227
+ attribs.rel === "alternate" && "type" in attribs && (
228
+ attribs.type === "application/activity+json" ||
229
+ attribs.type === "application/ld+json" ||
230
+ attribs.type.startsWith("application/ld+json;")
231
+ ) && "href" in attribs &&
232
+ new URL(attribs.href, docUrl).href !== docUrl.href
233
+ ) {
234
+ logger.debug(
235
+ "Found alternate document: {alternateUrl} from {url}",
236
+ { alternateUrl: attribs.href, url: documentUrl },
237
+ );
238
+ return await fetch(new URL(attribs.href, docUrl).href);
239
+ }
220
240
  }
241
+ document = JSON.parse(html);
221
242
  }
222
- document = JSON.parse(html);
223
243
  } else {
224
244
  document = await response.json();
225
245
  }
@@ -266,6 +286,9 @@ export function getDocumentLoader(
266
286
  { allowPrivateAddress, skipPreloadedContexts, userAgent }:
267
287
  GetDocumentLoaderOptions = {},
268
288
  ): DocumentLoader {
289
+ const tracerProvider = trace.getTracerProvider();
290
+ const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
291
+
269
292
  async function load(
270
293
  url: string,
271
294
  options?: DocumentLoaderOptions,
@@ -289,23 +312,56 @@ export function getDocumentLoader(
289
312
  throw error;
290
313
  }
291
314
  }
292
- const request = createActivityPubRequest(url, { userAgent });
293
- logRequest(logger, request);
294
- const response = await fetch(request, {
295
- // Since Bun has a bug that ignores the `Request.redirect` option,
296
- // to work around it we specify `redirect: "manual"` here too:
297
- // https://github.com/oven-sh/bun/issues/10754
298
- redirect: "manual",
299
- signal: options?.signal,
300
- });
301
- // Follow redirects manually to get the final URL:
302
- if (
303
- response.status >= 300 && response.status < 400 &&
304
- response.headers.has("Location")
305
- ) {
306
- return load(response.headers.get("Location")!, options);
307
- }
308
- return getRemoteDocument(url, response, load);
315
+
316
+ return await tracer.startActiveSpan(
317
+ "activitypub.fetch_document",
318
+ {
319
+ kind: SpanKind.CLIENT,
320
+ attributes: {
321
+ "url.full": url,
322
+ },
323
+ },
324
+ async (span) => {
325
+ try {
326
+ const request = createActivityPubRequest(url, { userAgent });
327
+ logRequest(logger, request);
328
+ const response = await fetch(request, {
329
+ // Since Bun has a bug that ignores the `Request.redirect` option,
330
+ // to work around it we specify `redirect: "manual"` here too:
331
+ // https://github.com/oven-sh/bun/issues/10754
332
+ redirect: "manual",
333
+ signal: options?.signal,
334
+ });
335
+ span.setAttribute("http.response.status_code", response.status);
336
+
337
+ // Follow redirects manually to get the final URL:
338
+ if (
339
+ response.status >= 300 && response.status < 400 &&
340
+ response.headers.has("Location")
341
+ ) {
342
+ const redirectUrl = response.headers.get("Location")!;
343
+ span.setAttribute("http.redirect.url", redirectUrl);
344
+ return await load(redirectUrl, options);
345
+ }
346
+
347
+ const result = await getRemoteDocument(url, response, load);
348
+ span.setAttribute("docloader.document_url", result.documentUrl);
349
+ if (result.contextUrl != null) {
350
+ span.setAttribute("docloader.context_url", result.contextUrl);
351
+ }
352
+ return result;
353
+ } catch (error) {
354
+ span.recordException(error as Error);
355
+ span.setStatus({
356
+ code: SpanStatusCode.ERROR,
357
+ message: String(error),
358
+ });
359
+ throw error;
360
+ } finally {
361
+ span.end();
362
+ }
363
+ },
364
+ );
309
365
  }
310
366
  return load;
311
367
  }
package/tsdown.config.ts CHANGED
@@ -1,9 +1,20 @@
1
+ import { glob } from "node:fs/promises";
2
+ import { sep } from "node:path";
1
3
  import { defineConfig } from "tsdown";
2
4
 
3
- export default defineConfig({
4
- entry: ["src/mod.ts"],
5
- dts: true,
6
- format: ["esm", "cjs"],
7
- platform: "node",
8
- external: [/^node:/],
9
- });
5
+ export default [
6
+ defineConfig({
7
+ entry: ["src/mod.ts"],
8
+ dts: true,
9
+ format: ["esm", "cjs"],
10
+ platform: "neutral",
11
+ external: [/^node:/],
12
+ }),
13
+ defineConfig({
14
+ entry: (await Array.fromAsync(glob(`src/**/*.test.ts`)))
15
+ .map((f) => f.replace(sep, "/")),
16
+ format: ["esm", "cjs"],
17
+ platform: "node",
18
+ external: [/^node:/],
19
+ }),
20
+ ];