@fedify/vocab 2.4.0-pr.936.41 → 2.4.1

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 (36) hide show
  1. package/deno.json +1 -1
  2. package/dist/mod.cjs +32014 -9323
  3. package/dist/mod.d.cts +4770 -91
  4. package/dist/mod.d.ts +4770 -91
  5. package/dist/mod.js +31907 -9223
  6. package/dist-tests/actor-D_QaCMro.mjs +276 -0
  7. package/dist-tests/actor.test.mjs +55 -49
  8. package/dist-tests/{actor-MBpzF3-D.mjs → esm-YWo4aQQe.mjs} +15 -211
  9. package/dist-tests/gateway.test.mjs +1906 -0
  10. package/dist-tests/lookup-BevjwZhB.mjs +480 -0
  11. package/dist-tests/lookup.test.mjs +10 -268
  12. package/dist-tests/object.yaml +15 -0
  13. package/dist-tests/portable-lookup.test.mjs +653 -0
  14. package/dist-tests/signed-representation.test.mjs +189 -0
  15. package/dist-tests/translation.test.mjs +404 -0
  16. package/dist-tests/translation.yaml +91 -0
  17. package/dist-tests/type.test.mjs +2 -2
  18. package/dist-tests/{vocab-Bjwctrxn.mjs → vocab-DaniQwU7.mjs} +31669 -9267
  19. package/dist-tests/vocab.test.mjs +901 -11
  20. package/package.json +5 -5
  21. package/src/__snapshots__/vocab.test.ts.snap +291 -15
  22. package/src/actor.test.ts +41 -40
  23. package/src/actor.ts +120 -1
  24. package/src/gateway.test.ts +2637 -0
  25. package/src/lookup.test.ts +6 -5
  26. package/src/lookup.ts +460 -33
  27. package/src/mod.ts +9 -0
  28. package/src/object.yaml +15 -0
  29. package/src/portable-lookup.test.ts +889 -0
  30. package/src/preprocessors.ts +1 -1
  31. package/src/signed-representation.test.ts +277 -0
  32. package/src/translation.test.ts +518 -0
  33. package/src/translation.yaml +91 -0
  34. package/src/vocab.test.ts +1219 -5
  35. /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
  36. /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
@@ -1,13 +1,15 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { A as OrderedCollectionPage, B as Source, C as InteractionRule, D as Note, E as Multikey, F as Question, I as QuoteAuthorization, L as QuoteRequest, M as Person, N as Place, O as Object$1, P as Proposal, R as Reject, S as InteractionPolicy, T as Measure, V as Tombstone, W as vocab_exports, _ as Follow, a as Application, b as Image, c as Create, d as Document, f as Endpoints, g as FeaturedItem, h as FeaturedCollection, i as Announce, j as Organization, k as Offer, l as CryptographicKey, m as FeatureRequest, n as Activity, o as Collection, p as FeatureAuthorization, r as Agreement, s as Commitment, t as Accept, u as Delete, v as Group, w as Link, x as Intent, y as Hashtag, z as Service } from "./vocab-Bjwctrxn.mjs";
4
- import { t as assertInstanceOf } from "./utils-CQjrpUkt.mjs";
3
+ import { A as Object$1, B as QuoteAuthorization, C as Intent, D as Measure, E as Link, F as Person, G as Tombstone, H as Reject, I as Place, L as PropertyValue, N as OrderedCollectionPage, O as Multikey, P as Organization, R as Proposal, S as Image, T as InteractionRule, U as Service, V as QuoteRequest, W as Source, X as vocab_exports, _ as FeaturedCollection, a as Application, b as Group, d as CryptographicKey, f as Delete, g as FeatureRequest, h as FeatureAuthorization, i as Announce, j as Offer, k as Note, l as Commitment, m as Endpoints, n as Activity, p as Document, r as Agreement, s as Collection, t as Accept, u as Create, v as FeaturedItem, w as InteractionPolicy, x as Hashtag, y as Follow, z as Question } from "./vocab-DaniQwU7.mjs";
4
+ import { t as esm_default } from "./esm-YWo4aQQe.mjs";
5
+ import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
5
6
  import { mockDocumentLoader, test } from "@fedify/fixture";
6
- import { deepStrictEqual, notDeepStrictEqual, ok, rejects, throws } from "node:assert/strict";
7
+ import { deepStrictEqual, notDeepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
8
+ import { FetchError, LanguageString, decodeMultibase, getDocumentLoader, parseDecimal, parseIri } from "@fedify/vocab-runtime";
7
9
  import { configure, reset } from "@logtape/logtape";
8
- import { LanguageString, decodeMultibase, parseDecimal } from "@fedify/vocab-runtime";
9
10
  import { pascalCase } from "es-toolkit";
10
11
  import { areAllScalarTypes, loadSchemaFiles } from "@fedify/vocab-tools";
12
+ import { inspect } from "node:util";
11
13
  //#region src/vocab.test.ts
12
14
  const NOTE_QUOTE_CONTEXT = [
13
15
  "https://www.w3.org/ns/activitystreams",
@@ -249,6 +251,91 @@ test("Object.toJsonLd()", async () => {
249
251
  }
250
252
  });
251
253
  });
254
+ test("Note.fromJsonLd() ignores malformed language tags", async () => {
255
+ const note = await Note.fromJsonLd({
256
+ "@context": "https://www.w3.org/ns/activitystreams",
257
+ type: "Note",
258
+ contentMap: {
259
+ invalid_tag: "Hello",
260
+ en: "Hi"
261
+ }
262
+ });
263
+ deepStrictEqual(note.contents, [new LanguageString("Hi", "en")]);
264
+ deepStrictEqual((await note.toJsonLd()).contentMap, { en: "Hi" });
265
+ });
266
+ test("Note.fromJsonLd() ignores malformed language tags in nested objects", async () => {
267
+ const note = await Note.fromJsonLd({
268
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
269
+ "https://www.w3.org/ns/activitystreams#attachment": [{
270
+ "@type": ["http://schema.org#PropertyValue"],
271
+ "https://www.w3.org/ns/activitystreams#name": [{
272
+ "@value": "Malformed",
273
+ "@language": "invalid_tag"
274
+ }, {
275
+ "@value": "Valid",
276
+ "@language": "en"
277
+ }]
278
+ }]
279
+ });
280
+ deepStrictEqual(await Array.fromAsync(note.getAttachments()), [new PropertyValue({ name: new LanguageString("Valid", "en") })]);
281
+ const attachment = (await note.toJsonLd()).attachment;
282
+ deepStrictEqual(attachment.nameMap, { en: "Valid" });
283
+ });
284
+ test("Note.fromJsonLd() normalizes extlang language tags", async () => {
285
+ const json = {
286
+ "@context": "https://www.w3.org/ns/activitystreams",
287
+ type: "Note",
288
+ content: "<p>早晨</p>",
289
+ contentMap: {
290
+ "zh-YUE": "<p>早晨</p>",
291
+ "yue": "<p>早晨呀</p>"
292
+ }
293
+ };
294
+ const note = await Note.fromJsonLd(json);
295
+ deepStrictEqual(note.contents, [
296
+ "<p>早晨</p>",
297
+ new LanguageString("<p>早晨呀</p>", "yue"),
298
+ new LanguageString("<p>早晨</p>", "yue")
299
+ ]);
300
+ deepStrictEqual(note.contents.map((c) => typeof c === "string" ? null : c.locale.baseName), [
301
+ null,
302
+ "yue",
303
+ "yue"
304
+ ]);
305
+ deepStrictEqual(await note.toJsonLd(), json);
306
+ deepStrictEqual((await note.toJsonLd({ format: "compact" })).contentMap, { yue: ["<p>早晨呀</p>", "<p>早晨</p>"] });
307
+ deepStrictEqual((await note.toJsonLd({ format: "expand" }))[0]["https://www.w3.org/ns/activitystreams#content"], [
308
+ { "@value": "<p>早晨</p>" },
309
+ {
310
+ "@value": "<p>早晨呀</p>",
311
+ "@language": "yue"
312
+ },
313
+ {
314
+ "@value": "<p>早晨</p>",
315
+ "@language": "yue"
316
+ }
317
+ ]);
318
+ const expandedNote = await Note.fromJsonLd({
319
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
320
+ "https://www.w3.org/ns/activitystreams#name": [{
321
+ "@value": "早晨",
322
+ "@language": "zh-yue-HK"
323
+ }]
324
+ });
325
+ deepStrictEqual(expandedNote.names, [new LanguageString("早晨", "yue-HK")]);
326
+ deepStrictEqual(expandedNote.names[0].locale.baseName, "yue-HK");
327
+ });
328
+ test("Note.fromJsonLd() caches original language tags", async () => {
329
+ const json = {
330
+ "@context": "https://www.w3.org/ns/activitystreams",
331
+ type: "Note",
332
+ contentMap: { "EN-us": "Hello" }
333
+ };
334
+ const note = await Note.fromJsonLd(json);
335
+ deepStrictEqual(note.contents, [new LanguageString("Hello", "en-US")]);
336
+ deepStrictEqual(note.contents[0].locale.baseName, "en-US");
337
+ deepStrictEqual(await note.toJsonLd(), json);
338
+ });
252
339
  test("Note.toJsonLd()", async () => {
253
340
  const note = new Note({ tags: [new Hashtag({
254
341
  name: "#Fedify",
@@ -380,6 +467,22 @@ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
380
467
  deepStrictEqual(pageJson.next, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=2");
381
468
  deepStrictEqual(pageJson.prev, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=1");
382
469
  });
470
+ test("fromJsonLd() refuses portable IDs changed by URL parsing", async () => {
471
+ const base = "ap://did:key:z6Mkabc";
472
+ for (const id of [
473
+ `${base}/a/../b`,
474
+ `${base}/a/%2e%2E/b`,
475
+ "https://gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b",
476
+ "https:/gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b"
477
+ ]) await rejects(() => Note.fromJsonLd({
478
+ "@context": "https://www.w3.org/ns/activitystreams",
479
+ type: "Note",
480
+ id
481
+ }, {
482
+ documentLoader: mockDocumentLoader,
483
+ contextLoader: mockDocumentLoader
484
+ }), TypeError);
485
+ });
383
486
  test("FEP-ef61: actor gateways round-trip as an ordered URI list", async () => {
384
487
  const actorClasses = [
385
488
  Application,
@@ -397,17 +500,112 @@ test("FEP-ef61: actor gateways round-trip as an ordered URI list", async () => {
397
500
  gateways: gateways.map((gateway) => gateway.href)
398
501
  });
399
502
  deepStrictEqual(actor.gateways, gateways);
400
- const jsonLd = await actor.toJsonLd();
503
+ const jsonLd = await actor.toJsonLd({ format: "compact" });
401
504
  deepStrictEqual(jsonLd.type, ActorClass.name);
402
- deepStrictEqual(jsonLd.gateways, gateways.map((gateway) => gateway.href));
505
+ deepStrictEqual(jsonLd.gateways, gateways.map((gateway) => gateway.origin));
403
506
  deepStrictEqual((await ActorClass.fromJsonLd(jsonLd)).gateways, gateways);
404
507
  }
405
508
  });
509
+ test("FEP-ef61: actor gateways serialize as origins", async () => {
510
+ const actorClasses = [
511
+ Application,
512
+ Group,
513
+ Organization,
514
+ Person,
515
+ Service
516
+ ];
517
+ const gateways = [
518
+ new URL("https://first.example:443/"),
519
+ new URL("http://second.example:80/"),
520
+ new URL("https://third.example:8443/"),
521
+ new URL("http://fourth.example:8080/")
522
+ ];
523
+ const origins = [
524
+ "https://first.example",
525
+ "http://second.example",
526
+ "https://third.example:8443",
527
+ "http://fourth.example:8080"
528
+ ];
529
+ for (const ActorClass of actorClasses) {
530
+ const actor = new ActorClass({
531
+ id: parseIri("ap://did:key:z6Mkabc/actor"),
532
+ gateways
533
+ });
534
+ const compact = await actor.toJsonLd();
535
+ deepStrictEqual(compact.gateways, origins);
536
+ const expanded = await actor.toJsonLd({ format: "expand" });
537
+ deepStrictEqual(expanded[0]["https://w3id.org/fep/ef61/gateways"], [{ "@list": origins.map((origin) => ({ "@id": origin })) }]);
538
+ deepStrictEqual(actor.gateways, gateways);
539
+ deepStrictEqual(actor.gateways.map((gateway) => gateway.href), origins.map((origin) => `${origin}/`));
540
+ deepStrictEqual((await ActorClass.fromJsonLd(compact)).gateways, gateways);
541
+ deepStrictEqual((await ActorClass.fromJsonLd(expanded)).gateways, gateways);
542
+ }
543
+ });
544
+ test("FEP-ef61: portable actors parse unmapped gateway lists", async () => {
545
+ const actorClasses = [
546
+ Application,
547
+ Group,
548
+ Organization,
549
+ Person,
550
+ Service
551
+ ];
552
+ const gateways = ["https://first.example", "https://second.example"];
553
+ const ids = ["ap://did:key:z6Mkabc/actor", "https://first.example/.well-known/apgateway/did:key:z6Mkabc/actor"];
554
+ for (const ActorClass of actorClasses) for (const id of ids) deepStrictEqual((await ActorClass.fromJsonLd({
555
+ "@context": "https://www.w3.org/ns/activitystreams",
556
+ type: ActorClass.name,
557
+ id,
558
+ inbox: `${id}/inbox`,
559
+ outbox: `${id}/outbox`,
560
+ gateways
561
+ })).gateways, gateways.map((g) => new URL(g)));
562
+ });
563
+ test("FEP-ef61: unmapped gateways retain their context meaning", async () => {
564
+ const actorClasses = [
565
+ Application,
566
+ Group,
567
+ Organization,
568
+ Person,
569
+ Service
570
+ ];
571
+ const id = "ap://did:key:z6Mkabc/actor";
572
+ for (const ActorClass of actorClasses) {
573
+ const document = {
574
+ type: ActorClass.name,
575
+ id,
576
+ inbox: `${id}/inbox`,
577
+ outbox: `${id}/outbox`,
578
+ gateways: ["https://unmapped.example"]
579
+ };
580
+ for (const context of [
581
+ { gateways: "https://example.com/unrelated" },
582
+ { gateways: null },
583
+ { "@vocab": "https://example.com/" },
584
+ { [ActorClass.name]: {
585
+ "@id": `https://www.w3.org/ns/activitystreams#${ActorClass.name}`,
586
+ "@context": { gateways: "https://example.com/scoped" }
587
+ } }
588
+ ]) deepStrictEqual((await ActorClass.fromJsonLd({
589
+ "@context": ["https://www.w3.org/ns/activitystreams", context],
590
+ ...document
591
+ })).gateways, []);
592
+ deepStrictEqual((await ActorClass.fromJsonLd({
593
+ "@context": "https://www.w3.org/ns/activitystreams",
594
+ ...document,
595
+ id: "https://ordinary.example/actor"
596
+ })).gateways, []);
597
+ deepStrictEqual((await ActorClass.fromJsonLd({
598
+ "@context": "https://www.w3.org/ns/activitystreams",
599
+ ...document,
600
+ "https://w3id.org/fep/ef61/gateways": { "@list": [{ "@id": "https://canonical.example" }] }
601
+ })).gateways, [new URL("https://canonical.example")]);
602
+ }
603
+ });
406
604
  test("FEP-ef61: actor gateways preserve single, empty, and invalid cases", async () => {
407
605
  deepStrictEqual((await new Person({
408
606
  id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
409
607
  gateways: [new URL("https://server.example/")]
410
- }).toJsonLd()).gateways, ["https://server.example/"]);
608
+ }).toJsonLd()).gateways, ["https://server.example"]);
411
609
  ok(!("gateways" in await new Person({
412
610
  id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
413
611
  gateways: []
@@ -442,7 +640,9 @@ test("FEP-ef61: actor gateways must be HTTP(S) base URIs", async () => {
442
640
  "https://user@server.example/",
443
641
  "https://server.example/path",
444
642
  "https://server.example/?x=1",
445
- "https://server.example/#fragment"
643
+ "https://server.example/?",
644
+ "https://server.example/#fragment",
645
+ "https://server.example/#"
446
646
  ]) {
447
647
  throws(() => new Person({
448
648
  id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
@@ -1173,6 +1373,59 @@ test({
1173
1373
  });
1174
1374
  }
1175
1375
  });
1376
+ test({
1377
+ name: "Announce.getObject() logs suppressed failures as warnings",
1378
+ permissions: {
1379
+ env: true,
1380
+ read: true
1381
+ },
1382
+ async fn() {
1383
+ const records = [];
1384
+ const notFoundUrl = "https://example.com/suppressed-not-found";
1385
+ const invalidObjectUrl = "https://example.com/invalid-object";
1386
+ await reset();
1387
+ esm_default.spyGlobal();
1388
+ try {
1389
+ await configure({
1390
+ sinks: { buffer(record) {
1391
+ records.push(record);
1392
+ } },
1393
+ filters: {},
1394
+ loggers: [{
1395
+ category: [],
1396
+ sinks: ["buffer"]
1397
+ }]
1398
+ });
1399
+ esm_default.get(notFoundUrl, { status: 404 });
1400
+ deepStrictEqual(await new Announce({ object: new URL(notFoundUrl) }).getObject({ suppressError: true }), null);
1401
+ ok(records.some((record) => record.level === "warning" && record.rawMessage === "Failed to fetch document: {status} {url} {headers}"));
1402
+ ok(records.some((record) => record.level === "warning" && record.rawMessage === "Failed to fetch {url}: {error}"));
1403
+ deepStrictEqual(records.some((record) => record.level === "error" && (record.rawMessage === "Failed to fetch document: {status} {url} {headers}" || record.rawMessage === "Failed to fetch {url}: {error}")), false);
1404
+ records.length = 0;
1405
+ const unsuppressedFetch = new Announce({ object: new URL(notFoundUrl) });
1406
+ await rejects(() => unsuppressedFetch.getObject(), FetchError);
1407
+ ok(records.some((record) => record.level === "error" && record.rawMessage === "Failed to fetch document: {status} {url} {headers}"));
1408
+ records.length = 0;
1409
+ const invalidDocumentLoader = async (url) => ({
1410
+ contextUrl: null,
1411
+ documentUrl: url,
1412
+ document: null
1413
+ });
1414
+ deepStrictEqual(await new Announce({ object: new URL(invalidObjectUrl) }).getObject({
1415
+ documentLoader: invalidDocumentLoader,
1416
+ suppressError: true
1417
+ }), null);
1418
+ ok(records.some((record) => record.level === "warning" && record.rawMessage === "Failed to parse {url}: {error}"));
1419
+ deepStrictEqual(records.some((record) => record.level === "error" && record.rawMessage === "Failed to parse {url}: {error}"), false);
1420
+ records.length = 0;
1421
+ const unsuppressedParsing = new Announce({ object: new URL(invalidObjectUrl) });
1422
+ await rejects(() => unsuppressedParsing.getObject({ documentLoader: invalidDocumentLoader }), TypeError);
1423
+ } finally {
1424
+ esm_default.hardReset();
1425
+ await reset();
1426
+ }
1427
+ }
1428
+ });
1176
1429
  test("Activity.getObject() fetches canonical portable IRIs", async () => {
1177
1430
  const fetchedUrls = [];
1178
1431
  const documentLoader = async (url) => {
@@ -1197,7 +1450,8 @@ test("Activity.getObject() fetches canonical portable IRIs", async () => {
1197
1450
  contextLoader: mockDocumentLoader
1198
1451
  })).getObject({
1199
1452
  documentLoader,
1200
- contextLoader: mockDocumentLoader
1453
+ contextLoader: mockDocumentLoader,
1454
+ verifyPortableObject: async () => ({ verified: true })
1201
1455
  }), Note);
1202
1456
  deepStrictEqual(fetchedUrls, ["ap+ef61://did:key:z6Mkabc/objects/1"]);
1203
1457
  });
@@ -1226,6 +1480,72 @@ test({
1226
1480
  deepStrictEqual(objects2[0].name, "Second object");
1227
1481
  }
1228
1482
  });
1483
+ function embeddedPropertyDocument(property) {
1484
+ const context = "https://www.w3.org/ns/activitystreams";
1485
+ return {
1486
+ "@context": [context, { CustomImage: `${context}#Image` }],
1487
+ type: property === "attachment" ? "Note" : "Create",
1488
+ [property]: [
1489
+ {
1490
+ "@context": [context, { "@vocab": "https://example.com/" }],
1491
+ type: "CustomImage",
1492
+ name: "Malformed when reparsed"
1493
+ },
1494
+ {
1495
+ "@context": context,
1496
+ type: "Image",
1497
+ name: "Cached image"
1498
+ },
1499
+ {
1500
+ type: "Image",
1501
+ name: "Decoded image"
1502
+ }
1503
+ ]
1504
+ };
1505
+ }
1506
+ for (const suppressError of [
1507
+ void 0,
1508
+ false,
1509
+ true
1510
+ ]) {
1511
+ test(`Note.getAttachments() cached parse failure (suppressError: ${suppressError})`, async () => {
1512
+ const jsonLd = embeddedPropertyDocument("attachment");
1513
+ const note = await Note.fromJsonLd(jsonLd);
1514
+ if (suppressError) {
1515
+ for (let i = 0; i < 2; i++) deepStrictEqual((await Array.fromAsync(note.getAttachments({ suppressError }))).map((attachment) => attachment.name), ["Cached image", "Decoded image"]);
1516
+ await rejects(() => Array.fromAsync(note.getAttachments()), TypeError);
1517
+ } else await rejects(() => Array.fromAsync(note.getAttachments({ suppressError })), TypeError);
1518
+ deepStrictEqual(await note.toJsonLd(), jsonLd);
1519
+ });
1520
+ test(`Activity.getObject() cached parse failure (suppressError: ${suppressError})`, async () => {
1521
+ const jsonLd = embeddedPropertyDocument("object");
1522
+ const activity = await Activity.fromJsonLd(jsonLd);
1523
+ if (suppressError) {
1524
+ deepStrictEqual(await activity.getObject({ suppressError }), null);
1525
+ deepStrictEqual(await activity.getObject({ suppressError }), null);
1526
+ await rejects(() => activity.getObject(), TypeError);
1527
+ } else await rejects(() => activity.getObject({ suppressError }), TypeError);
1528
+ deepStrictEqual(await activity.toJsonLd(), jsonLd);
1529
+ });
1530
+ }
1531
+ test("Note.getAttachments() suppresses cached context loader failures", async () => {
1532
+ const note = await Note.fromJsonLd(embeddedPropertyDocument("attachment"));
1533
+ const contextLoader = () => Promise.reject(/* @__PURE__ */ new Error("Context loader failed"));
1534
+ await rejects(() => Array.fromAsync(note.getAttachments({ contextLoader })), (error) => error instanceof Error && !(error instanceof TypeError));
1535
+ deepStrictEqual((await Array.fromAsync(note.getAttachments({
1536
+ contextLoader,
1537
+ suppressError: true
1538
+ }))).map((attachment) => attachment.name), ["Decoded image"]);
1539
+ });
1540
+ test("Activity.getObject() suppresses cached context loader failures", async () => {
1541
+ const activity = await Activity.fromJsonLd(embeddedPropertyDocument("object"));
1542
+ const contextLoader = () => Promise.reject(/* @__PURE__ */ new Error("Context loader failed"));
1543
+ await rejects(() => activity.getObject({ contextLoader }), (error) => error instanceof Error && !(error instanceof TypeError));
1544
+ deepStrictEqual(await activity.getObject({
1545
+ contextLoader,
1546
+ suppressError: true
1547
+ }), null);
1548
+ });
1229
1549
  test("Activity.clone()", async () => {
1230
1550
  const activity = new Activity({
1231
1551
  actor: new Person({ name: "John Doe" }),
@@ -1273,6 +1593,21 @@ test({
1273
1593
  }), "Deno" in globalThis ? "Object {\n attribution: URL \"https://example.com/foo\",\n contents: [\n <en> \"Hello\",\n <zh> \"你好\"\n ],\n id: URL \"https://example.com/\",\n name: \"Test\"\n}" : "Object {\n attribution: URL 'https://example.com/foo',\n contents: [\n <en> 'Hello',\n <zh> '你好'\n ],\n id: URL 'https://example.com/',\n name: 'Test'\n}");
1274
1594
  }
1275
1595
  });
1596
+ test("inspecting an object prints portable IRIs in their canonical form", () => {
1597
+ const did = "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2";
1598
+ const obj = new Object$1({
1599
+ id: parseIri(`ap://${did}/note`),
1600
+ attribution: parseIri(`ap://${did}/actor`)
1601
+ });
1602
+ const inspected = [inspect(obj, { colors: false })];
1603
+ if ("Deno" in globalThis) inspected.push(Deno.inspect(obj, { colors: false }));
1604
+ for (const text of inspected) {
1605
+ ok(text.includes(`ap+ef61://${did}/note`), text);
1606
+ ok(text.includes(`ap+ef61://${did}/actor`), text);
1607
+ ok(!text.includes("%3A"), text);
1608
+ }
1609
+ ok(inspect(new Object$1({ id: new URL("ap://not-a-did/note") })).includes("ap://not-a-did/note"));
1610
+ });
1276
1611
  test("Person.fromJsonLd()", async () => {
1277
1612
  const person = await Person.fromJsonLd({
1278
1613
  "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
@@ -2103,6 +2438,83 @@ test("Delete.toJsonLd() compacts embedded QuoteRequest", async () => {
2103
2438
  type: "Delete"
2104
2439
  });
2105
2440
  });
2441
+ test("Note.quoteUrl (IRI-typed alias terms)", async () => {
2442
+ const jsonLd = {
2443
+ "@context": ["https://www.w3.org/ns/activitystreams", {
2444
+ fedibird: "http://fedibird.com/ns#",
2445
+ misskey: "https://misskey-hub.net/ns#",
2446
+ _misskey_quote: {
2447
+ "@id": "misskey:_misskey_quote",
2448
+ "@type": "@id"
2449
+ },
2450
+ quoteUri: {
2451
+ "@id": "fedibird:quoteUri",
2452
+ "@type": "@id"
2453
+ }
2454
+ }],
2455
+ id: "https://example.com/notes/1",
2456
+ type: "Note",
2457
+ _misskey_quote: "https://example.com/notes/quoted",
2458
+ quoteUri: "https://example.com/notes/quoted2"
2459
+ };
2460
+ deepStrictEqual((await Note.fromJsonLd(jsonLd)).quoteUrl, new URL("https://example.com/notes/quoted"));
2461
+ delete jsonLd._misskey_quote;
2462
+ deepStrictEqual((await Note.fromJsonLd(jsonLd)).quoteUrl, new URL("https://example.com/notes/quoted2"));
2463
+ });
2464
+ test("Note.quoteUrl (IRI-typed primary term)", async () => {
2465
+ const context = ["https://www.w3.org/ns/activitystreams", {
2466
+ misskey: "https://misskey-hub.net/ns#",
2467
+ quoteUrl: {
2468
+ "@id": "as:quoteUrl",
2469
+ "@type": "@id"
2470
+ },
2471
+ _misskey_quote: "misskey:_misskey_quote"
2472
+ }];
2473
+ deepStrictEqual((await Note.fromJsonLd({
2474
+ "@context": context,
2475
+ type: "Note",
2476
+ quoteUrl: "https://example.com/object"
2477
+ })).quoteUrl, new URL("https://example.com/object"));
2478
+ deepStrictEqual((await Note.fromJsonLd({
2479
+ "@context": context,
2480
+ type: "Note",
2481
+ quoteUrl: "https://example.com/object",
2482
+ _misskey_quote: "https://example.com/object2"
2483
+ })).quoteUrl, new URL("https://example.com/object"));
2484
+ });
2485
+ test("Note.quoteUrl (unparsable IRI-valued terms)", async () => {
2486
+ const context = ["https://www.w3.org/ns/activitystreams", {
2487
+ misskey: "https://misskey-hub.net/ns#",
2488
+ _misskey_quote: {
2489
+ "@id": "misskey:_misskey_quote",
2490
+ "@type": "@id"
2491
+ }
2492
+ }];
2493
+ for (const quote of [
2494
+ {
2495
+ type: "Note",
2496
+ content: "An inlined quote without an id"
2497
+ },
2498
+ "_:b0",
2499
+ "",
2500
+ "notes/relative"
2501
+ ]) {
2502
+ const loaded = await Note.fromJsonLd({
2503
+ "@context": context,
2504
+ id: "https://example.com/notes/1",
2505
+ type: "Note",
2506
+ content: "Hello",
2507
+ _misskey_quote: quote
2508
+ });
2509
+ deepStrictEqual(loaded.quoteUrl, null, JSON.stringify(quote));
2510
+ deepStrictEqual(loaded.content, "Hello");
2511
+ }
2512
+ deepStrictEqual((await Note.fromJsonLd({
2513
+ "@context": context,
2514
+ type: "Note",
2515
+ _misskey_quote: "at://did:plc:abc/app.bsky.feed.post/xyz"
2516
+ })).quoteUrl, new URL("at://" + encodeURIComponent("did:plc:abc/app.bsky.feed.post/xyz")));
2517
+ });
2106
2518
  test("Key.publicKey", async () => {
2107
2519
  const jwk = {
2108
2520
  kty: "RSA",
@@ -2200,6 +2612,22 @@ test("Link.fromJsonLd()", async () => {
2200
2612
  "href": "at://did:plc:ia76kvnndjutgedggx2ibrem"
2201
2613
  })).href, new URL("at://did%3Aplc%3Aia76kvnndjutgedggx2ibrem"));
2202
2614
  });
2615
+ test("Link.fromJsonLd() normalizes extlang hreflang", async () => {
2616
+ const link = await Link.fromJsonLd({
2617
+ "@context": "https://www.w3.org/ns/activitystreams",
2618
+ type: "Link",
2619
+ href: "https://example.com/yue",
2620
+ hreflang: "zh-YUE"
2621
+ });
2622
+ deepStrictEqual(link.language?.baseName, "yue");
2623
+ deepStrictEqual((await link.toJsonLd({ format: "compact" })).hreflang, "yue");
2624
+ await rejects(() => Link.fromJsonLd({
2625
+ "@context": "https://www.w3.org/ns/activitystreams",
2626
+ type: "Link",
2627
+ href: "https://example.com/en",
2628
+ hreflang: "en-USA"
2629
+ }), RangeError);
2630
+ });
2203
2631
  test("Person.fromJsonLd() with relative URLs", async () => {
2204
2632
  deepStrictEqual((await (await Person.fromJsonLd({
2205
2633
  "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
@@ -2613,6 +3041,158 @@ test("FEP-fe34: Trust tracking in object cloning", () => {
2613
3041
  });
2614
3042
  deepStrictEqual(create.clone({ object: newNote }).objectId, new URL("https://example.com/new-note"));
2615
3043
  });
3044
+ for (const property of ["object", "attachment"]) for (const fetchOriginal of [false, true]) test(`clone() isolates ${property} dereferencing (${fetchOriginal ? "original" : "clone"} first)`, async () => {
3045
+ const embedded = {
3046
+ id: "https://b.example/notes/1",
3047
+ type: "Note",
3048
+ content: "embedded"
3049
+ };
3050
+ const json = {
3051
+ "@context": "https://www.w3.org/ns/activitystreams",
3052
+ id: "https://a.example/activities/1",
3053
+ type: "Create",
3054
+ [property]: embedded
3055
+ };
3056
+ const original = await Create.fromJsonLd(json, { contextLoader: mockDocumentLoader });
3057
+ const copy = original.clone();
3058
+ const fetching = fetchOriginal ? original : copy;
3059
+ const untouched = fetchOriginal ? copy : original;
3060
+ let fetches = 0;
3061
+ const documentLoader = async (url) => {
3062
+ fetches++;
3063
+ return {
3064
+ contextUrl: null,
3065
+ documentUrl: url,
3066
+ document: {
3067
+ "@context": "https://www.w3.org/ns/activitystreams",
3068
+ ...embedded,
3069
+ content: "fetched"
3070
+ }
3071
+ };
3072
+ };
3073
+ const options = {
3074
+ documentLoader,
3075
+ contextLoader: mockDocumentLoader
3076
+ };
3077
+ const fetched = property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0];
3078
+ assertInstanceOf(fetched, Note);
3079
+ deepStrictEqual(fetched.content, "fetched");
3080
+ const trusted = { crossOrigin: "trust" };
3081
+ const preserved = property === "object" ? await untouched.getObject(trusted) : (await Array.fromAsync(untouched.getAttachments(trusted)))[0];
3082
+ assertInstanceOf(preserved, Note);
3083
+ deepStrictEqual(preserved.content, "embedded");
3084
+ deepStrictEqual((await original.toJsonLd())[property], fetchOriginal ? {
3085
+ ...embedded,
3086
+ content: "fetched"
3087
+ } : embedded);
3088
+ const nested = (await new Accept({ object: untouched }).toJsonLd()).object;
3089
+ deepStrictEqual(nested[property], embedded);
3090
+ strictEqual(property === "object" ? await fetching.getObject(options) : (await Array.fromAsync(fetching.getAttachments(options)))[0], fetched);
3091
+ deepStrictEqual(fetches, 1);
3092
+ const independentlyFetched = property === "object" ? await untouched.getObject(options) : (await Array.fromAsync(untouched.getAttachments(options)))[0];
3093
+ assertInstanceOf(independentlyFetched, Note);
3094
+ deepStrictEqual(independentlyFetched.content, "fetched");
3095
+ deepStrictEqual(fetches, 2);
3096
+ const warmedClone = fetching.clone();
3097
+ strictEqual(property === "object" ? await warmedClone.getObject(options) : (await Array.fromAsync(warmedClone.getAttachments(options)))[0], fetched);
3098
+ deepStrictEqual(fetches, 2);
3099
+ });
3100
+ for (const clone of [false, true]) for (const frozen of [false, true]) test(`${clone ? "clone()" : "constructor"} copies ${frozen ? "frozen" : "mutable"} plural arrays`, async () => {
3101
+ const items = [new URL("https://example.com/object"), new URL("https://example.com/object")];
3102
+ const before = items.slice();
3103
+ if (frozen) globalThis.Object.freeze(items);
3104
+ const base = new Collection({});
3105
+ const collection = clone ? base.clone({ items }) : new Collection({ items });
3106
+ const sibling = clone ? base.clone({ items }) : new Collection({ items });
3107
+ const fetched = await Array.fromAsync(collection.getItems({
3108
+ documentLoader: mockDocumentLoader,
3109
+ contextLoader: mockDocumentLoader
3110
+ }));
3111
+ deepStrictEqual(fetched.length, 2);
3112
+ deepStrictEqual(fetched.map((item) => item.name), ["Fetched object", "Fetched object"]);
3113
+ deepStrictEqual(items, before);
3114
+ strictEqual(items[0], before[0]);
3115
+ strictEqual(items[1], before[1]);
3116
+ deepStrictEqual((await sibling.toJsonLd()).items, before.map(String));
3117
+ if (!frozen) {
3118
+ items.push(new URL("https://example.com/extra"));
3119
+ deepStrictEqual(collection.itemIds, before);
3120
+ deepStrictEqual(sibling.itemIds, before);
3121
+ }
3122
+ });
3123
+ test("clone() copies scalar arrays without changing sparse inputs", () => {
3124
+ const names = ["original"];
3125
+ const original = new Object$1({ names });
3126
+ names.push("caller mutation");
3127
+ deepStrictEqual(original.names, ["original"]);
3128
+ const copy = original.clone();
3129
+ copy.names.push("clone mutation");
3130
+ deepStrictEqual(original.names, ["original"]);
3131
+ deepStrictEqual(copy.names, ["original", "clone mutation"]);
3132
+ const replacements = ["replacement"];
3133
+ const replaced = original.clone({ names: replacements });
3134
+ replacements.push("caller mutation");
3135
+ deepStrictEqual(replaced.names, ["replacement"]);
3136
+ const sparseNames = new Array(2);
3137
+ sparseNames[1] = "kept";
3138
+ const sparse = new Object$1({ names: sparseNames });
3139
+ for (const instance of [
3140
+ sparse,
3141
+ sparse.clone(),
3142
+ sparse.clone({ names: sparse.names })
3143
+ ]) {
3144
+ deepStrictEqual(instance.names.length, 2);
3145
+ deepStrictEqual(0 in instance.names, false);
3146
+ deepStrictEqual(instance.names[1], "kept");
3147
+ }
3148
+ });
3149
+ test("clone() preserves embedded object identity and trust", async () => {
3150
+ const note = new Note({ id: new URL("https://b.example/note") });
3151
+ const original = new Create({
3152
+ id: new URL("https://a.example/create"),
3153
+ object: note
3154
+ });
3155
+ const documentLoader = async () => {
3156
+ throw new Error("Trusted embedded objects must not be fetched.");
3157
+ };
3158
+ for (const instance of [
3159
+ original,
3160
+ original.clone(),
3161
+ original.clone({ objects: [note] })
3162
+ ]) {
3163
+ strictEqual(await instance.getObject({ documentLoader }), note);
3164
+ strictEqual(instance.objectId, note.id);
3165
+ }
3166
+ });
3167
+ test("clone() isolates crossOrigin trust when fetching", async () => {
3168
+ const original = new Create({
3169
+ id: new URL("https://a.example/create"),
3170
+ object: new URL("https://b.example/note")
3171
+ });
3172
+ const copy = original.clone();
3173
+ const documentLoader = async (url) => ({
3174
+ contextUrl: null,
3175
+ documentUrl: url,
3176
+ document: {
3177
+ "@context": "https://www.w3.org/ns/activitystreams",
3178
+ id: "https://other.example/note",
3179
+ type: "Note",
3180
+ content: "cross-origin"
3181
+ }
3182
+ });
3183
+ const options = {
3184
+ documentLoader,
3185
+ contextLoader: mockDocumentLoader
3186
+ };
3187
+ const trusted = await copy.getObject({
3188
+ ...options,
3189
+ crossOrigin: "trust"
3190
+ });
3191
+ assertInstanceOf(trusted, Note);
3192
+ deepStrictEqual(trusted.content, "cross-origin");
3193
+ deepStrictEqual(await original.getObject(options), null);
3194
+ deepStrictEqual(original.objectId, new URL("https://b.example/note"));
3195
+ });
2616
3196
  test("FEP-fe34: crossOrigin ignore behavior (default)", async () => {
2617
3197
  const crossOriginDocumentLoader = async (url) => {
2618
3198
  if (url === "https://different-origin.com/note") return {
@@ -2840,7 +3420,11 @@ test("FEP-fe34: Portable DID authorities are cross-origin boundaries", async ()
2840
3420
  };
2841
3421
  throw new Error("Document not found");
2842
3422
  };
2843
- const result = await create.getObject({ documentLoader });
3423
+ const result = await create.getObject({
3424
+ documentLoader,
3425
+ contextLoader: mockDocumentLoader,
3426
+ verifyPortableObject: async () => ({ verified: true })
3427
+ });
2844
3428
  assertInstanceOf(result, Note);
2845
3429
  deepStrictEqual(result.content, "Fetched portable note");
2846
3430
  });
@@ -3536,7 +4120,9 @@ for (const typeUri in types) {
3536
4120
  ...initValues
3537
4121
  });
3538
4122
  const jsonLd = await instance.toJsonLd({ contextLoader: mockDocumentLoader });
3539
- deepStrictEqual(jsonLd["@context"], type.defaultContext);
4123
+ const defaultContexts = Array.isArray(type.defaultContext) ? type.defaultContext : [type.defaultContext];
4124
+ const extraContexts = [...new Set(allProperties.flatMap((property) => property.extraContext == null || defaultContexts.some((context) => context === property.extraContext) ? [] : [property.extraContext]))];
4125
+ deepStrictEqual(jsonLd["@context"], extraContexts.length === 0 ? type.defaultContext : [...defaultContexts, ...extraContexts]);
3540
4126
  if (type.entity) deepStrictEqual(jsonLd.id, "https://example.com/");
3541
4127
  const restored = await cls.fromJsonLd(jsonLd, {
3542
4128
  documentLoader: mockDocumentLoader,
@@ -3599,5 +4185,309 @@ for (const typeUri in types) {
3599
4185
  deepStrictEqual(cls.typeId, new URL(type.uri));
3600
4186
  });
3601
4187
  }
4188
+ test("Announce.getObject() suppresses redirect, context, and private URL logs", async () => {
4189
+ const records = [];
4190
+ const objectUrl = "https://example.com/suppression-object";
4191
+ const contextUrl = "https://example.com/suppression-context";
4192
+ const scenarios = [
4193
+ "redirect limit",
4194
+ "redirect loop",
4195
+ "context",
4196
+ "private",
4197
+ "private redirect"
4198
+ ];
4199
+ await reset();
4200
+ await configure({
4201
+ sinks: { capture: (record) => {
4202
+ records.push(record);
4203
+ } },
4204
+ loggers: [{
4205
+ category: ["fedify"],
4206
+ lowestLevel: "debug",
4207
+ sinks: ["capture"]
4208
+ }]
4209
+ });
4210
+ try {
4211
+ for (const scenario of scenarios) {
4212
+ esm_default.spyGlobal();
4213
+ let fetches = 0;
4214
+ esm_default.get("begin:https://example.com/", (call) => {
4215
+ fetches++;
4216
+ if (scenario === "redirect limit") return Response.redirect(`https://example.com/redirect-${fetches}`, 302);
4217
+ if (scenario === "redirect loop") return Response.redirect(objectUrl, 302);
4218
+ if (scenario === "private redirect") return Response.redirect("http://127.0.0.1/private", 302);
4219
+ if (call.url === contextUrl) return new Response(null, { status: 404 });
4220
+ return new Response(JSON.stringify({
4221
+ "@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
4222
+ id: objectUrl,
4223
+ type: "Note"
4224
+ }), { headers: { "Content-Type": "application/activity+json" } });
4225
+ });
4226
+ try {
4227
+ for (const suppressError of [
4228
+ true,
4229
+ false,
4230
+ void 0
4231
+ ]) {
4232
+ records.length = 0;
4233
+ fetches = 0;
4234
+ const announce = new Announce({ object: new URL(scenario === "private" ? "http://127.0.0.1/private" : objectUrl) });
4235
+ if (suppressError) {
4236
+ deepStrictEqual(await announce.getObject({ suppressError }), null);
4237
+ deepStrictEqual(records.filter((r) => r.level === "error"), [], scenario);
4238
+ ok(records.some((r) => r.level === "warning"), scenario);
4239
+ } else {
4240
+ await rejects(() => announce.getObject({ suppressError }));
4241
+ ok(records.some((r) => r.level === "error"), scenario);
4242
+ }
4243
+ if (scenario === "private") deepStrictEqual(fetches, 0);
4244
+ if (scenario === "private redirect") deepStrictEqual(fetches, 1);
4245
+ }
4246
+ } finally {
4247
+ esm_default.hardReset();
4248
+ }
4249
+ }
4250
+ } finally {
4251
+ await reset();
4252
+ }
4253
+ });
4254
+ test("suppressed context loading does not persist on fetched objects", async () => {
4255
+ const states = [];
4256
+ const contextLoader = (url, options) => {
4257
+ if (url === "https://www.w3.org/ns/activitystreams") return mockDocumentLoader(url);
4258
+ states.push(options?.suppressError);
4259
+ return Promise.resolve({
4260
+ contextUrl: null,
4261
+ documentUrl: url,
4262
+ document: { "@context": {
4263
+ as: "https://www.w3.org/ns/activitystreams#",
4264
+ name: "as:name"
4265
+ } }
4266
+ });
4267
+ };
4268
+ const documentLoader = (url) => Promise.resolve({
4269
+ contextUrl: null,
4270
+ documentUrl: url,
4271
+ document: {
4272
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://example.com/scoped-context"],
4273
+ id: url,
4274
+ type: url.endsWith("actor") ? "Person" : "Note",
4275
+ attributedTo: "https://example.com/scoped-actor"
4276
+ }
4277
+ });
4278
+ const note = await new Announce({ object: new URL("https://example.com/scoped-note") }).getObject({
4279
+ documentLoader,
4280
+ contextLoader,
4281
+ suppressError: true
4282
+ });
4283
+ assertInstanceOf(note, Note);
4284
+ ok(states.includes(true));
4285
+ states.length = 0;
4286
+ assertInstanceOf(await note.getAttribution(), Person);
4287
+ ok(states.length > 0);
4288
+ deepStrictEqual(states.includes(true), false);
4289
+ });
4290
+ test("attribution chains alternate ordinary and portable suppression", async () => {
4291
+ const states = [];
4292
+ const depths = [];
4293
+ let hop = 0;
4294
+ const contextUrl = "https://example.com/attribution-context";
4295
+ const contextLoader = (url, options) => {
4296
+ if (url !== contextUrl) return mockDocumentLoader(url, options);
4297
+ states.push(options?.suppressError);
4298
+ const stack = (/* @__PURE__ */ new Error()).stack;
4299
+ ok(stack);
4300
+ depths[hop]?.push(stack.split("\n").length);
4301
+ return Promise.resolve({
4302
+ contextUrl: null,
4303
+ documentUrl: url,
4304
+ document: { "@context": { name: "https://www.w3.org/ns/activitystreams#name" } }
4305
+ });
4306
+ };
4307
+ const documentLoader = (url) => {
4308
+ const hop = Number(new URL(url).pathname.split("/").at(-1));
4309
+ return Promise.resolve({
4310
+ contextUrl: null,
4311
+ documentUrl: url,
4312
+ document: {
4313
+ "@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
4314
+ id: url,
4315
+ type: "Person",
4316
+ name: `Actor ${hop}`,
4317
+ attributedTo: `https://example.com/actors/${hop + 1}`
4318
+ }
4319
+ });
4320
+ };
4321
+ let person = new Person({
4322
+ id: new URL("https://example.com/actors/root"),
4323
+ attribution: new URL("https://example.com/actors/0")
4324
+ }, {
4325
+ documentLoader,
4326
+ contextLoader
4327
+ });
4328
+ const stackTraceLimit = globalThis.Object.getOwnPropertyDescriptor(Error, "stackTraceLimit");
4329
+ try {
4330
+ ok(Reflect.set(Error, "stackTraceLimit", Infinity));
4331
+ for (; hop < 100; hop++) {
4332
+ depths[hop] = [];
4333
+ const next = await person.getAttribution({
4334
+ suppressError: true,
4335
+ ...hop % 2 === 0 ? {} : { verifyPortableObject: () => Promise.resolve({ verified: true }) }
4336
+ });
4337
+ assertInstanceOf(next, Person);
4338
+ deepStrictEqual(next.id, new URL(`https://example.com/actors/${hop}`));
4339
+ person = next;
4340
+ }
4341
+ ok(depths.every((values) => values.length > 0));
4342
+ deepStrictEqual(depths[98], depths[2]);
4343
+ deepStrictEqual(depths[99], depths[3]);
4344
+ ok(states.length >= 100);
4345
+ ok(states.every((state) => state === true));
4346
+ states.length = 0;
4347
+ assertInstanceOf(await person.getAttribution(), Person);
4348
+ ok(states.length > 0);
4349
+ ok(states.every((state) => state !== true));
4350
+ } finally {
4351
+ if (stackTraceLimit == null) Reflect.deleteProperty(Error, "stackTraceLimit");
4352
+ else globalThis.Object.defineProperty(Error, "stackTraceLimit", stackTraceLimit);
4353
+ }
4354
+ });
4355
+ test("portable accessors suppress document and context failure logs", async () => {
4356
+ const records = [];
4357
+ const id = new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
4358
+ const contextUrl = "https://example.com/portable-suppression-context";
4359
+ await reset();
4360
+ await configure({
4361
+ sinks: { capture: (record) => {
4362
+ records.push(record);
4363
+ } },
4364
+ loggers: [{
4365
+ category: ["fedify"],
4366
+ lowestLevel: "debug",
4367
+ sinks: ["capture"]
4368
+ }]
4369
+ });
4370
+ try {
4371
+ for (const failContext of [false, true]) {
4372
+ esm_default.spyGlobal();
4373
+ esm_default.get("begin:https://example.com/", (call) => {
4374
+ if (!failContext || call.url === contextUrl) return new Response(null, { status: 404 });
4375
+ return new Response(JSON.stringify({
4376
+ "@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
4377
+ id: "ap://did:key:z6Mkabc/objects/1",
4378
+ type: "Note"
4379
+ }), { headers: { "Content-Type": "application/activity+json" } });
4380
+ });
4381
+ try {
4382
+ for (const suppressError of [
4383
+ true,
4384
+ false,
4385
+ void 0
4386
+ ]) {
4387
+ records.length = 0;
4388
+ const announce = new Announce({ object: id });
4389
+ const options = {
4390
+ suppressError,
4391
+ gateways: ["https://example.com/"],
4392
+ verifyPortableObject: () => Promise.resolve({ verified: true })
4393
+ };
4394
+ if (suppressError) {
4395
+ deepStrictEqual(await announce.getObject(options), null);
4396
+ deepStrictEqual(records.filter((r) => r.level === "error"), []);
4397
+ ok(records.some((r) => r.level === "warning"));
4398
+ } else {
4399
+ await rejects(() => announce.getObject(options));
4400
+ ok(records.some((r) => r.level === "error"));
4401
+ }
4402
+ }
4403
+ } finally {
4404
+ esm_default.hardReset();
4405
+ }
4406
+ }
4407
+ } finally {
4408
+ await reset();
4409
+ }
4410
+ });
4411
+ test("portable verifiers suppress document logs only during dereferencing", async () => {
4412
+ const records = [];
4413
+ const id = new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
4414
+ const verificationUrl = "https://example.com/verification-document";
4415
+ const missingUrl = "https://example.com/missing-verification-document";
4416
+ const signal = new AbortController().signal;
4417
+ await reset();
4418
+ await configure({
4419
+ sinks: { capture: (record) => records.push(record) },
4420
+ loggers: [{
4421
+ category: ["fedify"],
4422
+ lowestLevel: "debug",
4423
+ sinks: ["capture"]
4424
+ }]
4425
+ });
4426
+ try {
4427
+ for (const failVerification of [false, true]) {
4428
+ esm_default.spyGlobal();
4429
+ esm_default.get("begin:https://example.com/.well-known/", {
4430
+ headers: { "Content-Type": "application/activity+json" },
4431
+ body: {
4432
+ "@context": "https://www.w3.org/ns/activitystreams",
4433
+ id: "ap://did:key:z6Mkabc/objects/1",
4434
+ type: "Note"
4435
+ }
4436
+ });
4437
+ esm_default.get(verificationUrl, {
4438
+ status: failVerification ? 404 : 200,
4439
+ headers: { "Content-Type": "application/ld+json" },
4440
+ body: {}
4441
+ });
4442
+ esm_default.get(missingUrl, { status: 404 });
4443
+ try {
4444
+ for (const suppressError of [
4445
+ true,
4446
+ false,
4447
+ void 0
4448
+ ]) {
4449
+ records.length = 0;
4450
+ const baseLoader = getDocumentLoader();
4451
+ const calls = [];
4452
+ const documentLoader = (url, options) => {
4453
+ calls.push(options);
4454
+ return baseLoader(url, options);
4455
+ };
4456
+ let retainedLoader;
4457
+ const loaderOptions = { signal };
4458
+ const object = await new Announce({ object: id }).getObject({
4459
+ suppressError,
4460
+ documentLoader,
4461
+ gateways: ["https://example.com/"],
4462
+ verifyPortableObject: async (_document, options) => {
4463
+ retainedLoader = options.documentLoader;
4464
+ ok(options.documentLoader);
4465
+ await options.documentLoader(verificationUrl, loaderOptions);
4466
+ return { verified: true };
4467
+ }
4468
+ });
4469
+ if (failVerification) {
4470
+ deepStrictEqual(object, null);
4471
+ deepStrictEqual(records.some((record) => record.level === "error"), !suppressError);
4472
+ ok(records.some((record) => record.level === "warning"));
4473
+ } else assertInstanceOf(object, Note);
4474
+ deepStrictEqual(calls.at(-1)?.signal, signal);
4475
+ deepStrictEqual(calls.at(-1)?.suppressError, suppressError ? true : void 0);
4476
+ deepStrictEqual(loaderOptions, { signal });
4477
+ records.length = 0;
4478
+ const releasedLoader = retainedLoader;
4479
+ ok(releasedLoader);
4480
+ await rejects(() => releasedLoader(missingUrl, loaderOptions), FetchError);
4481
+ deepStrictEqual(calls.at(-1), loaderOptions);
4482
+ ok(records.some((record) => record.level === "error"));
4483
+ }
4484
+ } finally {
4485
+ esm_default.hardReset();
4486
+ }
4487
+ }
4488
+ } finally {
4489
+ await reset();
4490
+ }
4491
+ });
3602
4492
  //#endregion
3603
4493
  export {};