@fedify/vocab 2.4.0-dev.1570 → 2.4.0-dev.1571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  globalThis.addEventListener = () => {};
3
- import { A as QuoteAuthorization, C as Offer, D as Place, E as Person, F as Tombstone, M as Reject, O as Proposal, P as Source, R as vocab_exports, S as Object$1, _ as InteractionPolicy, b as Measure, c as Create, d as Document, f as Endpoints, g as Intent, h as Hashtag, i as Announce, j as QuoteRequest, k as Question, l as CryptographicKey, n as Activity, o as Collection, p as Follow, r as Agreement, s as Commitment, t as Accept, u as Delete, v as InteractionRule, w as OrderedCollectionPage, x as Note, y as Link } from "./vocab-XnSGFOqq.mjs";
3
+ import { A as Person, C as Link, D as Offer, E as Object$1, F as QuoteRequest, H as vocab_exports, I as Reject, M as Proposal, N as Question, O as OrderedCollectionPage, P as QuoteAuthorization, R as Source, S as InteractionRule, T as Note, _ as Follow, b as Intent, c as Create, d as Document, f as Endpoints, g as FeaturedItem, h as FeaturedCollection, i as Announce, j as Place, l as CryptographicKey, m as FeatureRequest, n as Activity, o as Collection, p as FeatureAuthorization, r as Agreement, s as Commitment, t as Accept, u as Delete, w as Measure, x as InteractionPolicy, y as Hashtag, z as Tombstone } from "./vocab-DOHLQ4UQ.mjs";
4
4
  import { t as assertInstanceOf } from "./utils-CE8Dk5hm.mjs";
5
5
  import { mockDocumentLoader, test } from "@fedify/fixture";
6
6
  import { deepStrictEqual, notDeepStrictEqual, ok, rejects, throws } from "node:assert/strict";
@@ -53,6 +53,18 @@ const QUOTE_REQUEST_CONTEXT = [
53
53
  }
54
54
  }
55
55
  ];
56
+ const FEATURE_CONTEXT = [
57
+ "https://www.w3.org/ns/activitystreams",
58
+ "https://w3id.org/security/data-integrity/v1",
59
+ "https://gotosocial.org/ns",
60
+ "https://w3id.org/fep/7aa9"
61
+ ];
62
+ const FEATURED_COLLECTION_CONTEXT = [...FEATURE_CONTEXT, {
63
+ Hashtag: "as:Hashtag",
64
+ discoverable: "toot:discoverable",
65
+ sensitive: "as:sensitive",
66
+ toot: "http://joinmastodon.org/ns#"
67
+ }];
56
68
  const DELETE_QUOTE_REQUEST_CONTEXT = [
57
69
  "https://w3id.org/identity/v1",
58
70
  "https://www.w3.org/ns/activitystreams",
@@ -302,6 +314,662 @@ test("Activity.fromJsonLd()", async () => {
302
314
  deepStrictEqual(proofs[0].proofValue, decodeMultibase("z3sXaxjKs4M3BRicwWA9peyNPJvJqxtGsDmpt1jjoHCjgeUf71TRFz56osPSfDErszyLp5Ks1EhYSgpDaNM977Rg2"));
303
315
  deepStrictEqual(proofs[0].created, Temporal.Instant.from("2023-02-24T23:36:38Z"));
304
316
  });
317
+ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
318
+ const did = "did:key:z6Mkabc";
319
+ const portableActor = `ap://${did}/actor?gateways=https%3A%2F%2Fserver.example`;
320
+ const portableObject = `ap://${did}/objects/1?gateways=https%3A%2F%2Fserver.example`;
321
+ const uppercasePortableObject = `AP://${did}/objects/1?gateways=https%3A%2F%2Fserver.example`;
322
+ const portablePage = `ap://${did}/actor/outbox?page=2`;
323
+ const note = await Note.fromJsonLd({
324
+ "@context": "https://www.w3.org/ns/activitystreams",
325
+ type: "Note",
326
+ id: uppercasePortableObject,
327
+ attributedTo: `ap+ef61://${encodeURIComponent(did)}/actor?gateways=https%3A%2F%2Fserver.example`
328
+ }, {
329
+ documentLoader: mockDocumentLoader,
330
+ contextLoader: mockDocumentLoader
331
+ });
332
+ deepStrictEqual(note.id, new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example"));
333
+ deepStrictEqual(note.attributionId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor?gateways=https%3A%2F%2Fserver.example"));
334
+ const noteJson = await note.toJsonLd({ contextLoader: mockDocumentLoader });
335
+ deepStrictEqual(noteJson.type, "Note");
336
+ deepStrictEqual(noteJson.id, "ap+ef61://did:key:z6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example");
337
+ deepStrictEqual(noteJson.attributedTo, "ap+ef61://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fserver.example");
338
+ const activity = await Activity.fromJsonLd({
339
+ "@context": "https://www.w3.org/ns/activitystreams",
340
+ type: "Create",
341
+ actor: portableActor,
342
+ object: portableObject
343
+ }, {
344
+ documentLoader: mockDocumentLoader,
345
+ contextLoader: mockDocumentLoader
346
+ });
347
+ deepStrictEqual(activity.actorId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor?gateways=https%3A%2F%2Fserver.example"));
348
+ const activityJson = await activity.toJsonLd({ contextLoader: mockDocumentLoader });
349
+ deepStrictEqual(activityJson.type, "Create");
350
+ deepStrictEqual(activityJson.actor, "ap+ef61://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fserver.example");
351
+ deepStrictEqual(activityJson.object, "ap+ef61://did:key:z6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example");
352
+ const person = await Person.fromJsonLd({
353
+ "@context": "https://www.w3.org/ns/activitystreams",
354
+ type: "Person",
355
+ inbox: `ap+ef61://${did}/actor/inbox`,
356
+ outbox: `ap+ef61://${did}/actor/outbox`
357
+ }, {
358
+ documentLoader: mockDocumentLoader,
359
+ contextLoader: mockDocumentLoader
360
+ });
361
+ deepStrictEqual(person.inboxId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/inbox"));
362
+ deepStrictEqual(person.outboxId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox"));
363
+ const personJson = await person.toJsonLd({ contextLoader: mockDocumentLoader });
364
+ deepStrictEqual(personJson.type, "Person");
365
+ deepStrictEqual(personJson.inbox, "ap+ef61://did:key:z6Mkabc/actor/inbox");
366
+ deepStrictEqual(personJson.outbox, "ap+ef61://did:key:z6Mkabc/actor/outbox");
367
+ const page = await OrderedCollectionPage.fromJsonLd({
368
+ "@context": "https://www.w3.org/ns/activitystreams",
369
+ type: "OrderedCollectionPage",
370
+ next: portablePage,
371
+ prev: `ap+ef61://${encodeURIComponent(did)}/actor/outbox?page=1`
372
+ }, {
373
+ documentLoader: mockDocumentLoader,
374
+ contextLoader: mockDocumentLoader
375
+ });
376
+ deepStrictEqual(page.nextId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox?page=2"));
377
+ deepStrictEqual(page.prevId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox?page=1"));
378
+ const pageJson = await page.toJsonLd({ contextLoader: mockDocumentLoader });
379
+ deepStrictEqual(pageJson.type, "OrderedCollectionPage");
380
+ deepStrictEqual(pageJson.next, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=2");
381
+ deepStrictEqual(pageJson.prev, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=1");
382
+ });
383
+ test("fromJsonLd() caches text that mentions portable ActivityPub IRIs", async () => {
384
+ const noteJson = {
385
+ "@context": ["https://www.w3.org/ns/activitystreams", { extra: "https://example.com/ns#extra" }],
386
+ type: "Note",
387
+ id: "https://example.com/notes/1",
388
+ content: "This is text about ap://did:key:z6Mkabc/actor.",
389
+ extra: "This extension property should stay cached."
390
+ };
391
+ deepStrictEqual(await (await Note.fromJsonLd(noteJson, {
392
+ documentLoader: mockDocumentLoader,
393
+ contextLoader: mockDocumentLoader
394
+ })).toJsonLd(), noteJson);
395
+ });
396
+ test("fromJsonLd() preserves extensions with portable ActivityPub IRIs", async () => {
397
+ const jsonLd = await (await Note.fromJsonLd({
398
+ "@context": ["https://www.w3.org/ns/activitystreams", { extra: "https://example.com/ns#extra" }],
399
+ type: "Note",
400
+ id: "ap://did:key:z6Mkabc/objects/1",
401
+ extra: "This extension property should stay cached."
402
+ }, {
403
+ documentLoader: mockDocumentLoader,
404
+ contextLoader: mockDocumentLoader
405
+ })).toJsonLd();
406
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
407
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
408
+ });
409
+ test("fromJsonLd() preserves unmapped terms with portable IRIs", async () => {
410
+ const jsonLd = await (await Note.fromJsonLd({
411
+ "@context": "https://www.w3.org/ns/activitystreams",
412
+ type: "Note",
413
+ id: "ap://did:key:z6Mkabc/objects/1",
414
+ extra: "This unmapped property should stay cached."
415
+ }, {
416
+ documentLoader: mockDocumentLoader,
417
+ contextLoader: mockDocumentLoader
418
+ })).toJsonLd();
419
+ deepStrictEqual(jsonLd.extra, "This unmapped property should stay cached.");
420
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
421
+ });
422
+ test("fromJsonLd() preserves expanded arrays with portable IRIs", async () => {
423
+ const expanded = [{
424
+ "@id": "https://example.com/activities/1",
425
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
426
+ "https://www.w3.org/ns/activitystreams#actor": [{ "@id": "ap://did:key:z6Mkabc/actor" }],
427
+ "https://www.w3.org/ns/activitystreams#object": [{ "@id": "https://example.com/objects/1" }]
428
+ }, {
429
+ "@id": "https://example.com/objects/1",
430
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
431
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "Sibling node should stay cached." }]
432
+ }];
433
+ const activity = await Activity.fromJsonLd(expanded, {
434
+ documentLoader: mockDocumentLoader,
435
+ contextLoader: mockDocumentLoader
436
+ });
437
+ deepStrictEqual(activity.actorId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
438
+ deepStrictEqual(await activity.toJsonLd(), [{
439
+ "@id": "https://example.com/activities/1",
440
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
441
+ "https://www.w3.org/ns/activitystreams#actor": [{ "@id": "ap+ef61://did:key:z6Mkabc/actor" }],
442
+ "https://www.w3.org/ns/activitystreams#object": [{ "@id": "https://example.com/objects/1" }]
443
+ }, {
444
+ "@id": "https://example.com/objects/1",
445
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
446
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "Sibling node should stay cached." }]
447
+ }]);
448
+ deepStrictEqual(expanded[0]["https://www.w3.org/ns/activitystreams#actor"], [{ "@id": "ap://did:key:z6Mkabc/actor" }]);
449
+ });
450
+ test("fromJsonLd() preserves single-node expanded arrays with portable IRIs", async () => {
451
+ const expanded = [{
452
+ "@id": "ap://did:key:z6Mkabc/objects/1",
453
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
454
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap://did:key:z6Mkabc/actor" }],
455
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "Single expanded node should stay cached as an array." }]
456
+ }];
457
+ deepStrictEqual(await (await Note.fromJsonLd(expanded, {
458
+ documentLoader: mockDocumentLoader,
459
+ contextLoader: mockDocumentLoader
460
+ })).toJsonLd(), [{
461
+ "@id": "ap+ef61://did:key:z6Mkabc/objects/1",
462
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
463
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap+ef61://did:key:z6Mkabc/actor" }],
464
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "Single expanded node should stay cached as an array." }]
465
+ }]);
466
+ deepStrictEqual(expanded[0]["@id"], "ap://did:key:z6Mkabc/objects/1");
467
+ });
468
+ test("fromJsonLd() preserves no-context object shape with portable IRIs", async () => {
469
+ const expanded = {
470
+ "@id": "ap://did:key:z6Mkabc/objects/1",
471
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
472
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap://did:key:z6Mkabc/actor" }],
473
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "No-context object shape should stay cached." }]
474
+ };
475
+ deepStrictEqual(await (await Note.fromJsonLd(expanded, {
476
+ documentLoader: mockDocumentLoader,
477
+ contextLoader: mockDocumentLoader
478
+ })).toJsonLd(), {
479
+ "@id": "ap+ef61://did:key:z6Mkabc/objects/1",
480
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
481
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap+ef61://did:key:z6Mkabc/actor" }],
482
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "No-context object shape should stay cached." }]
483
+ });
484
+ deepStrictEqual(expanded["@id"], "ap://did:key:z6Mkabc/objects/1");
485
+ });
486
+ test("fromJsonLd() preserves expanded subtype cache types", async () => {
487
+ const expanded = [{
488
+ "@id": "https://example.com/activities/1",
489
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
490
+ "https://www.w3.org/ns/activitystreams#actor": [{ "@id": "https://example.com/actors/alice" }],
491
+ "https://www.w3.org/ns/activitystreams#object": [{ "@id": "https://example.com/objects/1" }]
492
+ }, {
493
+ "@id": "https://example.com/objects/1",
494
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
495
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap://did:key:z6Mkabc/actor" }]
496
+ }];
497
+ deepStrictEqual(await (await Activity.fromJsonLd(expanded, {
498
+ documentLoader: mockDocumentLoader,
499
+ contextLoader: mockDocumentLoader
500
+ })).toJsonLd(), [{
501
+ "@id": "https://example.com/activities/1",
502
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
503
+ "https://www.w3.org/ns/activitystreams#actor": [{ "@id": "https://example.com/actors/alice" }],
504
+ "https://www.w3.org/ns/activitystreams#object": [{ "@id": "https://example.com/objects/1" }]
505
+ }, {
506
+ "@id": "https://example.com/objects/1",
507
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
508
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap+ef61://did:key:z6Mkabc/actor" }]
509
+ }]);
510
+ deepStrictEqual(expanded[0]["@type"], ["https://www.w3.org/ns/activitystreams#Create"]);
511
+ });
512
+ test("fromJsonLd() preserves compact array contexts with portable IRIs", async () => {
513
+ deepStrictEqual(await (await Note.fromJsonLd([{
514
+ "@context": "https://www.w3.org/ns/activitystreams",
515
+ type: "Note",
516
+ id: "ap://did:key:z6Mkabc/objects/1"
517
+ }], {
518
+ documentLoader: mockDocumentLoader,
519
+ contextLoader: mockDocumentLoader
520
+ })).toJsonLd(), [{
521
+ "@context": "https://www.w3.org/ns/activitystreams",
522
+ type: "Note",
523
+ id: "ap+ef61://did:key:z6Mkabc/objects/1"
524
+ }]);
525
+ });
526
+ test("fromJsonLd() preserves compact single-item arrays with portable IRIs", async () => {
527
+ const createJson = {
528
+ "@context": "https://www.w3.org/ns/activitystreams",
529
+ type: "Create",
530
+ actor: ["ap://did:key:z6Mkabc/actor"],
531
+ object: "https://example.com/objects/1"
532
+ };
533
+ deepStrictEqual(await (await Create.fromJsonLd(createJson, {
534
+ documentLoader: mockDocumentLoader,
535
+ contextLoader: mockDocumentLoader
536
+ })).toJsonLd(), {
537
+ "@context": "https://www.w3.org/ns/activitystreams",
538
+ type: "Create",
539
+ actor: ["ap+ef61://did:key:z6Mkabc/actor"],
540
+ object: "https://example.com/objects/1"
541
+ });
542
+ deepStrictEqual(createJson.actor, ["ap://did:key:z6Mkabc/actor"]);
543
+ });
544
+ test("fromJsonLd() preserves compact multi-node arrays with portable IRIs", async () => {
545
+ deepStrictEqual(await (await Activity.fromJsonLd([{
546
+ "@context": "https://www.w3.org/ns/activitystreams",
547
+ type: "Create",
548
+ id: "https://example.com/activities/1",
549
+ actor: "https://example.com/actors/alice",
550
+ object: "https://example.com/objects/1"
551
+ }, {
552
+ "@context": "https://www.w3.org/ns/activitystreams",
553
+ type: "Note",
554
+ id: "https://example.com/objects/1",
555
+ attributedTo: "ap://did:key:z6Mkabc/actor"
556
+ }], {
557
+ documentLoader: mockDocumentLoader,
558
+ contextLoader: mockDocumentLoader
559
+ })).toJsonLd(), [{
560
+ "@context": "https://www.w3.org/ns/activitystreams",
561
+ type: "Create",
562
+ id: "https://example.com/activities/1",
563
+ actor: "https://example.com/actors/alice",
564
+ object: "https://example.com/objects/1"
565
+ }, {
566
+ "@context": "https://www.w3.org/ns/activitystreams",
567
+ type: "Note",
568
+ id: "https://example.com/objects/1",
569
+ attributedTo: "ap+ef61://did:key:z6Mkabc/actor"
570
+ }]);
571
+ });
572
+ test("fromJsonLd() preserves nested unmapped terms with portable IRIs", async () => {
573
+ deepStrictEqual(await (await Note.fromJsonLd({
574
+ "@context": "https://www.w3.org/ns/activitystreams",
575
+ type: "Note",
576
+ id: "ap://did:key:z6Mkabc/objects/1",
577
+ attachment: {
578
+ type: "Object",
579
+ name: "Attachment with an unmapped extension.",
580
+ extra: "This nested unmapped property should stay cached."
581
+ }
582
+ }, {
583
+ documentLoader: mockDocumentLoader,
584
+ contextLoader: mockDocumentLoader
585
+ })).toJsonLd(), {
586
+ "@context": "https://www.w3.org/ns/activitystreams",
587
+ type: "Note",
588
+ id: "ap+ef61://did:key:z6Mkabc/objects/1",
589
+ attachment: {
590
+ type: "Object",
591
+ name: "Attachment with an unmapped extension.",
592
+ extra: "This nested unmapped property should stay cached."
593
+ }
594
+ });
595
+ });
596
+ test("fromJsonLd() preserves compact array item extension contexts with portable IRIs", async () => {
597
+ deepStrictEqual(await (await Activity.fromJsonLd([{
598
+ "@context": "https://www.w3.org/ns/activitystreams",
599
+ type: "Create",
600
+ id: "https://example.com/activities/1",
601
+ actor: "https://example.com/actors/alice",
602
+ object: "https://example.com/objects/1"
603
+ }, {
604
+ "@context": ["https://www.w3.org/ns/activitystreams", { extraRef: {
605
+ "@id": "https://example.com/ns#extraRef",
606
+ "@type": "@id"
607
+ } }],
608
+ type: "Note",
609
+ id: "https://example.com/objects/1",
610
+ extraRef: "ap://did:key:z6Mkabc/extra"
611
+ }], {
612
+ documentLoader: mockDocumentLoader,
613
+ contextLoader: mockDocumentLoader
614
+ })).toJsonLd(), [{
615
+ "@context": "https://www.w3.org/ns/activitystreams",
616
+ type: "Create",
617
+ id: "https://example.com/activities/1",
618
+ actor: "https://example.com/actors/alice",
619
+ object: "https://example.com/objects/1"
620
+ }, {
621
+ "@context": ["https://www.w3.org/ns/activitystreams", { extraRef: {
622
+ "@id": "https://example.com/ns#extraRef",
623
+ "@type": "@id"
624
+ } }],
625
+ type: "Note",
626
+ id: "https://example.com/objects/1",
627
+ extraRef: "ap+ef61://did:key:z6Mkabc/extra"
628
+ }]);
629
+ });
630
+ test("fromJsonLd() formats portable IRIs in JSON-LD containers", async () => {
631
+ const jsonLd = await (await Note.fromJsonLd({
632
+ "@context": "https://www.w3.org/ns/activitystreams",
633
+ type: "Note",
634
+ id: "https://example.com/notes/1",
635
+ attributedTo: { "@list": ["ap://did:key:z6Mkabc/actor"] },
636
+ to: { "@set": ["ap://did:key:z6Mkabc/followers"] }
637
+ }, {
638
+ documentLoader: mockDocumentLoader,
639
+ contextLoader: mockDocumentLoader
640
+ })).toJsonLd();
641
+ deepStrictEqual(jsonLd.attributedTo, { "@list": ["ap+ef61://did:key:z6Mkabc/actor"] });
642
+ deepStrictEqual(jsonLd.to, "ap+ef61://did:key:z6Mkabc/followers");
643
+ });
644
+ test("fromJsonLd() formats portable IRIs hidden behind JSON-LD aliases", async () => {
645
+ const activity = await Activity.fromJsonLd({
646
+ "@context": ["https://www.w3.org/ns/activitystreams", {
647
+ as: {
648
+ "@id": "https://www.w3.org/ns/activitystreams#",
649
+ "@prefix": true
650
+ },
651
+ extra: "https://example.com/ns#extra",
652
+ extraRef: {
653
+ "@id": "https://example.com/ns#extraRef",
654
+ "@type": "@id"
655
+ },
656
+ actorRef: {
657
+ "@id": "as:actor",
658
+ "@type": "@id"
659
+ },
660
+ targetRef: {
661
+ "@id": "as:target",
662
+ "@type": "@id"
663
+ }
664
+ }],
665
+ type: "Create",
666
+ actorRef: "ap://did:key:z6Mkabc/actor",
667
+ object: "https://example.com/objects/1",
668
+ targetRef: "ap://did:key:z6Mkabc/target",
669
+ extra: "This extension property should stay cached.",
670
+ extraRef: "ap://did:key:z6Mkabc/extra"
671
+ }, {
672
+ documentLoader: mockDocumentLoader,
673
+ contextLoader: mockDocumentLoader
674
+ });
675
+ deepStrictEqual(activity.actorId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
676
+ deepStrictEqual(activity.targetId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/target"));
677
+ const jsonLd = await activity.toJsonLd({ contextLoader: mockDocumentLoader });
678
+ deepStrictEqual(jsonLd.actor, "ap+ef61://did:key:z6Mkabc/actor");
679
+ deepStrictEqual("actorRef" in jsonLd, false);
680
+ deepStrictEqual(jsonLd.target, "ap+ef61://did:key:z6Mkabc/target");
681
+ deepStrictEqual("targetRef" in jsonLd, false);
682
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
683
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
684
+ });
685
+ test("fromJsonLd() preserves portable IRIs in @id extension terms", async () => {
686
+ deepStrictEqual((await (await Note.fromJsonLd({
687
+ "@context": ["https://www.w3.org/ns/activitystreams", { extraRef: {
688
+ "@id": "https://example.com/ns#extraRef",
689
+ "@type": "@id"
690
+ } }],
691
+ type: "Note",
692
+ id: "https://example.com/notes/1",
693
+ extraRef: "ap://did:key:z6Mkabc/extra"
694
+ }, {
695
+ documentLoader: mockDocumentLoader,
696
+ contextLoader: mockDocumentLoader
697
+ })).toJsonLd({ contextLoader: mockDocumentLoader })).extraRef, "ap+ef61://did:key:z6Mkabc/extra");
698
+ });
699
+ test("fromJsonLd() ignores malformed portable IRIs in extension cache terms", async () => {
700
+ const noteJson = {
701
+ "@context": ["https://www.w3.org/ns/activitystreams", { extraRef: {
702
+ "@id": "https://example.com/ns#extraRef",
703
+ "@type": "@id"
704
+ } }],
705
+ type: "Note",
706
+ id: "https://example.com/notes/1",
707
+ extraRef: "ap://example.com/not-portable"
708
+ };
709
+ deepStrictEqual(await (await Note.fromJsonLd(noteJson, {
710
+ documentLoader: mockDocumentLoader,
711
+ contextLoader: mockDocumentLoader
712
+ })).toJsonLd(), noteJson);
713
+ });
714
+ test("fromJsonLd() preserves portable IRIs in @id typed terms", async () => {
715
+ const jsonLd = await (await Note.fromJsonLd({
716
+ "@context": ["https://www.w3.org/ns/activitystreams", {
717
+ "@vocab": "https://example.com/ns#",
718
+ extraRef: { "@type": "@id" }
719
+ }],
720
+ type: "Note",
721
+ id: "https://example.com/notes/1",
722
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
723
+ extraRef: "ap://did:key:z6Mkabc/extra"
724
+ }, {
725
+ documentLoader: mockDocumentLoader,
726
+ contextLoader: mockDocumentLoader
727
+ })).toJsonLd({ contextLoader: mockDocumentLoader });
728
+ deepStrictEqual(jsonLd.content, "This text mentions ap://did:key:z6Mkabc/text.");
729
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
730
+ });
731
+ test("fromJsonLd() preserves portable IRIs hidden behind remote contexts", async () => {
732
+ const contextUrl = "https://example.com/contexts/portable-iris";
733
+ const contextLoader = async (resource, options) => {
734
+ if (resource === contextUrl) return {
735
+ contextUrl: null,
736
+ documentUrl: resource,
737
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
738
+ "@vocab": "https://example.com/ns#",
739
+ extra: "https://example.com/ns#extra",
740
+ extraRef: { "@type": "@id" }
741
+ }] }
742
+ };
743
+ return await mockDocumentLoader(resource, options);
744
+ };
745
+ const jsonLd = await (await Note.fromJsonLd({
746
+ "@context": contextUrl,
747
+ type: "Note",
748
+ id: "https://example.com/notes/1",
749
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
750
+ extra: "This extension property should stay cached.",
751
+ extraRef: "ap://did:key:z6Mkabc/extra"
752
+ }, {
753
+ documentLoader: mockDocumentLoader,
754
+ contextLoader
755
+ })).toJsonLd({ contextLoader });
756
+ deepStrictEqual(jsonLd.content, "This text mentions ap://did:key:z6Mkabc/text.");
757
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
758
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
759
+ });
760
+ test("fromJsonLd() formats portable IRIs hidden behind nested remote contexts", async () => {
761
+ const rootContextUrl = "https://example.com/contexts/nested-portable-iris";
762
+ const nestedContextUrl = "https://example.com/contexts/nested-portable-ref";
763
+ const contextLoader = async (resource, options) => {
764
+ if (resource === rootContextUrl) return {
765
+ contextUrl: null,
766
+ documentUrl: resource,
767
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
768
+ "@vocab": "https://example.com/ns#",
769
+ extraContainer: "https://example.com/ns#extraContainer"
770
+ }] }
771
+ };
772
+ if (resource === nestedContextUrl) return {
773
+ contextUrl: null,
774
+ documentUrl: resource,
775
+ document: { "@context": {
776
+ "@vocab": "https://example.com/ns#",
777
+ extra: "https://example.com/ns#extra",
778
+ extraRef: { "@type": "@id" }
779
+ } }
780
+ };
781
+ return await mockDocumentLoader(resource, options);
782
+ };
783
+ deepStrictEqual((await (await Note.fromJsonLd({
784
+ "@context": rootContextUrl,
785
+ type: "Note",
786
+ id: "https://example.com/notes/1",
787
+ extraContainer: {
788
+ "@context": nestedContextUrl,
789
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
790
+ extra: "This nested extension object should stay cached.",
791
+ extraRef: "ap://did:key:z6Mkabc/extra"
792
+ }
793
+ }, {
794
+ documentLoader: mockDocumentLoader,
795
+ contextLoader
796
+ })).toJsonLd({ contextLoader })).extraContainer, {
797
+ "@context": nestedContextUrl,
798
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
799
+ extra: "This nested extension object should stay cached.",
800
+ extraRef: { id: "ap+ef61://did:key:z6Mkabc/extra" }
801
+ });
802
+ });
803
+ test("fromJsonLd() formats portable IRIs in sibling remote-context objects", async () => {
804
+ const rootContextUrl = "https://example.com/contexts/sibling-containers";
805
+ const nestedContextUrl = "https://example.com/contexts/sibling-extra-ref";
806
+ const contextLoader = async (resource, options) => {
807
+ if (resource === rootContextUrl) return {
808
+ contextUrl: null,
809
+ documentUrl: resource,
810
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
811
+ "@vocab": "https://example.com/ns#",
812
+ firstExtraContainer: "https://example.com/ns#firstExtraContainer",
813
+ secondExtraContainer: "https://example.com/ns#secondExtraContainer"
814
+ }] }
815
+ };
816
+ if (resource === nestedContextUrl) return {
817
+ contextUrl: null,
818
+ documentUrl: resource,
819
+ document: { "@context": {
820
+ "@vocab": "https://example.com/ns#",
821
+ extraRef: { "@type": "@id" }
822
+ } }
823
+ };
824
+ return await mockDocumentLoader(resource, options);
825
+ };
826
+ const jsonLd = await (await Note.fromJsonLd({
827
+ "@context": rootContextUrl,
828
+ type: "Note",
829
+ id: "https://example.com/notes/1",
830
+ firstExtraContainer: {
831
+ "@context": nestedContextUrl,
832
+ content: "No portable IRI here."
833
+ },
834
+ secondExtraContainer: {
835
+ "@context": nestedContextUrl,
836
+ extraRef: "ap://did:key:z6Mkabc/second"
837
+ }
838
+ }, {
839
+ documentLoader: mockDocumentLoader,
840
+ contextLoader
841
+ })).toJsonLd({ contextLoader });
842
+ deepStrictEqual(jsonLd.firstExtraContainer, {
843
+ "@context": nestedContextUrl,
844
+ content: "No portable IRI here."
845
+ });
846
+ deepStrictEqual(jsonLd.secondExtraContainer, {
847
+ "@context": nestedContextUrl,
848
+ extraRef: { id: "ap+ef61://did:key:z6Mkabc/second" }
849
+ });
850
+ });
851
+ test("fromJsonLd() batches unmapped portable IRI term checks", async () => {
852
+ const contextUrl = "https://example.com/contexts/batched-portable-aliases";
853
+ let contextLoads = 0;
854
+ const contextLoader = async (resource, options) => {
855
+ if (resource === contextUrl) {
856
+ contextLoads++;
857
+ return {
858
+ contextUrl: null,
859
+ documentUrl: resource,
860
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
861
+ as: {
862
+ "@id": "https://www.w3.org/ns/activitystreams#",
863
+ "@prefix": true
864
+ },
865
+ actorRef0: {
866
+ "@id": "as:actor",
867
+ "@type": "@id"
868
+ },
869
+ actorRef1: {
870
+ "@id": "as:actor",
871
+ "@type": "@id"
872
+ },
873
+ targetRef0: {
874
+ "@id": "as:target",
875
+ "@type": "@id"
876
+ },
877
+ targetRef1: {
878
+ "@id": "as:target",
879
+ "@type": "@id"
880
+ },
881
+ objectRef0: {
882
+ "@id": "as:object",
883
+ "@type": "@id"
884
+ },
885
+ objectRef1: {
886
+ "@id": "as:object",
887
+ "@type": "@id"
888
+ }
889
+ }] }
890
+ };
891
+ }
892
+ return await mockDocumentLoader(resource, options);
893
+ };
894
+ await (await Activity.fromJsonLd({
895
+ "@context": contextUrl,
896
+ type: "Create",
897
+ actorRef0: "ap://did:key:z6Mkabc/actor0",
898
+ actorRef1: "ap://did:key:z6Mkabc/actor1",
899
+ targetRef0: "ap://did:key:z6Mkabc/target0",
900
+ targetRef1: "ap://did:key:z6Mkabc/target1",
901
+ objectRef0: "https://example.com/objects/0",
902
+ objectRef1: "https://example.com/objects/1"
903
+ }, {
904
+ documentLoader: mockDocumentLoader,
905
+ contextLoader
906
+ })).toJsonLd({ contextLoader });
907
+ ok(contextLoads <= 5);
908
+ });
909
+ test("fromJsonLd() falls back when portable IRI cache merge fails", async () => {
910
+ const contextUrl = "https://example.com/contexts/failing-cache-merge";
911
+ let contextLoads = 0;
912
+ const contextLoader = async (resource, options) => {
913
+ if (resource === contextUrl) {
914
+ contextLoads++;
915
+ if (contextLoads > 1) throw new Error("merge context unavailable");
916
+ return {
917
+ contextUrl: null,
918
+ documentUrl: resource,
919
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
920
+ "@vocab": "https://example.com/ns#",
921
+ extraRef: { "@type": "@id" }
922
+ }] }
923
+ };
924
+ }
925
+ return await mockDocumentLoader(resource, options);
926
+ };
927
+ const jsonLd = await (await Note.fromJsonLd({
928
+ "@context": contextUrl,
929
+ type: "Note",
930
+ id: "ap://did:key:z6Mkabc/objects/1",
931
+ extraRef: "ap://did:key:z6Mkabc/extra"
932
+ }, {
933
+ documentLoader: mockDocumentLoader,
934
+ contextLoader
935
+ })).toJsonLd({ contextLoader: mockDocumentLoader });
936
+ deepStrictEqual(jsonLd.type, "Note");
937
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
938
+ });
939
+ test("fromJsonLd() formats portable IRIs in scalar URL values", async () => {
940
+ const note = await Note.fromJsonLd({
941
+ "@context": [
942
+ "https://www.w3.org/ns/activitystreams",
943
+ "https://gotosocial.org/ns",
944
+ { quoteUrl: "as:quoteUrl" }
945
+ ],
946
+ type: "Note",
947
+ id: "https://example.com/notes/1",
948
+ quoteUrl: "ap://did:key:z6Mkabc/objects/1"
949
+ }, {
950
+ documentLoader: mockDocumentLoader,
951
+ contextLoader: mockDocumentLoader
952
+ });
953
+ deepStrictEqual(note.quoteUrl, new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"));
954
+ deepStrictEqual((await note.toJsonLd({ contextLoader: mockDocumentLoader })).quoteUrl, "ap+ef61://did:key:z6Mkabc/objects/1");
955
+ });
956
+ test("fromJsonLd() formats portable IRIs in URL value lists", async () => {
957
+ const note = await Note.fromJsonLd({
958
+ "@context": [
959
+ "https://www.w3.org/ns/activitystreams",
960
+ "https://gotosocial.org/ns",
961
+ { quoteUrl: "as:quoteUrl" }
962
+ ],
963
+ type: "Note",
964
+ id: "https://example.com/notes/1",
965
+ quoteUrl: { "@list": [{ "@value": "ap://did:key:z6Mkabc/objects/1" }] }
966
+ }, {
967
+ documentLoader: mockDocumentLoader,
968
+ contextLoader: mockDocumentLoader
969
+ });
970
+ deepStrictEqual(note.quoteUrl, new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"));
971
+ deepStrictEqual((await note.toJsonLd({ contextLoader: mockDocumentLoader })).quoteUrl, { "@list": ["ap+ef61://did:key:z6Mkabc/objects/1"] });
972
+ });
305
973
  test({
306
974
  name: "Activity.getObject()",
307
975
  permissions: {
@@ -357,6 +1025,34 @@ test({
357
1025
  });
358
1026
  }
359
1027
  });
1028
+ test("Activity.getObject() fetches canonical portable IRIs", async () => {
1029
+ const fetchedUrls = [];
1030
+ const documentLoader = async (url) => {
1031
+ fetchedUrls.push(url);
1032
+ return {
1033
+ contextUrl: null,
1034
+ documentUrl: url,
1035
+ document: {
1036
+ "@context": "https://www.w3.org/ns/activitystreams",
1037
+ id: url,
1038
+ type: "Note",
1039
+ content: "Fetched portable object"
1040
+ }
1041
+ };
1042
+ };
1043
+ assertInstanceOf(await (await Activity.fromJsonLd({
1044
+ "@context": "https://www.w3.org/ns/activitystreams",
1045
+ type: "Create",
1046
+ object: "ap://did:key:z6Mkabc/objects/1"
1047
+ }, {
1048
+ documentLoader,
1049
+ contextLoader: mockDocumentLoader
1050
+ })).getObject({
1051
+ documentLoader,
1052
+ contextLoader: mockDocumentLoader
1053
+ }), Note);
1054
+ deepStrictEqual(fetchedUrls, ["ap+ef61://did:key:z6Mkabc/objects/1"]);
1055
+ });
360
1056
  test({
361
1057
  name: "Activity.getObjects()",
362
1058
  permissions: {
@@ -465,6 +1161,7 @@ test("Person.toJsonLd()", async () => {
465
1161
  "https://www.w3.org/ns/did/v1",
466
1162
  "https://w3id.org/security/multikey/v1",
467
1163
  "https://gotosocial.org/ns",
1164
+ "https://w3id.org/fep/7aa9",
468
1165
  {
469
1166
  PropertyValue: "schema:PropertyValue",
470
1167
  alsoKnownAs: {
@@ -480,6 +1177,10 @@ test("Person.toJsonLd()", async () => {
480
1177
  "@id": "toot:featured",
481
1178
  "@type": "@id"
482
1179
  },
1180
+ featuredCollections: {
1181
+ "@id": "https://w3id.org/fep/7aa9#featuredCollections",
1182
+ "@type": "@id"
1183
+ },
483
1184
  featuredTags: {
484
1185
  "@id": "toot:featuredTags",
485
1186
  "@type": "@id"
@@ -926,6 +1627,187 @@ test("InteractionPolicy.canQuote", async () => {
926
1627
  contextLoader: mockDocumentLoader
927
1628
  })).toJsonLd({ contextLoader: mockDocumentLoader }), expected);
928
1629
  });
1630
+ test("InteractionPolicy.canFeature", async () => {
1631
+ const person = new Person({
1632
+ id: new URL("https://example.com/users/alice"),
1633
+ featuredCollections: new URL("https://example.com/users/alice/featured_collections"),
1634
+ interactionPolicy: new InteractionPolicy({ canFeature: new InteractionRule({ automaticApproval: new URL("https://www.w3.org/ns/activitystreams#Public") }) })
1635
+ });
1636
+ const expected = {
1637
+ "@context": [
1638
+ "https://www.w3.org/ns/activitystreams",
1639
+ "https://w3id.org/security/v1",
1640
+ "https://w3id.org/security/data-integrity/v1",
1641
+ "https://www.w3.org/ns/did/v1",
1642
+ "https://w3id.org/security/multikey/v1",
1643
+ "https://gotosocial.org/ns",
1644
+ "https://w3id.org/fep/7aa9",
1645
+ {
1646
+ Emoji: "toot:Emoji",
1647
+ PropertyValue: "schema:PropertyValue",
1648
+ _misskey_followedMessage: "misskey:_misskey_followedMessage",
1649
+ alsoKnownAs: {
1650
+ "@id": "as:alsoKnownAs",
1651
+ "@type": "@id"
1652
+ },
1653
+ discoverable: "toot:discoverable",
1654
+ featured: {
1655
+ "@id": "toot:featured",
1656
+ "@type": "@id"
1657
+ },
1658
+ featuredCollections: {
1659
+ "@id": "https://w3id.org/fep/7aa9#featuredCollections",
1660
+ "@type": "@id"
1661
+ },
1662
+ featuredTags: {
1663
+ "@id": "toot:featuredTags",
1664
+ "@type": "@id"
1665
+ },
1666
+ indexable: "toot:indexable",
1667
+ isCat: "misskey:isCat",
1668
+ manuallyApprovesFollowers: "as:manuallyApprovesFollowers",
1669
+ memorial: "toot:memorial",
1670
+ misskey: "https://misskey-hub.net/ns#",
1671
+ movedTo: {
1672
+ "@id": "as:movedTo",
1673
+ "@type": "@id"
1674
+ },
1675
+ schema: "http://schema.org#",
1676
+ suspended: "toot:suspended",
1677
+ toot: "http://joinmastodon.org/ns#",
1678
+ value: "schema:value"
1679
+ }
1680
+ ],
1681
+ type: "Person",
1682
+ id: "https://example.com/users/alice",
1683
+ featuredCollections: "https://example.com/users/alice/featured_collections",
1684
+ interactionPolicy: { canFeature: { automaticApproval: "as:Public" } }
1685
+ };
1686
+ deepStrictEqual(await person.toJsonLd({ contextLoader: mockDocumentLoader }), expected);
1687
+ const loaded = await Person.fromJsonLd(expected, {
1688
+ documentLoader: mockDocumentLoader,
1689
+ contextLoader: mockDocumentLoader
1690
+ });
1691
+ deepStrictEqual(loaded.featuredCollectionsId, new URL("https://example.com/users/alice/featured_collections"));
1692
+ assertInstanceOf(loaded.interactionPolicy, InteractionPolicy);
1693
+ assertInstanceOf(loaded.interactionPolicy.canFeature, InteractionRule);
1694
+ deepStrictEqual(loaded.interactionPolicy.canFeature.automaticApproval, new URL("https://www.w3.org/ns/activitystreams#Public"));
1695
+ deepStrictEqual(await loaded.toJsonLd({ contextLoader: mockDocumentLoader }), expected);
1696
+ deepStrictEqual((await Person.fromJsonLd({
1697
+ "@context": [
1698
+ "https://www.w3.org/ns/activitystreams",
1699
+ "https://gotosocial.org/ns",
1700
+ "https://w3id.org/fep/7aa9",
1701
+ { featuredCollections: {
1702
+ "@id": "https://w3id.org/fep/7aa9#featuredCollections",
1703
+ "@type": "@id"
1704
+ } }
1705
+ ],
1706
+ type: "Person",
1707
+ id: "https://example.com/users/alice",
1708
+ featuredCollections: "https://example.com/users/alice/featured_collections"
1709
+ }, {
1710
+ documentLoader: mockDocumentLoader,
1711
+ contextLoader: mockDocumentLoader
1712
+ })).featuredCollectionsId, new URL("https://example.com/users/alice/featured_collections"));
1713
+ });
1714
+ test("FeaturedCollection.toJsonLd()", async () => {
1715
+ const collection = new FeaturedCollection({
1716
+ id: new URL("https://example.com/users/alice/featured/1"),
1717
+ name: "Cute cats",
1718
+ attribution: new URL("https://example.com/users/alice"),
1719
+ topic: new Hashtag({ name: "#cats" }),
1720
+ discoverable: false,
1721
+ totalItems: 1
1722
+ });
1723
+ const expected = {
1724
+ "@context": FEATURED_COLLECTION_CONTEXT,
1725
+ type: "FeaturedCollection",
1726
+ id: "https://example.com/users/alice/featured/1",
1727
+ attributedTo: "https://example.com/users/alice",
1728
+ name: "Cute cats",
1729
+ totalItems: 1,
1730
+ topic: {
1731
+ type: "Hashtag",
1732
+ name: "#cats"
1733
+ },
1734
+ discoverable: false
1735
+ };
1736
+ deepStrictEqual(await collection.toJsonLd({ contextLoader: mockDocumentLoader }), expected);
1737
+ const loaded = await FeaturedCollection.fromJsonLd(expected, {
1738
+ documentLoader: mockDocumentLoader,
1739
+ contextLoader: mockDocumentLoader
1740
+ });
1741
+ assertInstanceOf(loaded, FeaturedCollection);
1742
+ deepStrictEqual(loaded.name, "Cute cats");
1743
+ deepStrictEqual(loaded.attributionId, new URL("https://example.com/users/alice"));
1744
+ assertInstanceOf(loaded.topic, Hashtag);
1745
+ deepStrictEqual(loaded.topic.name, "#cats");
1746
+ deepStrictEqual(loaded.discoverable, false);
1747
+ deepStrictEqual(loaded.totalItems, 1);
1748
+ deepStrictEqual(await loaded.toJsonLd({ contextLoader: mockDocumentLoader }), expected);
1749
+ });
1750
+ test("FeaturedItem.toJsonLd()", async () => {
1751
+ const item = new FeaturedItem({
1752
+ id: new URL("https://example.com/users/alice/featured/1/items/1"),
1753
+ featuredObject: new URL("https://example.com/users/bob"),
1754
+ featureAuthorization: new URL("https://example.com/users/bob/stamps/1")
1755
+ });
1756
+ const expected = {
1757
+ "@context": FEATURE_CONTEXT,
1758
+ type: "FeaturedItem",
1759
+ id: "https://example.com/users/alice/featured/1/items/1",
1760
+ featuredObject: "https://example.com/users/bob",
1761
+ featureAuthorization: "https://example.com/users/bob/stamps/1"
1762
+ };
1763
+ deepStrictEqual(await item.toJsonLd({ contextLoader: mockDocumentLoader }), expected);
1764
+ const loaded = await FeaturedItem.fromJsonLd(expected, {
1765
+ documentLoader: mockDocumentLoader,
1766
+ contextLoader: mockDocumentLoader
1767
+ });
1768
+ assertInstanceOf(loaded, FeaturedItem);
1769
+ deepStrictEqual(loaded.featuredObjectId, new URL("https://example.com/users/bob"));
1770
+ deepStrictEqual(loaded.featureAuthorizationId, new URL("https://example.com/users/bob/stamps/1"));
1771
+ });
1772
+ test("FeatureAuthorization.fromJsonLd()", async () => {
1773
+ const jsonLd = {
1774
+ "@context": FEATURE_CONTEXT,
1775
+ type: "FeatureAuthorization",
1776
+ id: "https://example.com/users/bob/stamps/1",
1777
+ interactingObject: "https://example.com/users/alice/featured/1",
1778
+ interactionTarget: "https://example.com/users/bob"
1779
+ };
1780
+ const authorization = await FeatureAuthorization.fromJsonLd(jsonLd, {
1781
+ documentLoader: mockDocumentLoader,
1782
+ contextLoader: mockDocumentLoader
1783
+ });
1784
+ assertInstanceOf(authorization, FeatureAuthorization);
1785
+ deepStrictEqual(authorization.interactingObjectId, new URL("https://example.com/users/alice/featured/1"));
1786
+ deepStrictEqual(authorization.interactionTargetId, new URL("https://example.com/users/bob"));
1787
+ deepStrictEqual(await authorization.toJsonLd({ contextLoader: mockDocumentLoader }), jsonLd);
1788
+ });
1789
+ test("FeatureRequest.toJsonLd()", async () => {
1790
+ const request = new FeatureRequest({
1791
+ id: new URL("https://example.com/users/alice/featured/1/requests/1"),
1792
+ object: new URL("https://example.com/users/bob"),
1793
+ instrument: new URL("https://example.com/users/alice/featured/1")
1794
+ });
1795
+ const expected = {
1796
+ "@context": ["https://w3id.org/identity/v1", ...FEATURE_CONTEXT],
1797
+ type: "FeatureRequest",
1798
+ id: "https://example.com/users/alice/featured/1/requests/1",
1799
+ object: "https://example.com/users/bob",
1800
+ instrument: "https://example.com/users/alice/featured/1"
1801
+ };
1802
+ deepStrictEqual(await request.toJsonLd({ contextLoader: mockDocumentLoader }), expected);
1803
+ const loaded = await FeatureRequest.fromJsonLd(expected, {
1804
+ documentLoader: mockDocumentLoader,
1805
+ contextLoader: mockDocumentLoader
1806
+ });
1807
+ assertInstanceOf(loaded, FeatureRequest);
1808
+ deepStrictEqual(loaded.objectId, new URL("https://example.com/users/bob"));
1809
+ deepStrictEqual(loaded.instrumentId, new URL("https://example.com/users/alice/featured/1"));
1810
+ });
929
1811
  test("QuoteAuthorization.fromJsonLd()", async () => {
930
1812
  const jsonLd = {
931
1813
  "@context": [
@@ -1630,6 +2512,26 @@ test("FEP-fe34: crossOrigin trust behavior", async () => {
1630
2512
  deepStrictEqual(result?.id, new URL("https://malicious.com/fake-note"));
1631
2513
  deepStrictEqual(result?.content, "This is a spoofed note");
1632
2514
  });
2515
+ test("FEP-fe34: id-less owners honor crossOrigin trust", async () => {
2516
+ const result = await (await Create.fromJsonLd({
2517
+ "@context": "https://www.w3.org/ns/activitystreams",
2518
+ "@type": "Create",
2519
+ "actor": "https://example.com/actor",
2520
+ "object": {
2521
+ "@type": "Note",
2522
+ "@id": "https://different-origin.com/note",
2523
+ "content": "Embedded note"
2524
+ }
2525
+ })).getObject({
2526
+ crossOrigin: "trust",
2527
+ documentLoader: async (url) => {
2528
+ throw new Error(`Unexpected fetch: ${url}`);
2529
+ }
2530
+ });
2531
+ assertInstanceOf(result, Note);
2532
+ deepStrictEqual(result.id, new URL("https://different-origin.com/note"));
2533
+ deepStrictEqual(result.content, "Embedded note");
2534
+ });
1633
2535
  test("FEP-fe34: Same origin objects are trusted", async () => {
1634
2536
  const sameOriginDocumentLoader = async (url) => {
1635
2537
  if (url === "https://example.com/note") return {
@@ -1725,6 +2627,35 @@ test("FEP-fe34: Constructor vs JSON-LD parsing trust difference", async () => {
1725
2627
  };
1726
2628
  deepStrictEqual((await jsonLdCreate.getObject({ documentLoader }))?.content, "Fetched from origin");
1727
2629
  });
2630
+ test("FEP-fe34: Portable DID authorities are cross-origin boundaries", async () => {
2631
+ const create = await Create.fromJsonLd({
2632
+ "@context": "https://www.w3.org/ns/activitystreams",
2633
+ "@type": "Create",
2634
+ "@id": "ap://did:key:z6MkOwner/create",
2635
+ "actor": "ap://did:key:z6MkOwner/actor",
2636
+ "object": {
2637
+ "@type": "Note",
2638
+ "@id": "ap://did:key:z6MkOther/note",
2639
+ "content": "Embedded portable note"
2640
+ }
2641
+ });
2642
+ const documentLoader = async (url) => {
2643
+ if (url === "ap+ef61://did:key:z6MkOther/note") return {
2644
+ documentUrl: url,
2645
+ contextUrl: null,
2646
+ document: {
2647
+ "@context": "https://www.w3.org/ns/activitystreams",
2648
+ "@type": "Note",
2649
+ "@id": "ap://did:key:z6MkOther/note",
2650
+ "content": "Fetched portable note"
2651
+ }
2652
+ };
2653
+ throw new Error("Document not found");
2654
+ };
2655
+ const result = await create.getObject({ documentLoader });
2656
+ assertInstanceOf(result, Note);
2657
+ deepStrictEqual(result.content, "Fetched portable note");
2658
+ });
1728
2659
  test("FEP-fe34: Array properties respect cross-origin policy", async () => {
1729
2660
  const crossOriginDocumentLoader = async (url) => {
1730
2661
  if (url === "https://different-origin.com/note1") return {
@@ -1798,6 +2729,60 @@ test("FEP-fe34: Array properties with crossOrigin trust option", async () => {
1798
2729
  deepStrictEqual(items[0].content, "Fake note 1");
1799
2730
  deepStrictEqual(items[1].content, "Legitimate note 2");
1800
2731
  });
2732
+ test("FEP-fe34: id-less arrays honor crossOrigin trust", async () => {
2733
+ const collection = await Collection.fromJsonLd({
2734
+ "@context": "https://www.w3.org/ns/activitystreams",
2735
+ "@type": "Collection",
2736
+ "items": [{
2737
+ "@type": "Note",
2738
+ "@id": "https://different-origin.com/note1",
2739
+ "content": "Embedded note 1"
2740
+ }, {
2741
+ "@type": "Note",
2742
+ "@id": "https://different-origin.com/note2",
2743
+ "content": "Embedded note 2"
2744
+ }]
2745
+ });
2746
+ const items = [];
2747
+ for await (const item of collection.getItems({
2748
+ crossOrigin: "trust",
2749
+ documentLoader: async (url) => {
2750
+ throw new Error(`Unexpected fetch: ${url}`);
2751
+ }
2752
+ })) items.push(item);
2753
+ deepStrictEqual(items.length, 2);
2754
+ assertInstanceOf(items[0], Note);
2755
+ assertInstanceOf(items[1], Note);
2756
+ deepStrictEqual(items[0].content, "Embedded note 1");
2757
+ deepStrictEqual(items[1].content, "Embedded note 2");
2758
+ });
2759
+ test("FEP-fe34: Array properties track trust per item", async () => {
2760
+ const collection = new Collection({
2761
+ id: new URL("https://example.com/collection"),
2762
+ items: [new Note({
2763
+ id: new URL("https://malicious.com/fake-note1"),
2764
+ content: "Trusted constructor note 1"
2765
+ }), new URL("https://different-origin.com/note2")]
2766
+ });
2767
+ const documentLoader = async (url) => {
2768
+ if (url === "https://different-origin.com/note2") return {
2769
+ documentUrl: url,
2770
+ contextUrl: null,
2771
+ document: {
2772
+ "@context": "https://www.w3.org/ns/activitystreams",
2773
+ "@type": "Note",
2774
+ "@id": "https://malicious.com/fake-note2",
2775
+ "content": "Untrusted fetched note 2"
2776
+ }
2777
+ };
2778
+ throw new Error("Document not found");
2779
+ };
2780
+ const items = [];
2781
+ for await (const item of collection.getItems({ documentLoader })) items.push(item);
2782
+ deepStrictEqual(items.length, 1);
2783
+ assertInstanceOf(items[0], Note);
2784
+ deepStrictEqual(items[0].content, "Trusted constructor note 1");
2785
+ });
1801
2786
  test("FEP-fe34: Embedded objects in arrays from JSON-LD respect cross-origin policy", async () => {
1802
2787
  const collectionDocumentLoader = async (url) => {
1803
2788
  if (url === "https://example.com/collection") return {