@fedify/vocab 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.
- package/deno.json +1 -1
- package/dist/mod.cjs +31713 -8698
- package/dist/mod.d.cts +4708 -7
- package/dist/mod.d.ts +4708 -7
- package/dist/mod.js +31606 -8598
- package/dist-tests/actor-D7_iX4I4.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/application.yaml +15 -0
- package/dist-tests/audio.yaml +2 -1
- package/dist-tests/document.yaml +14 -1
- package/dist-tests/{actor-CnnyTI7i.mjs → esm-DP2Spt0u.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/group.yaml +15 -0
- package/dist-tests/image.yaml +2 -1
- package/dist-tests/link.yaml +14 -1
- package/dist-tests/lookup-IiNFWuXL.mjs +480 -0
- package/dist-tests/lookup.test.mjs +10 -268
- package/dist-tests/object.yaml +15 -0
- package/dist-tests/organization.yaml +15 -0
- package/dist-tests/page.yaml +2 -1
- package/dist-tests/person.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -0
- package/dist-tests/service.yaml +15 -0
- package/dist-tests/signed-representation.test.mjs +189 -0
- package/dist-tests/translation.test.mjs +404 -0
- package/dist-tests/translation.yaml +91 -0
- package/dist-tests/type.test.mjs +2 -2
- package/dist-tests/video.yaml +2 -1
- package/dist-tests/{vocab-OU5GYbNg.mjs → vocab-COZcxf00.mjs} +31227 -8501
- package/dist-tests/vocab.test.mjs +824 -6
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +345 -30
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/application.yaml +15 -0
- package/src/audio.yaml +2 -1
- package/src/document.yaml +14 -1
- package/src/gateway.test.ts +2637 -0
- package/src/group.yaml +15 -0
- package/src/image.yaml +2 -1
- package/src/link.yaml +14 -1
- package/src/lookup.test.ts +6 -5
- package/src/lookup.ts +460 -33
- package/src/mod.ts +9 -0
- package/src/object.yaml +15 -0
- package/src/organization.yaml +15 -0
- package/src/page.yaml +2 -1
- package/src/person.yaml +15 -0
- package/src/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +4 -2
- package/src/service.yaml +15 -0
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/video.yaml +2 -1
- package/src/vocab.test.ts +1155 -2
- /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
- /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 { B as
|
|
4
|
-
import { t as
|
|
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-COZcxf00.mjs";
|
|
4
|
+
import { t as esm_default } from "./esm-DP2Spt0u.mjs";
|
|
5
|
+
import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
|
|
5
6
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
6
7
|
import { deepStrictEqual, notDeepStrictEqual, ok, rejects, 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,36 @@ 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
|
+
});
|
|
252
284
|
test("Note.toJsonLd()", async () => {
|
|
253
285
|
const note = new Note({ tags: [new Hashtag({
|
|
254
286
|
name: "#Fedify",
|
|
@@ -380,6 +412,267 @@ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
|
|
|
380
412
|
deepStrictEqual(pageJson.next, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=2");
|
|
381
413
|
deepStrictEqual(pageJson.prev, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=1");
|
|
382
414
|
});
|
|
415
|
+
test("fromJsonLd() refuses portable IDs changed by URL parsing", async () => {
|
|
416
|
+
const base = "ap://did:key:z6Mkabc";
|
|
417
|
+
for (const id of [
|
|
418
|
+
`${base}/a/../b`,
|
|
419
|
+
`${base}/a/%2e%2E/b`,
|
|
420
|
+
"https://gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b",
|
|
421
|
+
"https:/gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b"
|
|
422
|
+
]) await rejects(() => Note.fromJsonLd({
|
|
423
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
424
|
+
type: "Note",
|
|
425
|
+
id
|
|
426
|
+
}, {
|
|
427
|
+
documentLoader: mockDocumentLoader,
|
|
428
|
+
contextLoader: mockDocumentLoader
|
|
429
|
+
}), TypeError);
|
|
430
|
+
});
|
|
431
|
+
test("FEP-ef61: actor gateways round-trip as an ordered URI list", async () => {
|
|
432
|
+
const actorClasses = [
|
|
433
|
+
Application,
|
|
434
|
+
Group,
|
|
435
|
+
Organization,
|
|
436
|
+
Person,
|
|
437
|
+
Service
|
|
438
|
+
];
|
|
439
|
+
const gateways = [new URL("https://server1.example/"), new URL("https://server2.example/")];
|
|
440
|
+
for (const ActorClass of actorClasses) {
|
|
441
|
+
const actor = await ActorClass.fromJsonLd({
|
|
442
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/ef61"],
|
|
443
|
+
type: ActorClass.name,
|
|
444
|
+
id: "ap+ef61://did:key:z6Mkabc/actor",
|
|
445
|
+
gateways: gateways.map((gateway) => gateway.href)
|
|
446
|
+
});
|
|
447
|
+
deepStrictEqual(actor.gateways, gateways);
|
|
448
|
+
const jsonLd = await actor.toJsonLd({ format: "compact" });
|
|
449
|
+
deepStrictEqual(jsonLd.type, ActorClass.name);
|
|
450
|
+
deepStrictEqual(jsonLd.gateways, gateways.map((gateway) => gateway.origin));
|
|
451
|
+
deepStrictEqual((await ActorClass.fromJsonLd(jsonLd)).gateways, gateways);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
test("FEP-ef61: actor gateways serialize as origins", async () => {
|
|
455
|
+
const actorClasses = [
|
|
456
|
+
Application,
|
|
457
|
+
Group,
|
|
458
|
+
Organization,
|
|
459
|
+
Person,
|
|
460
|
+
Service
|
|
461
|
+
];
|
|
462
|
+
const gateways = [
|
|
463
|
+
new URL("https://first.example:443/"),
|
|
464
|
+
new URL("http://second.example:80/"),
|
|
465
|
+
new URL("https://third.example:8443/"),
|
|
466
|
+
new URL("http://fourth.example:8080/")
|
|
467
|
+
];
|
|
468
|
+
const origins = [
|
|
469
|
+
"https://first.example",
|
|
470
|
+
"http://second.example",
|
|
471
|
+
"https://third.example:8443",
|
|
472
|
+
"http://fourth.example:8080"
|
|
473
|
+
];
|
|
474
|
+
for (const ActorClass of actorClasses) {
|
|
475
|
+
const actor = new ActorClass({
|
|
476
|
+
id: parseIri("ap://did:key:z6Mkabc/actor"),
|
|
477
|
+
gateways
|
|
478
|
+
});
|
|
479
|
+
const compact = await actor.toJsonLd();
|
|
480
|
+
deepStrictEqual(compact.gateways, origins);
|
|
481
|
+
const expanded = await actor.toJsonLd({ format: "expand" });
|
|
482
|
+
deepStrictEqual(expanded[0]["https://w3id.org/fep/ef61/gateways"], [{ "@list": origins.map((origin) => ({ "@id": origin })) }]);
|
|
483
|
+
deepStrictEqual(actor.gateways, gateways);
|
|
484
|
+
deepStrictEqual(actor.gateways.map((gateway) => gateway.href), origins.map((origin) => `${origin}/`));
|
|
485
|
+
deepStrictEqual((await ActorClass.fromJsonLd(compact)).gateways, gateways);
|
|
486
|
+
deepStrictEqual((await ActorClass.fromJsonLd(expanded)).gateways, gateways);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
test("FEP-ef61: portable actors parse unmapped gateway lists", async () => {
|
|
490
|
+
const actorClasses = [
|
|
491
|
+
Application,
|
|
492
|
+
Group,
|
|
493
|
+
Organization,
|
|
494
|
+
Person,
|
|
495
|
+
Service
|
|
496
|
+
];
|
|
497
|
+
const gateways = ["https://first.example", "https://second.example"];
|
|
498
|
+
const ids = ["ap://did:key:z6Mkabc/actor", "https://first.example/.well-known/apgateway/did:key:z6Mkabc/actor"];
|
|
499
|
+
for (const ActorClass of actorClasses) for (const id of ids) deepStrictEqual((await ActorClass.fromJsonLd({
|
|
500
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
501
|
+
type: ActorClass.name,
|
|
502
|
+
id,
|
|
503
|
+
inbox: `${id}/inbox`,
|
|
504
|
+
outbox: `${id}/outbox`,
|
|
505
|
+
gateways
|
|
506
|
+
})).gateways, gateways.map((g) => new URL(g)));
|
|
507
|
+
});
|
|
508
|
+
test("FEP-ef61: unmapped gateways retain their context meaning", async () => {
|
|
509
|
+
const actorClasses = [
|
|
510
|
+
Application,
|
|
511
|
+
Group,
|
|
512
|
+
Organization,
|
|
513
|
+
Person,
|
|
514
|
+
Service
|
|
515
|
+
];
|
|
516
|
+
const id = "ap://did:key:z6Mkabc/actor";
|
|
517
|
+
for (const ActorClass of actorClasses) {
|
|
518
|
+
const document = {
|
|
519
|
+
type: ActorClass.name,
|
|
520
|
+
id,
|
|
521
|
+
inbox: `${id}/inbox`,
|
|
522
|
+
outbox: `${id}/outbox`,
|
|
523
|
+
gateways: ["https://unmapped.example"]
|
|
524
|
+
};
|
|
525
|
+
for (const context of [
|
|
526
|
+
{ gateways: "https://example.com/unrelated" },
|
|
527
|
+
{ gateways: null },
|
|
528
|
+
{ "@vocab": "https://example.com/" },
|
|
529
|
+
{ [ActorClass.name]: {
|
|
530
|
+
"@id": `https://www.w3.org/ns/activitystreams#${ActorClass.name}`,
|
|
531
|
+
"@context": { gateways: "https://example.com/scoped" }
|
|
532
|
+
} }
|
|
533
|
+
]) deepStrictEqual((await ActorClass.fromJsonLd({
|
|
534
|
+
"@context": ["https://www.w3.org/ns/activitystreams", context],
|
|
535
|
+
...document
|
|
536
|
+
})).gateways, []);
|
|
537
|
+
deepStrictEqual((await ActorClass.fromJsonLd({
|
|
538
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
539
|
+
...document,
|
|
540
|
+
id: "https://ordinary.example/actor"
|
|
541
|
+
})).gateways, []);
|
|
542
|
+
deepStrictEqual((await ActorClass.fromJsonLd({
|
|
543
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
544
|
+
...document,
|
|
545
|
+
"https://w3id.org/fep/ef61/gateways": { "@list": [{ "@id": "https://canonical.example" }] }
|
|
546
|
+
})).gateways, [new URL("https://canonical.example")]);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
test("FEP-ef61: actor gateways preserve single, empty, and invalid cases", async () => {
|
|
550
|
+
deepStrictEqual((await new Person({
|
|
551
|
+
id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
|
|
552
|
+
gateways: [new URL("https://server.example/")]
|
|
553
|
+
}).toJsonLd()).gateways, ["https://server.example"]);
|
|
554
|
+
ok(!("gateways" in await new Person({
|
|
555
|
+
id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
|
|
556
|
+
gateways: []
|
|
557
|
+
}).toJsonLd()));
|
|
558
|
+
await rejects(() => Person.fromJsonLd({
|
|
559
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/ef61"],
|
|
560
|
+
type: "Person",
|
|
561
|
+
gateways: ["not a uri"]
|
|
562
|
+
}), TypeError);
|
|
563
|
+
});
|
|
564
|
+
test("FEP-ef61: actor gateways accept @id typed JSON-LD references", async () => {
|
|
565
|
+
deepStrictEqual((await Person.fromJsonLd({
|
|
566
|
+
"@context": ["https://www.w3.org/ns/activitystreams", { gateways: {
|
|
567
|
+
"@id": "https://w3id.org/fep/ef61/gateways",
|
|
568
|
+
"@type": "@id",
|
|
569
|
+
"@container": "@list"
|
|
570
|
+
} }],
|
|
571
|
+
type: "Person",
|
|
572
|
+
id: "ap+ef61://did:key:z6Mkabc/actor",
|
|
573
|
+
gateways: ["https://gateway.example/"]
|
|
574
|
+
})).gateways, [new URL("https://gateway.example/")]);
|
|
575
|
+
});
|
|
576
|
+
test("FEP-ef61: actor gateways must be HTTP(S) base URIs", async () => {
|
|
577
|
+
const validGateways = [new URL("https://server.example/"), new URL("http://server.example/")];
|
|
578
|
+
deepStrictEqual(new Person({
|
|
579
|
+
id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
|
|
580
|
+
gateways: validGateways
|
|
581
|
+
}).gateways, validGateways);
|
|
582
|
+
for (const gateway of [
|
|
583
|
+
"ftp://server.example/",
|
|
584
|
+
"https://user:pass@server.example/",
|
|
585
|
+
"https://user@server.example/",
|
|
586
|
+
"https://server.example/path",
|
|
587
|
+
"https://server.example/?x=1",
|
|
588
|
+
"https://server.example/?",
|
|
589
|
+
"https://server.example/#fragment",
|
|
590
|
+
"https://server.example/#"
|
|
591
|
+
]) {
|
|
592
|
+
throws(() => new Person({
|
|
593
|
+
id: new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"),
|
|
594
|
+
gateways: [new URL(gateway)]
|
|
595
|
+
}), TypeError);
|
|
596
|
+
await rejects(() => Person.fromJsonLd({
|
|
597
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/ef61"],
|
|
598
|
+
type: "Person",
|
|
599
|
+
gateways: [gateway]
|
|
600
|
+
}), TypeError);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
test("FEP-ef61: digestMultibase round-trips on links and media objects", async () => {
|
|
604
|
+
const digestMultibase = "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n";
|
|
605
|
+
const link = await Link.fromJsonLd({
|
|
606
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/ef61"],
|
|
607
|
+
type: "Link",
|
|
608
|
+
href: "hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
|
|
609
|
+
digestMultibase
|
|
610
|
+
});
|
|
611
|
+
deepStrictEqual(link.digestMultibase, digestMultibase);
|
|
612
|
+
deepStrictEqual((await link.toJsonLd()).digestMultibase, digestMultibase);
|
|
613
|
+
for (const cls of [Document, Image]) {
|
|
614
|
+
const media = await cls.fromJsonLd({
|
|
615
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/ef61"],
|
|
616
|
+
type: cls.name,
|
|
617
|
+
url: "hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
|
|
618
|
+
mediaType: "image/png",
|
|
619
|
+
digestMultibase
|
|
620
|
+
});
|
|
621
|
+
deepStrictEqual(media.digestMultibase, digestMultibase);
|
|
622
|
+
const jsonLd = await media.toJsonLd();
|
|
623
|
+
deepStrictEqual(jsonLd.type, cls.name);
|
|
624
|
+
deepStrictEqual(jsonLd.digestMultibase, digestMultibase);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
test("FEP-ef61: digestMultibase avoids Data Integrity context conflicts", async () => {
|
|
628
|
+
const digestMultibase = "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n";
|
|
629
|
+
const image = new Image({
|
|
630
|
+
url: new URL("hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"),
|
|
631
|
+
mediaType: "image/png",
|
|
632
|
+
digestMultibase
|
|
633
|
+
});
|
|
634
|
+
const expanded = await image.toJsonLd({ format: "expand" });
|
|
635
|
+
deepStrictEqual(expanded[0]["https://www.w3.org/ns/credentials/v2#digestMultibase"], [{ "@value": digestMultibase }]);
|
|
636
|
+
ok(!("https://w3id.org/security#digestMultibase" in expanded[0]));
|
|
637
|
+
const compact = await image.toJsonLd();
|
|
638
|
+
deepStrictEqual(compact.digestMultibase, digestMultibase);
|
|
639
|
+
ok(!compact["@context"].includes("https://w3id.org/fep/ef61"));
|
|
640
|
+
ok(compact["@context"].some((context) => context != null && typeof context === "object" && context.digestMultibase === "https://www.w3.org/ns/credentials/v2#digestMultibase"));
|
|
641
|
+
});
|
|
642
|
+
test("FEP-ef61: digestMultibase parses after Data Integrity v1 contexts", async () => {
|
|
643
|
+
const digestMultibase = "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n";
|
|
644
|
+
deepStrictEqual((await Image.fromJsonLd({
|
|
645
|
+
"@context": [
|
|
646
|
+
"https://www.w3.org/ns/activitystreams",
|
|
647
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
648
|
+
"https://w3id.org/fep/ef61"
|
|
649
|
+
],
|
|
650
|
+
type: "Image",
|
|
651
|
+
url: "hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
|
|
652
|
+
mediaType: "image/png",
|
|
653
|
+
digestMultibase
|
|
654
|
+
})).digestMultibase, digestMultibase);
|
|
655
|
+
});
|
|
656
|
+
test("FEP-ef61: Link image normalization preserves digestMultibase", async () => {
|
|
657
|
+
const digestMultibase = "zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n";
|
|
658
|
+
const obj = await Object$1.fromJsonLd({
|
|
659
|
+
"@context": [
|
|
660
|
+
"https://www.w3.org/ns/activitystreams",
|
|
661
|
+
"https://w3id.org/security/data-integrity/v1",
|
|
662
|
+
"https://w3id.org/fep/ef61"
|
|
663
|
+
],
|
|
664
|
+
type: "Note",
|
|
665
|
+
image: {
|
|
666
|
+
type: "Link",
|
|
667
|
+
href: "hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
|
|
668
|
+
mediaType: "image/png",
|
|
669
|
+
digestMultibase
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
const images = [];
|
|
673
|
+
for await (const img of obj.getImages()) images.push(img);
|
|
674
|
+
deepStrictEqual(images[0]?.digestMultibase, digestMultibase);
|
|
675
|
+
});
|
|
383
676
|
test("fromJsonLd() caches text that mentions portable ActivityPub IRIs", async () => {
|
|
384
677
|
const noteJson = {
|
|
385
678
|
"@context": ["https://www.w3.org/ns/activitystreams", { extra: "https://example.com/ns#extra" }],
|
|
@@ -1025,6 +1318,59 @@ test({
|
|
|
1025
1318
|
});
|
|
1026
1319
|
}
|
|
1027
1320
|
});
|
|
1321
|
+
test({
|
|
1322
|
+
name: "Announce.getObject() logs suppressed failures as warnings",
|
|
1323
|
+
permissions: {
|
|
1324
|
+
env: true,
|
|
1325
|
+
read: true
|
|
1326
|
+
},
|
|
1327
|
+
async fn() {
|
|
1328
|
+
const records = [];
|
|
1329
|
+
const notFoundUrl = "https://example.com/suppressed-not-found";
|
|
1330
|
+
const invalidObjectUrl = "https://example.com/invalid-object";
|
|
1331
|
+
await reset();
|
|
1332
|
+
esm_default.spyGlobal();
|
|
1333
|
+
try {
|
|
1334
|
+
await configure({
|
|
1335
|
+
sinks: { buffer(record) {
|
|
1336
|
+
records.push(record);
|
|
1337
|
+
} },
|
|
1338
|
+
filters: {},
|
|
1339
|
+
loggers: [{
|
|
1340
|
+
category: [],
|
|
1341
|
+
sinks: ["buffer"]
|
|
1342
|
+
}]
|
|
1343
|
+
});
|
|
1344
|
+
esm_default.get(notFoundUrl, { status: 404 });
|
|
1345
|
+
deepStrictEqual(await new Announce({ object: new URL(notFoundUrl) }).getObject({ suppressError: true }), null);
|
|
1346
|
+
ok(records.some((record) => record.level === "warning" && record.rawMessage === "Failed to fetch document: {status} {url} {headers}"));
|
|
1347
|
+
ok(records.some((record) => record.level === "warning" && record.rawMessage === "Failed to fetch {url}: {error}"));
|
|
1348
|
+
deepStrictEqual(records.some((record) => record.level === "error" && (record.rawMessage === "Failed to fetch document: {status} {url} {headers}" || record.rawMessage === "Failed to fetch {url}: {error}")), false);
|
|
1349
|
+
records.length = 0;
|
|
1350
|
+
const unsuppressedFetch = new Announce({ object: new URL(notFoundUrl) });
|
|
1351
|
+
await rejects(() => unsuppressedFetch.getObject(), FetchError);
|
|
1352
|
+
ok(records.some((record) => record.level === "error" && record.rawMessage === "Failed to fetch document: {status} {url} {headers}"));
|
|
1353
|
+
records.length = 0;
|
|
1354
|
+
const invalidDocumentLoader = async (url) => ({
|
|
1355
|
+
contextUrl: null,
|
|
1356
|
+
documentUrl: url,
|
|
1357
|
+
document: null
|
|
1358
|
+
});
|
|
1359
|
+
deepStrictEqual(await new Announce({ object: new URL(invalidObjectUrl) }).getObject({
|
|
1360
|
+
documentLoader: invalidDocumentLoader,
|
|
1361
|
+
suppressError: true
|
|
1362
|
+
}), null);
|
|
1363
|
+
ok(records.some((record) => record.level === "warning" && record.rawMessage === "Failed to parse {url}: {error}"));
|
|
1364
|
+
deepStrictEqual(records.some((record) => record.level === "error" && record.rawMessage === "Failed to parse {url}: {error}"), false);
|
|
1365
|
+
records.length = 0;
|
|
1366
|
+
const unsuppressedParsing = new Announce({ object: new URL(invalidObjectUrl) });
|
|
1367
|
+
await rejects(() => unsuppressedParsing.getObject({ documentLoader: invalidDocumentLoader }), TypeError);
|
|
1368
|
+
} finally {
|
|
1369
|
+
esm_default.hardReset();
|
|
1370
|
+
await reset();
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1028
1374
|
test("Activity.getObject() fetches canonical portable IRIs", async () => {
|
|
1029
1375
|
const fetchedUrls = [];
|
|
1030
1376
|
const documentLoader = async (url) => {
|
|
@@ -1049,7 +1395,8 @@ test("Activity.getObject() fetches canonical portable IRIs", async () => {
|
|
|
1049
1395
|
contextLoader: mockDocumentLoader
|
|
1050
1396
|
})).getObject({
|
|
1051
1397
|
documentLoader,
|
|
1052
|
-
contextLoader: mockDocumentLoader
|
|
1398
|
+
contextLoader: mockDocumentLoader,
|
|
1399
|
+
verifyPortableObject: async () => ({ verified: true })
|
|
1053
1400
|
}), Note);
|
|
1054
1401
|
deepStrictEqual(fetchedUrls, ["ap+ef61://did:key:z6Mkabc/objects/1"]);
|
|
1055
1402
|
});
|
|
@@ -1078,6 +1425,72 @@ test({
|
|
|
1078
1425
|
deepStrictEqual(objects2[0].name, "Second object");
|
|
1079
1426
|
}
|
|
1080
1427
|
});
|
|
1428
|
+
function embeddedPropertyDocument(property) {
|
|
1429
|
+
const context = "https://www.w3.org/ns/activitystreams";
|
|
1430
|
+
return {
|
|
1431
|
+
"@context": [context, { CustomImage: `${context}#Image` }],
|
|
1432
|
+
type: property === "attachment" ? "Note" : "Create",
|
|
1433
|
+
[property]: [
|
|
1434
|
+
{
|
|
1435
|
+
"@context": [context, { "@vocab": "https://example.com/" }],
|
|
1436
|
+
type: "CustomImage",
|
|
1437
|
+
name: "Malformed when reparsed"
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
"@context": context,
|
|
1441
|
+
type: "Image",
|
|
1442
|
+
name: "Cached image"
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
type: "Image",
|
|
1446
|
+
name: "Decoded image"
|
|
1447
|
+
}
|
|
1448
|
+
]
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
for (const suppressError of [
|
|
1452
|
+
void 0,
|
|
1453
|
+
false,
|
|
1454
|
+
true
|
|
1455
|
+
]) {
|
|
1456
|
+
test(`Note.getAttachments() cached parse failure (suppressError: ${suppressError})`, async () => {
|
|
1457
|
+
const jsonLd = embeddedPropertyDocument("attachment");
|
|
1458
|
+
const note = await Note.fromJsonLd(jsonLd);
|
|
1459
|
+
if (suppressError) {
|
|
1460
|
+
for (let i = 0; i < 2; i++) deepStrictEqual((await Array.fromAsync(note.getAttachments({ suppressError }))).map((attachment) => attachment.name), ["Cached image", "Decoded image"]);
|
|
1461
|
+
await rejects(() => Array.fromAsync(note.getAttachments()), TypeError);
|
|
1462
|
+
} else await rejects(() => Array.fromAsync(note.getAttachments({ suppressError })), TypeError);
|
|
1463
|
+
deepStrictEqual(await note.toJsonLd(), jsonLd);
|
|
1464
|
+
});
|
|
1465
|
+
test(`Activity.getObject() cached parse failure (suppressError: ${suppressError})`, async () => {
|
|
1466
|
+
const jsonLd = embeddedPropertyDocument("object");
|
|
1467
|
+
const activity = await Activity.fromJsonLd(jsonLd);
|
|
1468
|
+
if (suppressError) {
|
|
1469
|
+
deepStrictEqual(await activity.getObject({ suppressError }), null);
|
|
1470
|
+
deepStrictEqual(await activity.getObject({ suppressError }), null);
|
|
1471
|
+
await rejects(() => activity.getObject(), TypeError);
|
|
1472
|
+
} else await rejects(() => activity.getObject({ suppressError }), TypeError);
|
|
1473
|
+
deepStrictEqual(await activity.toJsonLd(), jsonLd);
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
test("Note.getAttachments() suppresses cached context loader failures", async () => {
|
|
1477
|
+
const note = await Note.fromJsonLd(embeddedPropertyDocument("attachment"));
|
|
1478
|
+
const contextLoader = () => Promise.reject(/* @__PURE__ */ new Error("Context loader failed"));
|
|
1479
|
+
await rejects(() => Array.fromAsync(note.getAttachments({ contextLoader })), (error) => error instanceof Error && !(error instanceof TypeError));
|
|
1480
|
+
deepStrictEqual((await Array.fromAsync(note.getAttachments({
|
|
1481
|
+
contextLoader,
|
|
1482
|
+
suppressError: true
|
|
1483
|
+
}))).map((attachment) => attachment.name), ["Decoded image"]);
|
|
1484
|
+
});
|
|
1485
|
+
test("Activity.getObject() suppresses cached context loader failures", async () => {
|
|
1486
|
+
const activity = await Activity.fromJsonLd(embeddedPropertyDocument("object"));
|
|
1487
|
+
const contextLoader = () => Promise.reject(/* @__PURE__ */ new Error("Context loader failed"));
|
|
1488
|
+
await rejects(() => activity.getObject({ contextLoader }), (error) => error instanceof Error && !(error instanceof TypeError));
|
|
1489
|
+
deepStrictEqual(await activity.getObject({
|
|
1490
|
+
contextLoader,
|
|
1491
|
+
suppressError: true
|
|
1492
|
+
}), null);
|
|
1493
|
+
});
|
|
1081
1494
|
test("Activity.clone()", async () => {
|
|
1082
1495
|
const activity = new Activity({
|
|
1083
1496
|
actor: new Person({ name: "John Doe" }),
|
|
@@ -1125,6 +1538,21 @@ test({
|
|
|
1125
1538
|
}), "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}");
|
|
1126
1539
|
}
|
|
1127
1540
|
});
|
|
1541
|
+
test("inspecting an object prints portable IRIs in their canonical form", () => {
|
|
1542
|
+
const did = "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2";
|
|
1543
|
+
const obj = new Object$1({
|
|
1544
|
+
id: parseIri(`ap://${did}/note`),
|
|
1545
|
+
attribution: parseIri(`ap://${did}/actor`)
|
|
1546
|
+
});
|
|
1547
|
+
const inspected = [inspect(obj, { colors: false })];
|
|
1548
|
+
if ("Deno" in globalThis) inspected.push(Deno.inspect(obj, { colors: false }));
|
|
1549
|
+
for (const text of inspected) {
|
|
1550
|
+
ok(text.includes(`ap+ef61://${did}/note`), text);
|
|
1551
|
+
ok(text.includes(`ap+ef61://${did}/actor`), text);
|
|
1552
|
+
ok(!text.includes("%3A"), text);
|
|
1553
|
+
}
|
|
1554
|
+
ok(inspect(new Object$1({ id: new URL("ap://not-a-did/note") })).includes("ap://not-a-did/note"));
|
|
1555
|
+
});
|
|
1128
1556
|
test("Person.fromJsonLd()", async () => {
|
|
1129
1557
|
const person = await Person.fromJsonLd({
|
|
1130
1558
|
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
|
|
@@ -1156,6 +1584,7 @@ test("Person.toJsonLd()", async () => {
|
|
|
1156
1584
|
deepStrictEqual(await new Person({ aliases: [new URL("https://example.com/alias")] }).toJsonLd(), {
|
|
1157
1585
|
"@context": [
|
|
1158
1586
|
"https://www.w3.org/ns/activitystreams",
|
|
1587
|
+
"https://w3id.org/fep/ef61",
|
|
1159
1588
|
"https://w3id.org/security/v1",
|
|
1160
1589
|
"https://w3id.org/security/data-integrity/v1",
|
|
1161
1590
|
"https://www.w3.org/ns/did/v1",
|
|
@@ -1657,6 +2086,7 @@ test("InteractionPolicy.canFeature", async () => {
|
|
|
1657
2086
|
const expected = {
|
|
1658
2087
|
"@context": [
|
|
1659
2088
|
"https://www.w3.org/ns/activitystreams",
|
|
2089
|
+
"https://w3id.org/fep/ef61",
|
|
1660
2090
|
"https://w3id.org/security/v1",
|
|
1661
2091
|
"https://w3id.org/security/data-integrity/v1",
|
|
1662
2092
|
"https://www.w3.org/ns/did/v1",
|
|
@@ -1953,6 +2383,83 @@ test("Delete.toJsonLd() compacts embedded QuoteRequest", async () => {
|
|
|
1953
2383
|
type: "Delete"
|
|
1954
2384
|
});
|
|
1955
2385
|
});
|
|
2386
|
+
test("Note.quoteUrl (IRI-typed alias terms)", async () => {
|
|
2387
|
+
const jsonLd = {
|
|
2388
|
+
"@context": ["https://www.w3.org/ns/activitystreams", {
|
|
2389
|
+
fedibird: "http://fedibird.com/ns#",
|
|
2390
|
+
misskey: "https://misskey-hub.net/ns#",
|
|
2391
|
+
_misskey_quote: {
|
|
2392
|
+
"@id": "misskey:_misskey_quote",
|
|
2393
|
+
"@type": "@id"
|
|
2394
|
+
},
|
|
2395
|
+
quoteUri: {
|
|
2396
|
+
"@id": "fedibird:quoteUri",
|
|
2397
|
+
"@type": "@id"
|
|
2398
|
+
}
|
|
2399
|
+
}],
|
|
2400
|
+
id: "https://example.com/notes/1",
|
|
2401
|
+
type: "Note",
|
|
2402
|
+
_misskey_quote: "https://example.com/notes/quoted",
|
|
2403
|
+
quoteUri: "https://example.com/notes/quoted2"
|
|
2404
|
+
};
|
|
2405
|
+
deepStrictEqual((await Note.fromJsonLd(jsonLd)).quoteUrl, new URL("https://example.com/notes/quoted"));
|
|
2406
|
+
delete jsonLd._misskey_quote;
|
|
2407
|
+
deepStrictEqual((await Note.fromJsonLd(jsonLd)).quoteUrl, new URL("https://example.com/notes/quoted2"));
|
|
2408
|
+
});
|
|
2409
|
+
test("Note.quoteUrl (IRI-typed primary term)", async () => {
|
|
2410
|
+
const context = ["https://www.w3.org/ns/activitystreams", {
|
|
2411
|
+
misskey: "https://misskey-hub.net/ns#",
|
|
2412
|
+
quoteUrl: {
|
|
2413
|
+
"@id": "as:quoteUrl",
|
|
2414
|
+
"@type": "@id"
|
|
2415
|
+
},
|
|
2416
|
+
_misskey_quote: "misskey:_misskey_quote"
|
|
2417
|
+
}];
|
|
2418
|
+
deepStrictEqual((await Note.fromJsonLd({
|
|
2419
|
+
"@context": context,
|
|
2420
|
+
type: "Note",
|
|
2421
|
+
quoteUrl: "https://example.com/object"
|
|
2422
|
+
})).quoteUrl, new URL("https://example.com/object"));
|
|
2423
|
+
deepStrictEqual((await Note.fromJsonLd({
|
|
2424
|
+
"@context": context,
|
|
2425
|
+
type: "Note",
|
|
2426
|
+
quoteUrl: "https://example.com/object",
|
|
2427
|
+
_misskey_quote: "https://example.com/object2"
|
|
2428
|
+
})).quoteUrl, new URL("https://example.com/object"));
|
|
2429
|
+
});
|
|
2430
|
+
test("Note.quoteUrl (unparsable IRI-valued terms)", async () => {
|
|
2431
|
+
const context = ["https://www.w3.org/ns/activitystreams", {
|
|
2432
|
+
misskey: "https://misskey-hub.net/ns#",
|
|
2433
|
+
_misskey_quote: {
|
|
2434
|
+
"@id": "misskey:_misskey_quote",
|
|
2435
|
+
"@type": "@id"
|
|
2436
|
+
}
|
|
2437
|
+
}];
|
|
2438
|
+
for (const quote of [
|
|
2439
|
+
{
|
|
2440
|
+
type: "Note",
|
|
2441
|
+
content: "An inlined quote without an id"
|
|
2442
|
+
},
|
|
2443
|
+
"_:b0",
|
|
2444
|
+
"",
|
|
2445
|
+
"notes/relative"
|
|
2446
|
+
]) {
|
|
2447
|
+
const loaded = await Note.fromJsonLd({
|
|
2448
|
+
"@context": context,
|
|
2449
|
+
id: "https://example.com/notes/1",
|
|
2450
|
+
type: "Note",
|
|
2451
|
+
content: "Hello",
|
|
2452
|
+
_misskey_quote: quote
|
|
2453
|
+
});
|
|
2454
|
+
deepStrictEqual(loaded.quoteUrl, null, JSON.stringify(quote));
|
|
2455
|
+
deepStrictEqual(loaded.content, "Hello");
|
|
2456
|
+
}
|
|
2457
|
+
deepStrictEqual((await Note.fromJsonLd({
|
|
2458
|
+
"@context": context,
|
|
2459
|
+
type: "Note",
|
|
2460
|
+
_misskey_quote: "at://did:plc:abc/app.bsky.feed.post/xyz"
|
|
2461
|
+
})).quoteUrl, new URL("at://" + encodeURIComponent("did:plc:abc/app.bsky.feed.post/xyz")));
|
|
2462
|
+
});
|
|
1956
2463
|
test("Key.publicKey", async () => {
|
|
1957
2464
|
const jwk = {
|
|
1958
2465
|
kty: "RSA",
|
|
@@ -2690,7 +3197,11 @@ test("FEP-fe34: Portable DID authorities are cross-origin boundaries", async ()
|
|
|
2690
3197
|
};
|
|
2691
3198
|
throw new Error("Document not found");
|
|
2692
3199
|
};
|
|
2693
|
-
const result = await create.getObject({
|
|
3200
|
+
const result = await create.getObject({
|
|
3201
|
+
documentLoader,
|
|
3202
|
+
contextLoader: mockDocumentLoader,
|
|
3203
|
+
verifyPortableObject: async () => ({ verified: true })
|
|
3204
|
+
});
|
|
2694
3205
|
assertInstanceOf(result, Note);
|
|
2695
3206
|
deepStrictEqual(result.content, "Fetched portable note");
|
|
2696
3207
|
});
|
|
@@ -3234,6 +3745,7 @@ const sampleValues = {
|
|
|
3234
3745
|
]),
|
|
3235
3746
|
"fedify:langTag": new Intl.Locale("en-Latn-US"),
|
|
3236
3747
|
"fedify:url": new URL("https://fedify.dev/"),
|
|
3748
|
+
"fedify:gatewayUrl": new URL("https://gateway.example/"),
|
|
3237
3749
|
"fedify:publicKey": rsaPublicKey.publicKey,
|
|
3238
3750
|
"fedify:multibaseKey": ed25519PublicKey.publicKey,
|
|
3239
3751
|
"fedify:proofPurpose": "assertionMethod",
|
|
@@ -3385,7 +3897,9 @@ for (const typeUri in types) {
|
|
|
3385
3897
|
...initValues
|
|
3386
3898
|
});
|
|
3387
3899
|
const jsonLd = await instance.toJsonLd({ contextLoader: mockDocumentLoader });
|
|
3388
|
-
|
|
3900
|
+
const defaultContexts = Array.isArray(type.defaultContext) ? type.defaultContext : [type.defaultContext];
|
|
3901
|
+
const extraContexts = [...new Set(allProperties.flatMap((property) => property.extraContext == null || defaultContexts.some((context) => context === property.extraContext) ? [] : [property.extraContext]))];
|
|
3902
|
+
deepStrictEqual(jsonLd["@context"], extraContexts.length === 0 ? type.defaultContext : [...defaultContexts, ...extraContexts]);
|
|
3389
3903
|
if (type.entity) deepStrictEqual(jsonLd.id, "https://example.com/");
|
|
3390
3904
|
const restored = await cls.fromJsonLd(jsonLd, {
|
|
3391
3905
|
documentLoader: mockDocumentLoader,
|
|
@@ -3448,5 +3962,309 @@ for (const typeUri in types) {
|
|
|
3448
3962
|
deepStrictEqual(cls.typeId, new URL(type.uri));
|
|
3449
3963
|
});
|
|
3450
3964
|
}
|
|
3965
|
+
test("Announce.getObject() suppresses redirect, context, and private URL logs", async () => {
|
|
3966
|
+
const records = [];
|
|
3967
|
+
const objectUrl = "https://example.com/suppression-object";
|
|
3968
|
+
const contextUrl = "https://example.com/suppression-context";
|
|
3969
|
+
const scenarios = [
|
|
3970
|
+
"redirect limit",
|
|
3971
|
+
"redirect loop",
|
|
3972
|
+
"context",
|
|
3973
|
+
"private",
|
|
3974
|
+
"private redirect"
|
|
3975
|
+
];
|
|
3976
|
+
await reset();
|
|
3977
|
+
await configure({
|
|
3978
|
+
sinks: { capture: (record) => {
|
|
3979
|
+
records.push(record);
|
|
3980
|
+
} },
|
|
3981
|
+
loggers: [{
|
|
3982
|
+
category: ["fedify"],
|
|
3983
|
+
lowestLevel: "debug",
|
|
3984
|
+
sinks: ["capture"]
|
|
3985
|
+
}]
|
|
3986
|
+
});
|
|
3987
|
+
try {
|
|
3988
|
+
for (const scenario of scenarios) {
|
|
3989
|
+
esm_default.spyGlobal();
|
|
3990
|
+
let fetches = 0;
|
|
3991
|
+
esm_default.get("begin:https://example.com/", (call) => {
|
|
3992
|
+
fetches++;
|
|
3993
|
+
if (scenario === "redirect limit") return Response.redirect(`https://example.com/redirect-${fetches}`, 302);
|
|
3994
|
+
if (scenario === "redirect loop") return Response.redirect(objectUrl, 302);
|
|
3995
|
+
if (scenario === "private redirect") return Response.redirect("http://127.0.0.1/private", 302);
|
|
3996
|
+
if (call.url === contextUrl) return new Response(null, { status: 404 });
|
|
3997
|
+
return new Response(JSON.stringify({
|
|
3998
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
3999
|
+
id: objectUrl,
|
|
4000
|
+
type: "Note"
|
|
4001
|
+
}), { headers: { "Content-Type": "application/activity+json" } });
|
|
4002
|
+
});
|
|
4003
|
+
try {
|
|
4004
|
+
for (const suppressError of [
|
|
4005
|
+
true,
|
|
4006
|
+
false,
|
|
4007
|
+
void 0
|
|
4008
|
+
]) {
|
|
4009
|
+
records.length = 0;
|
|
4010
|
+
fetches = 0;
|
|
4011
|
+
const announce = new Announce({ object: new URL(scenario === "private" ? "http://127.0.0.1/private" : objectUrl) });
|
|
4012
|
+
if (suppressError) {
|
|
4013
|
+
deepStrictEqual(await announce.getObject({ suppressError }), null);
|
|
4014
|
+
deepStrictEqual(records.filter((r) => r.level === "error"), [], scenario);
|
|
4015
|
+
ok(records.some((r) => r.level === "warning"), scenario);
|
|
4016
|
+
} else {
|
|
4017
|
+
await rejects(() => announce.getObject({ suppressError }));
|
|
4018
|
+
ok(records.some((r) => r.level === "error"), scenario);
|
|
4019
|
+
}
|
|
4020
|
+
if (scenario === "private") deepStrictEqual(fetches, 0);
|
|
4021
|
+
if (scenario === "private redirect") deepStrictEqual(fetches, 1);
|
|
4022
|
+
}
|
|
4023
|
+
} finally {
|
|
4024
|
+
esm_default.hardReset();
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
} finally {
|
|
4028
|
+
await reset();
|
|
4029
|
+
}
|
|
4030
|
+
});
|
|
4031
|
+
test("suppressed context loading does not persist on fetched objects", async () => {
|
|
4032
|
+
const states = [];
|
|
4033
|
+
const contextLoader = (url, options) => {
|
|
4034
|
+
if (url === "https://www.w3.org/ns/activitystreams") return mockDocumentLoader(url);
|
|
4035
|
+
states.push(options?.suppressError);
|
|
4036
|
+
return Promise.resolve({
|
|
4037
|
+
contextUrl: null,
|
|
4038
|
+
documentUrl: url,
|
|
4039
|
+
document: { "@context": {
|
|
4040
|
+
as: "https://www.w3.org/ns/activitystreams#",
|
|
4041
|
+
name: "as:name"
|
|
4042
|
+
} }
|
|
4043
|
+
});
|
|
4044
|
+
};
|
|
4045
|
+
const documentLoader = (url) => Promise.resolve({
|
|
4046
|
+
contextUrl: null,
|
|
4047
|
+
documentUrl: url,
|
|
4048
|
+
document: {
|
|
4049
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://example.com/scoped-context"],
|
|
4050
|
+
id: url,
|
|
4051
|
+
type: url.endsWith("actor") ? "Person" : "Note",
|
|
4052
|
+
attributedTo: "https://example.com/scoped-actor"
|
|
4053
|
+
}
|
|
4054
|
+
});
|
|
4055
|
+
const note = await new Announce({ object: new URL("https://example.com/scoped-note") }).getObject({
|
|
4056
|
+
documentLoader,
|
|
4057
|
+
contextLoader,
|
|
4058
|
+
suppressError: true
|
|
4059
|
+
});
|
|
4060
|
+
assertInstanceOf(note, Note);
|
|
4061
|
+
ok(states.includes(true));
|
|
4062
|
+
states.length = 0;
|
|
4063
|
+
assertInstanceOf(await note.getAttribution(), Person);
|
|
4064
|
+
ok(states.length > 0);
|
|
4065
|
+
deepStrictEqual(states.includes(true), false);
|
|
4066
|
+
});
|
|
4067
|
+
test("attribution chains alternate ordinary and portable suppression", async () => {
|
|
4068
|
+
const states = [];
|
|
4069
|
+
const depths = [];
|
|
4070
|
+
let hop = 0;
|
|
4071
|
+
const contextUrl = "https://example.com/attribution-context";
|
|
4072
|
+
const contextLoader = (url, options) => {
|
|
4073
|
+
if (url !== contextUrl) return mockDocumentLoader(url, options);
|
|
4074
|
+
states.push(options?.suppressError);
|
|
4075
|
+
const stack = (/* @__PURE__ */ new Error()).stack;
|
|
4076
|
+
ok(stack);
|
|
4077
|
+
depths[hop]?.push(stack.split("\n").length);
|
|
4078
|
+
return Promise.resolve({
|
|
4079
|
+
contextUrl: null,
|
|
4080
|
+
documentUrl: url,
|
|
4081
|
+
document: { "@context": { name: "https://www.w3.org/ns/activitystreams#name" } }
|
|
4082
|
+
});
|
|
4083
|
+
};
|
|
4084
|
+
const documentLoader = (url) => {
|
|
4085
|
+
const hop = Number(new URL(url).pathname.split("/").at(-1));
|
|
4086
|
+
return Promise.resolve({
|
|
4087
|
+
contextUrl: null,
|
|
4088
|
+
documentUrl: url,
|
|
4089
|
+
document: {
|
|
4090
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
4091
|
+
id: url,
|
|
4092
|
+
type: "Person",
|
|
4093
|
+
name: `Actor ${hop}`,
|
|
4094
|
+
attributedTo: `https://example.com/actors/${hop + 1}`
|
|
4095
|
+
}
|
|
4096
|
+
});
|
|
4097
|
+
};
|
|
4098
|
+
let person = new Person({
|
|
4099
|
+
id: new URL("https://example.com/actors/root"),
|
|
4100
|
+
attribution: new URL("https://example.com/actors/0")
|
|
4101
|
+
}, {
|
|
4102
|
+
documentLoader,
|
|
4103
|
+
contextLoader
|
|
4104
|
+
});
|
|
4105
|
+
const stackTraceLimit = globalThis.Object.getOwnPropertyDescriptor(Error, "stackTraceLimit");
|
|
4106
|
+
try {
|
|
4107
|
+
ok(Reflect.set(Error, "stackTraceLimit", Infinity));
|
|
4108
|
+
for (; hop < 100; hop++) {
|
|
4109
|
+
depths[hop] = [];
|
|
4110
|
+
const next = await person.getAttribution({
|
|
4111
|
+
suppressError: true,
|
|
4112
|
+
...hop % 2 === 0 ? {} : { verifyPortableObject: () => Promise.resolve({ verified: true }) }
|
|
4113
|
+
});
|
|
4114
|
+
assertInstanceOf(next, Person);
|
|
4115
|
+
deepStrictEqual(next.id, new URL(`https://example.com/actors/${hop}`));
|
|
4116
|
+
person = next;
|
|
4117
|
+
}
|
|
4118
|
+
ok(depths.every((values) => values.length > 0));
|
|
4119
|
+
deepStrictEqual(depths[98], depths[2]);
|
|
4120
|
+
deepStrictEqual(depths[99], depths[3]);
|
|
4121
|
+
ok(states.length >= 100);
|
|
4122
|
+
ok(states.every((state) => state === true));
|
|
4123
|
+
states.length = 0;
|
|
4124
|
+
assertInstanceOf(await person.getAttribution(), Person);
|
|
4125
|
+
ok(states.length > 0);
|
|
4126
|
+
ok(states.every((state) => state !== true));
|
|
4127
|
+
} finally {
|
|
4128
|
+
if (stackTraceLimit == null) Reflect.deleteProperty(Error, "stackTraceLimit");
|
|
4129
|
+
else globalThis.Object.defineProperty(Error, "stackTraceLimit", stackTraceLimit);
|
|
4130
|
+
}
|
|
4131
|
+
});
|
|
4132
|
+
test("portable accessors suppress document and context failure logs", async () => {
|
|
4133
|
+
const records = [];
|
|
4134
|
+
const id = new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
|
|
4135
|
+
const contextUrl = "https://example.com/portable-suppression-context";
|
|
4136
|
+
await reset();
|
|
4137
|
+
await configure({
|
|
4138
|
+
sinks: { capture: (record) => {
|
|
4139
|
+
records.push(record);
|
|
4140
|
+
} },
|
|
4141
|
+
loggers: [{
|
|
4142
|
+
category: ["fedify"],
|
|
4143
|
+
lowestLevel: "debug",
|
|
4144
|
+
sinks: ["capture"]
|
|
4145
|
+
}]
|
|
4146
|
+
});
|
|
4147
|
+
try {
|
|
4148
|
+
for (const failContext of [false, true]) {
|
|
4149
|
+
esm_default.spyGlobal();
|
|
4150
|
+
esm_default.get("begin:https://example.com/", (call) => {
|
|
4151
|
+
if (!failContext || call.url === contextUrl) return new Response(null, { status: 404 });
|
|
4152
|
+
return new Response(JSON.stringify({
|
|
4153
|
+
"@context": ["https://www.w3.org/ns/activitystreams", contextUrl],
|
|
4154
|
+
id: "ap://did:key:z6Mkabc/objects/1",
|
|
4155
|
+
type: "Note"
|
|
4156
|
+
}), { headers: { "Content-Type": "application/activity+json" } });
|
|
4157
|
+
});
|
|
4158
|
+
try {
|
|
4159
|
+
for (const suppressError of [
|
|
4160
|
+
true,
|
|
4161
|
+
false,
|
|
4162
|
+
void 0
|
|
4163
|
+
]) {
|
|
4164
|
+
records.length = 0;
|
|
4165
|
+
const announce = new Announce({ object: id });
|
|
4166
|
+
const options = {
|
|
4167
|
+
suppressError,
|
|
4168
|
+
gateways: ["https://example.com/"],
|
|
4169
|
+
verifyPortableObject: () => Promise.resolve({ verified: true })
|
|
4170
|
+
};
|
|
4171
|
+
if (suppressError) {
|
|
4172
|
+
deepStrictEqual(await announce.getObject(options), null);
|
|
4173
|
+
deepStrictEqual(records.filter((r) => r.level === "error"), []);
|
|
4174
|
+
ok(records.some((r) => r.level === "warning"));
|
|
4175
|
+
} else {
|
|
4176
|
+
await rejects(() => announce.getObject(options));
|
|
4177
|
+
ok(records.some((r) => r.level === "error"));
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
} finally {
|
|
4181
|
+
esm_default.hardReset();
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
} finally {
|
|
4185
|
+
await reset();
|
|
4186
|
+
}
|
|
4187
|
+
});
|
|
4188
|
+
test("portable verifiers suppress document logs only during dereferencing", async () => {
|
|
4189
|
+
const records = [];
|
|
4190
|
+
const id = new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1");
|
|
4191
|
+
const verificationUrl = "https://example.com/verification-document";
|
|
4192
|
+
const missingUrl = "https://example.com/missing-verification-document";
|
|
4193
|
+
const signal = new AbortController().signal;
|
|
4194
|
+
await reset();
|
|
4195
|
+
await configure({
|
|
4196
|
+
sinks: { capture: (record) => records.push(record) },
|
|
4197
|
+
loggers: [{
|
|
4198
|
+
category: ["fedify"],
|
|
4199
|
+
lowestLevel: "debug",
|
|
4200
|
+
sinks: ["capture"]
|
|
4201
|
+
}]
|
|
4202
|
+
});
|
|
4203
|
+
try {
|
|
4204
|
+
for (const failVerification of [false, true]) {
|
|
4205
|
+
esm_default.spyGlobal();
|
|
4206
|
+
esm_default.get("begin:https://example.com/.well-known/", {
|
|
4207
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
4208
|
+
body: {
|
|
4209
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
4210
|
+
id: "ap://did:key:z6Mkabc/objects/1",
|
|
4211
|
+
type: "Note"
|
|
4212
|
+
}
|
|
4213
|
+
});
|
|
4214
|
+
esm_default.get(verificationUrl, {
|
|
4215
|
+
status: failVerification ? 404 : 200,
|
|
4216
|
+
headers: { "Content-Type": "application/ld+json" },
|
|
4217
|
+
body: {}
|
|
4218
|
+
});
|
|
4219
|
+
esm_default.get(missingUrl, { status: 404 });
|
|
4220
|
+
try {
|
|
4221
|
+
for (const suppressError of [
|
|
4222
|
+
true,
|
|
4223
|
+
false,
|
|
4224
|
+
void 0
|
|
4225
|
+
]) {
|
|
4226
|
+
records.length = 0;
|
|
4227
|
+
const baseLoader = getDocumentLoader();
|
|
4228
|
+
const calls = [];
|
|
4229
|
+
const documentLoader = (url, options) => {
|
|
4230
|
+
calls.push(options);
|
|
4231
|
+
return baseLoader(url, options);
|
|
4232
|
+
};
|
|
4233
|
+
let retainedLoader;
|
|
4234
|
+
const loaderOptions = { signal };
|
|
4235
|
+
const object = await new Announce({ object: id }).getObject({
|
|
4236
|
+
suppressError,
|
|
4237
|
+
documentLoader,
|
|
4238
|
+
gateways: ["https://example.com/"],
|
|
4239
|
+
verifyPortableObject: async (_document, options) => {
|
|
4240
|
+
retainedLoader = options.documentLoader;
|
|
4241
|
+
ok(options.documentLoader);
|
|
4242
|
+
await options.documentLoader(verificationUrl, loaderOptions);
|
|
4243
|
+
return { verified: true };
|
|
4244
|
+
}
|
|
4245
|
+
});
|
|
4246
|
+
if (failVerification) {
|
|
4247
|
+
deepStrictEqual(object, null);
|
|
4248
|
+
deepStrictEqual(records.some((record) => record.level === "error"), !suppressError);
|
|
4249
|
+
ok(records.some((record) => record.level === "warning"));
|
|
4250
|
+
} else assertInstanceOf(object, Note);
|
|
4251
|
+
deepStrictEqual(calls.at(-1)?.signal, signal);
|
|
4252
|
+
deepStrictEqual(calls.at(-1)?.suppressError, suppressError ? true : void 0);
|
|
4253
|
+
deepStrictEqual(loaderOptions, { signal });
|
|
4254
|
+
records.length = 0;
|
|
4255
|
+
const releasedLoader = retainedLoader;
|
|
4256
|
+
ok(releasedLoader);
|
|
4257
|
+
await rejects(() => releasedLoader(missingUrl, loaderOptions), FetchError);
|
|
4258
|
+
deepStrictEqual(calls.at(-1), loaderOptions);
|
|
4259
|
+
ok(records.some((record) => record.level === "error"));
|
|
4260
|
+
}
|
|
4261
|
+
} finally {
|
|
4262
|
+
esm_default.hardReset();
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
} finally {
|
|
4266
|
+
await reset();
|
|
4267
|
+
}
|
|
4268
|
+
});
|
|
3451
4269
|
//#endregion
|
|
3452
4270
|
export {};
|