@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.
- package/deno.json +1 -1
- package/dist/mod.cjs +32014 -9323
- package/dist/mod.d.cts +4770 -91
- package/dist/mod.d.ts +4770 -91
- package/dist/mod.js +31907 -9223
- package/dist-tests/actor-D_QaCMro.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/{actor-MBpzF3-D.mjs → esm-YWo4aQQe.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/lookup-BevjwZhB.mjs +480 -0
- package/dist-tests/lookup.test.mjs +10 -268
- package/dist-tests/object.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -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/{vocab-Bjwctrxn.mjs → vocab-DaniQwU7.mjs} +31669 -9267
- package/dist-tests/vocab.test.mjs +901 -11
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +291 -15
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/gateway.test.ts +2637 -0
- 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/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +1 -1
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/vocab.test.ts +1219 -5
- /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 { A 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-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.
|
|
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
|
|
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({
|
|
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
|
-
|
|
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 {};
|