@fedify/vocab 2.4.0-dev.2063 → 2.4.0-dev.2065

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.
@@ -1,13 +1,13 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { A as Object$1 } from "./vocab-DZq1JW3s.mjs";
4
- import { i as isActor, o as name, s as version } from "./actor-hpNVHQvY.mjs";
3
+ import { A as Object$1 } from "./vocab-BHtBRusa.mjs";
4
+ import { i as isActor, o as name, s as version } from "./actor-B0IFZjgb.mjs";
5
5
  import { t as getTypeId } from "./type-BMdjm4Eg.mjs";
6
- import { canonicalizePortableUri, formatIri, fromCompatibleEf61Id, getDocumentLoader, haveSameFe34Origin, haveSameIriOrigin, parseIri } from "@fedify/vocab-runtime";
6
+ import { canonicalizePortableUri, formatIri, getDocumentLoader, haveSameFe34Origin, haveSameIriOrigin, parseIri } from "@fedify/vocab-runtime";
7
7
  import { lookupWebFinger } from "@fedify/webfinger";
8
8
  import { SpanStatusCode, trace } from "@opentelemetry/api";
9
9
  import { getLogger } from "@logtape/logtape";
10
- import { PortableObjectRejectedError, dereferencePortableIri, getPortableGatewayCandidates, isPortableIri } from "@fedify/vocab-runtime/internal/portable-dereference";
10
+ import { PortableObjectRejectedError, createSnapshotContextLoader, dereferencePortableIri, getPortableGatewayCandidates, getPortableResponseClaim, isPortableIri, parseCompatibleEf61Reference } from "@fedify/vocab-runtime/internal/portable-dereference";
11
11
  import { delay } from "es-toolkit";
12
12
  //#region src/handle.ts
13
13
  /**
@@ -269,35 +269,50 @@ async function lookupObjectInternal(identifier, options = {}) {
269
269
  }
270
270
  }
271
271
  if (remoteDoc == null) return null;
272
- let object;
273
- let documentUrl;
272
+ const snapshot = options.verifyPortableObject == null ? null : createSnapshotContextLoader(options.contextLoader ?? getDocumentLoader({ userAgent: options.userAgent }));
274
273
  try {
275
- documentUrl = parseIri(remoteDoc.documentUrl);
276
- object = await Object$1.fromJsonLd(remoteDoc.document, {
277
- documentLoader,
278
- contextLoader: options.contextLoader,
279
- tracerProvider: options.tracerProvider,
280
- baseUrl: documentUrl
281
- });
282
- } catch (error) {
283
- if (error instanceof TypeError) {
284
- logger.debug("Failed to parse JSON-LD document: {error}\n{document}", {
274
+ let object;
275
+ let documentUrl;
276
+ try {
277
+ documentUrl = parseIri(remoteDoc.documentUrl);
278
+ object = await Object$1.fromJsonLd(remoteDoc.document, {
279
+ documentLoader,
280
+ contextLoader: snapshot?.loader ?? options.contextLoader,
281
+ tracerProvider: options.tracerProvider,
282
+ baseUrl: documentUrl
283
+ });
284
+ } catch (error) {
285
+ if (error instanceof TypeError) {
286
+ logger.debug("Failed to parse JSON-LD document: {error}\n{document}", {
287
+ ...remoteDoc,
288
+ error
289
+ });
290
+ return null;
291
+ }
292
+ throw error;
293
+ }
294
+ if (snapshot != null) {
295
+ const claim = getPortableResponseClaim(remoteDoc.documentUrl, object.id);
296
+ if (claim === null) {
297
+ logger.debug("Refusing the document {documentUrl}, as it claims to be a portable object with a malformed compatible identifier.", { documentUrl: remoteDoc.documentUrl });
298
+ return null;
299
+ } else if (claim != null) return await dereference(portable, claim.id, claim.inferredGateways, {
300
+ response: remoteDoc,
301
+ contextLoader: snapshot.loader
302
+ });
303
+ }
304
+ if (object.id != null && (options.crossOrigin !== "trust" || isPortableIri(object.id)) && !haveSameIriOrigin(object.id, documentUrl) && !haveSameFe34Origin(object.id, documentUrl)) {
305
+ if (options.crossOrigin === "throw") throw new Error(`The object's @id (${object.id.href}) has a different origin than the document URL (${remoteDoc.documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to "trust".`);
306
+ logger.warn("The object's @id ({objectId}) has a different origin than the document URL ({documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to \"trust\".", {
285
307
  ...remoteDoc,
286
- error
308
+ objectId: object.id.href
287
309
  });
288
310
  return null;
289
311
  }
290
- throw error;
312
+ return object;
313
+ } finally {
314
+ snapshot?.release();
291
315
  }
292
- if (object.id != null && (options.crossOrigin !== "trust" || isPortableIri(object.id)) && !haveSameIriOrigin(object.id, documentUrl) && !haveSameFe34Origin(object.id, documentUrl)) {
293
- if (options.crossOrigin === "throw") throw new Error(`The object's @id (${object.id.href}) has a different origin than the document URL (${remoteDoc.documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to "trust".`);
294
- logger.warn("The object's @id ({objectId}) has a different origin than the document URL ({documentUrl}); refusing to return the object. If you want to bypass this check and are aware of the security implications, set the crossOrigin option to \"trust\".", {
295
- ...remoteDoc,
296
- objectId: object.id.href
297
- });
298
- return null;
299
- }
300
- return object;
301
316
  }
302
317
  const PORTABLE_IRI_PATTERN = /^ap(?:\+ef61)?:/i;
303
318
  /**
@@ -334,25 +349,8 @@ function parsePortableCandidate(iri) {
334
349
  * the gateway to ask.
335
350
  */
336
351
  function getCompatibleCandidate(href, options) {
337
- if (options.verifyPortableObject == null || !URL.canParse(href)) return;
338
- const url = new URL(href);
339
- try {
340
- const id = fromCompatibleEf61Id(url);
341
- if (id == null) return void 0;
342
- return {
343
- id,
344
- gateway: new URL(url.origin)
345
- };
346
- } catch (error) {
347
- if (error instanceof TypeError) {
348
- logger.debug("Invalid FEP-ef61 compatible identifier {href}: {error}", {
349
- href,
350
- error
351
- });
352
- return null;
353
- }
354
- throw error;
355
- }
352
+ if (options.verifyPortableObject == null) return void 0;
353
+ return parseCompatibleEf61Reference(href);
356
354
  }
357
355
  /**
358
356
  * Gets the origin of the WebFinger server that {@link lookupWebFinger} asks
@@ -405,14 +403,15 @@ async function lookupPortableObject(lookup, id, gateways) {
405
403
  lookup.remaining -= selected.length;
406
404
  return await dereference(lookup, id, selected);
407
405
  }
408
- async function dereference({ options, documentLoader }, id, gateways) {
406
+ async function dereference({ options, documentLoader }, id, gateways, extra = {}) {
409
407
  const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
410
408
  try {
411
409
  return await dereferencePortableIri(id, {
412
410
  documentLoader,
413
- contextLoader: options.contextLoader ?? getDocumentLoader({ userAgent: options.userAgent }),
411
+ contextLoader: extra.contextLoader ?? options.contextLoader ?? getDocumentLoader({ userAgent: options.userAgent }),
414
412
  tracerProvider,
415
- gateways,
413
+ inferredGateways: gateways,
414
+ response: extra.response,
416
415
  verifyPortableObject: options.verifyPortableObject,
417
416
  crossOrigin: options.crossOrigin === "throw" ? "throw" : "ignore",
418
417
  signal: options.signal,
@@ -1,8 +1,8 @@
1
1
  import "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { A as Object$1, F as Person, k as Note, s as Collection } from "./vocab-DZq1JW3s.mjs";
4
- import { t as esm_default } from "./esm-CuilqPDu.mjs";
5
- import { n as traverseCollection, t as lookupObject } from "./lookup-Bi6dOTm_.mjs";
3
+ import { A as Object$1, F as Person, k as Note, s as Collection } from "./vocab-BHtBRusa.mjs";
4
+ import { t as esm_default } from "./esm-C9nxP5_e.mjs";
5
+ import { n as traverseCollection, t as lookupObject } from "./lookup-BnzAXNN7.mjs";
6
6
  import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
7
7
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
8
8
  import { deepStrictEqual, equal, ok, rejects } from "node:assert/strict";
@@ -1,9 +1,9 @@
1
1
  import "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { F as Person } from "./vocab-DZq1JW3s.mjs";
4
- import { t as esm_default } from "./esm-CuilqPDu.mjs";
5
- import { n as getActorHandle } from "./actor-hpNVHQvY.mjs";
6
- import { t as lookupObject } from "./lookup-Bi6dOTm_.mjs";
3
+ import { F as Person } from "./vocab-BHtBRusa.mjs";
4
+ import { t as esm_default } from "./esm-C9nxP5_e.mjs";
5
+ import { n as getActorHandle } from "./actor-B0IFZjgb.mjs";
6
+ import { t as lookupObject } from "./lookup-BnzAXNN7.mjs";
7
7
  import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
8
8
  import { mockDocumentLoader, test } from "@fedify/fixture";
9
9
  import { deepStrictEqual, equal, rejects } from "node:assert/strict";
@@ -331,5 +331,101 @@ test("getActorHandle() with FEP-ef61 portable actors", {
331
331
  esm_default.hardReset();
332
332
  }
333
333
  });
334
+ test("lookupObject() reports inferred gateways as hints", async () => {
335
+ const calls = [];
336
+ const verifyPortableObject = async (_, options) => {
337
+ calls.push(options);
338
+ return { verified: true };
339
+ };
340
+ assertInstanceOf(await lookupObject(compatibleId, {
341
+ documentLoader: createLoader({ [compatibleId]: person() }),
342
+ contextLoader: mockDocumentLoader,
343
+ verifyPortableObject
344
+ }), Person);
345
+ deepStrictEqual(calls.length, 1);
346
+ deepStrictEqual(calls[0].gateways, void 0);
347
+ deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
348
+ });
349
+ test("lookupObject() verifies fetched documents that stand for portable objects", async (t) => {
350
+ const plainUrl = "https://example.com/users/alice";
351
+ const redirectingLoader = (documentUrl, document) => {
352
+ const fetched = [];
353
+ const loader = async (url) => {
354
+ fetched.push(url);
355
+ if (url !== plainUrl) throw new FetchError(url, "HTTP 404", new globalThis.Response(null, { status: 404 }));
356
+ return {
357
+ contextUrl: null,
358
+ documentUrl,
359
+ document
360
+ };
361
+ };
362
+ return Object.assign(loader, { fetched });
363
+ };
364
+ await t.step("a redirect to a compatible identifier", async () => {
365
+ const calls = [];
366
+ const documentLoader = redirectingLoader(compatibleId, person());
367
+ const actor = await lookupObject(plainUrl, {
368
+ documentLoader,
369
+ contextLoader: mockDocumentLoader,
370
+ verifyPortableObject: async (_, options) => {
371
+ calls.push(options);
372
+ return { verified: true };
373
+ }
374
+ });
375
+ assertInstanceOf(actor, Person);
376
+ deepStrictEqual(actor.id, parseIri(actorId));
377
+ deepStrictEqual(documentLoader.fetched, [plainUrl]);
378
+ deepStrictEqual(calls.length, 1);
379
+ deepStrictEqual(calls[0].documentUrl, new URL(compatibleId));
380
+ deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
381
+ equal(await lookupObject(plainUrl, {
382
+ documentLoader,
383
+ contextLoader: mockDocumentLoader,
384
+ verifyPortableObject: createVerifier(false)
385
+ }), null);
386
+ equal(await lookupObject(plainUrl, {
387
+ documentLoader: redirectingLoader(compatibleId, person(`ap://${did}/other`)),
388
+ contextLoader: mockDocumentLoader,
389
+ verifyPortableObject: createVerifier()
390
+ }), null);
391
+ });
392
+ await t.step("a compatible @id", async () => {
393
+ const documentLoader = redirectingLoader(plainUrl, person(compatibleId));
394
+ const verifyPortableObject = createVerifier();
395
+ equal(await lookupObject(plainUrl, {
396
+ documentLoader,
397
+ contextLoader: mockDocumentLoader,
398
+ verifyPortableObject
399
+ }), null);
400
+ deepStrictEqual(verifyPortableObject.documents, []);
401
+ const actor = await lookupObject(plainUrl, {
402
+ documentLoader,
403
+ contextLoader: mockDocumentLoader
404
+ });
405
+ assertInstanceOf(actor, Person);
406
+ deepStrictEqual(actor.id, new URL(compatibleId));
407
+ });
408
+ await t.step("a portable @id", async () => {
409
+ const documentLoader = redirectingLoader(plainUrl, person());
410
+ const actor = await lookupObject(plainUrl, {
411
+ documentLoader,
412
+ contextLoader: mockDocumentLoader,
413
+ verifyPortableObject: createVerifier()
414
+ });
415
+ assertInstanceOf(actor, Person);
416
+ deepStrictEqual(actor.id, parseIri(actorId));
417
+ equal(await lookupObject(plainUrl, {
418
+ documentLoader,
419
+ contextLoader: mockDocumentLoader,
420
+ verifyPortableObject: createVerifier(false)
421
+ }), null);
422
+ await rejects(() => lookupObject(plainUrl, {
423
+ documentLoader,
424
+ contextLoader: mockDocumentLoader,
425
+ verifyPortableObject: createVerifier(false),
426
+ crossOrigin: "throw"
427
+ }), /No gateway returned a valid portable object/);
428
+ });
429
+ });
334
430
  //#endregion
335
431
  export {};
@@ -1,6 +1,6 @@
1
1
  import "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { M as OrderedCollection, X as vocab_exports, i as Announce, k as Note, u as Create, z as Question } from "./vocab-DZq1JW3s.mjs";
3
+ import { M as OrderedCollection, X as vocab_exports, i as Announce, k as Note, u as Create, z as Question } from "./vocab-BHtBRusa.mjs";
4
4
  import { mockDocumentLoader, test } from "@fedify/fixture";
5
5
  import { deepStrictEqual, ok } from "node:assert/strict";
6
6
  import { isMarkerSafeContext, retainSignedRepresentation } from "@fedify/vocab-runtime/internal/signed-representation";
@@ -1,8 +1,8 @@
1
1
  import "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { A as Object$1, E as Link, F as Person, K as Translation, L as PropertyValue, P as Organization, U as Service, a as Application, b as Group, k as Note, o as Article, q as Update, s as Collection, u as Create } from "./vocab-DZq1JW3s.mjs";
4
- import "./actor-hpNVHQvY.mjs";
5
- import "./lookup-Bi6dOTm_.mjs";
3
+ import { A as Object$1, E as Link, F as Person, K as Translation, L as PropertyValue, P as Organization, U as Service, a as Application, b as Group, k as Note, o as Article, q as Update, s as Collection, u as Create } from "./vocab-BHtBRusa.mjs";
4
+ import "./actor-B0IFZjgb.mjs";
5
+ import "./lookup-BnzAXNN7.mjs";
6
6
  import { test } from "@fedify/fixture";
7
7
  import { deepStrictEqual, ok, rejects } from "node:assert/strict";
8
8
  import { LanguageString as LanguageString$1, getDocumentLoader, preloadedContexts } from "@fedify/vocab-runtime";
@@ -1,6 +1,6 @@
1
1
  import "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { E as Link, F as Person, J as getEntityTypeById, Y as isEntityType } from "./vocab-DZq1JW3s.mjs";
3
+ import { E as Link, F as Person, J as getEntityTypeById, Y as isEntityType } from "./vocab-BHtBRusa.mjs";
4
4
  import { t as getTypeId } from "./type-BMdjm4Eg.mjs";
5
5
  import { test } from "@fedify/fixture";
6
6
  import { deepStrictEqual } from "node:assert/strict";