@fedify/vocab 2.4.0-dev.1504 → 2.4.0-dev.1528

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 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-CQX0Zq_l.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";
@@ -302,6 +302,662 @@ test("Activity.fromJsonLd()", async () => {
302
302
  deepStrictEqual(proofs[0].proofValue, decodeMultibase("z3sXaxjKs4M3BRicwWA9peyNPJvJqxtGsDmpt1jjoHCjgeUf71TRFz56osPSfDErszyLp5Ks1EhYSgpDaNM977Rg2"));
303
303
  deepStrictEqual(proofs[0].created, Temporal.Instant.from("2023-02-24T23:36:38Z"));
304
304
  });
305
+ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
306
+ const did = "did:key:z6Mkabc";
307
+ const portableActor = `ap://${did}/actor?gateways=https%3A%2F%2Fserver.example`;
308
+ const portableObject = `ap://${did}/objects/1?gateways=https%3A%2F%2Fserver.example`;
309
+ const uppercasePortableObject = `AP://${did}/objects/1?gateways=https%3A%2F%2Fserver.example`;
310
+ const portablePage = `ap://${did}/actor/outbox?page=2`;
311
+ const note = await Note.fromJsonLd({
312
+ "@context": "https://www.w3.org/ns/activitystreams",
313
+ type: "Note",
314
+ id: uppercasePortableObject,
315
+ attributedTo: `ap+ef61://${encodeURIComponent(did)}/actor?gateways=https%3A%2F%2Fserver.example`
316
+ }, {
317
+ documentLoader: mockDocumentLoader,
318
+ contextLoader: mockDocumentLoader
319
+ });
320
+ deepStrictEqual(note.id, new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example"));
321
+ deepStrictEqual(note.attributionId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor?gateways=https%3A%2F%2Fserver.example"));
322
+ const noteJson = await note.toJsonLd({ contextLoader: mockDocumentLoader });
323
+ deepStrictEqual(noteJson.type, "Note");
324
+ deepStrictEqual(noteJson.id, "ap+ef61://did:key:z6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example");
325
+ deepStrictEqual(noteJson.attributedTo, "ap+ef61://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fserver.example");
326
+ const activity = await Activity.fromJsonLd({
327
+ "@context": "https://www.w3.org/ns/activitystreams",
328
+ type: "Create",
329
+ actor: portableActor,
330
+ object: portableObject
331
+ }, {
332
+ documentLoader: mockDocumentLoader,
333
+ contextLoader: mockDocumentLoader
334
+ });
335
+ deepStrictEqual(activity.actorId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor?gateways=https%3A%2F%2Fserver.example"));
336
+ const activityJson = await activity.toJsonLd({ contextLoader: mockDocumentLoader });
337
+ deepStrictEqual(activityJson.type, "Create");
338
+ deepStrictEqual(activityJson.actor, "ap+ef61://did:key:z6Mkabc/actor?gateways=https%3A%2F%2Fserver.example");
339
+ deepStrictEqual(activityJson.object, "ap+ef61://did:key:z6Mkabc/objects/1?gateways=https%3A%2F%2Fserver.example");
340
+ const person = await Person.fromJsonLd({
341
+ "@context": "https://www.w3.org/ns/activitystreams",
342
+ type: "Person",
343
+ inbox: `ap+ef61://${did}/actor/inbox`,
344
+ outbox: `ap+ef61://${did}/actor/outbox`
345
+ }, {
346
+ documentLoader: mockDocumentLoader,
347
+ contextLoader: mockDocumentLoader
348
+ });
349
+ deepStrictEqual(person.inboxId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/inbox"));
350
+ deepStrictEqual(person.outboxId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox"));
351
+ const personJson = await person.toJsonLd({ contextLoader: mockDocumentLoader });
352
+ deepStrictEqual(personJson.type, "Person");
353
+ deepStrictEqual(personJson.inbox, "ap+ef61://did:key:z6Mkabc/actor/inbox");
354
+ deepStrictEqual(personJson.outbox, "ap+ef61://did:key:z6Mkabc/actor/outbox");
355
+ const page = await OrderedCollectionPage.fromJsonLd({
356
+ "@context": "https://www.w3.org/ns/activitystreams",
357
+ type: "OrderedCollectionPage",
358
+ next: portablePage,
359
+ prev: `ap+ef61://${encodeURIComponent(did)}/actor/outbox?page=1`
360
+ }, {
361
+ documentLoader: mockDocumentLoader,
362
+ contextLoader: mockDocumentLoader
363
+ });
364
+ deepStrictEqual(page.nextId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox?page=2"));
365
+ deepStrictEqual(page.prevId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor/outbox?page=1"));
366
+ const pageJson = await page.toJsonLd({ contextLoader: mockDocumentLoader });
367
+ deepStrictEqual(pageJson.type, "OrderedCollectionPage");
368
+ deepStrictEqual(pageJson.next, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=2");
369
+ deepStrictEqual(pageJson.prev, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=1");
370
+ });
371
+ test("fromJsonLd() caches text that mentions portable ActivityPub IRIs", async () => {
372
+ const noteJson = {
373
+ "@context": ["https://www.w3.org/ns/activitystreams", { extra: "https://example.com/ns#extra" }],
374
+ type: "Note",
375
+ id: "https://example.com/notes/1",
376
+ content: "This is text about ap://did:key:z6Mkabc/actor.",
377
+ extra: "This extension property should stay cached."
378
+ };
379
+ deepStrictEqual(await (await Note.fromJsonLd(noteJson, {
380
+ documentLoader: mockDocumentLoader,
381
+ contextLoader: mockDocumentLoader
382
+ })).toJsonLd(), noteJson);
383
+ });
384
+ test("fromJsonLd() preserves extensions with portable ActivityPub IRIs", async () => {
385
+ const jsonLd = await (await Note.fromJsonLd({
386
+ "@context": ["https://www.w3.org/ns/activitystreams", { extra: "https://example.com/ns#extra" }],
387
+ type: "Note",
388
+ id: "ap://did:key:z6Mkabc/objects/1",
389
+ extra: "This extension property should stay cached."
390
+ }, {
391
+ documentLoader: mockDocumentLoader,
392
+ contextLoader: mockDocumentLoader
393
+ })).toJsonLd();
394
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
395
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
396
+ });
397
+ test("fromJsonLd() preserves unmapped terms with portable IRIs", async () => {
398
+ const jsonLd = await (await Note.fromJsonLd({
399
+ "@context": "https://www.w3.org/ns/activitystreams",
400
+ type: "Note",
401
+ id: "ap://did:key:z6Mkabc/objects/1",
402
+ extra: "This unmapped property should stay cached."
403
+ }, {
404
+ documentLoader: mockDocumentLoader,
405
+ contextLoader: mockDocumentLoader
406
+ })).toJsonLd();
407
+ deepStrictEqual(jsonLd.extra, "This unmapped property should stay cached.");
408
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
409
+ });
410
+ test("fromJsonLd() preserves expanded arrays with portable IRIs", async () => {
411
+ const expanded = [{
412
+ "@id": "https://example.com/activities/1",
413
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
414
+ "https://www.w3.org/ns/activitystreams#actor": [{ "@id": "ap://did:key:z6Mkabc/actor" }],
415
+ "https://www.w3.org/ns/activitystreams#object": [{ "@id": "https://example.com/objects/1" }]
416
+ }, {
417
+ "@id": "https://example.com/objects/1",
418
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
419
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "Sibling node should stay cached." }]
420
+ }];
421
+ const activity = await Activity.fromJsonLd(expanded, {
422
+ documentLoader: mockDocumentLoader,
423
+ contextLoader: mockDocumentLoader
424
+ });
425
+ deepStrictEqual(activity.actorId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
426
+ deepStrictEqual(await activity.toJsonLd(), [{
427
+ "@id": "https://example.com/activities/1",
428
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
429
+ "https://www.w3.org/ns/activitystreams#actor": [{ "@id": "ap+ef61://did:key:z6Mkabc/actor" }],
430
+ "https://www.w3.org/ns/activitystreams#object": [{ "@id": "https://example.com/objects/1" }]
431
+ }, {
432
+ "@id": "https://example.com/objects/1",
433
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
434
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "Sibling node should stay cached." }]
435
+ }]);
436
+ deepStrictEqual(expanded[0]["https://www.w3.org/ns/activitystreams#actor"], [{ "@id": "ap://did:key:z6Mkabc/actor" }]);
437
+ });
438
+ test("fromJsonLd() preserves single-node expanded arrays with portable IRIs", async () => {
439
+ const expanded = [{
440
+ "@id": "ap://did:key:z6Mkabc/objects/1",
441
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
442
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap://did:key:z6Mkabc/actor" }],
443
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "Single expanded node should stay cached as an array." }]
444
+ }];
445
+ deepStrictEqual(await (await Note.fromJsonLd(expanded, {
446
+ documentLoader: mockDocumentLoader,
447
+ contextLoader: mockDocumentLoader
448
+ })).toJsonLd(), [{
449
+ "@id": "ap+ef61://did:key:z6Mkabc/objects/1",
450
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
451
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap+ef61://did:key:z6Mkabc/actor" }],
452
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "Single expanded node should stay cached as an array." }]
453
+ }]);
454
+ deepStrictEqual(expanded[0]["@id"], "ap://did:key:z6Mkabc/objects/1");
455
+ });
456
+ test("fromJsonLd() preserves no-context object shape with portable IRIs", async () => {
457
+ const expanded = {
458
+ "@id": "ap://did:key:z6Mkabc/objects/1",
459
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
460
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap://did:key:z6Mkabc/actor" }],
461
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "No-context object shape should stay cached." }]
462
+ };
463
+ deepStrictEqual(await (await Note.fromJsonLd(expanded, {
464
+ documentLoader: mockDocumentLoader,
465
+ contextLoader: mockDocumentLoader
466
+ })).toJsonLd(), {
467
+ "@id": "ap+ef61://did:key:z6Mkabc/objects/1",
468
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
469
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap+ef61://did:key:z6Mkabc/actor" }],
470
+ "https://www.w3.org/ns/activitystreams#content": [{ "@value": "No-context object shape should stay cached." }]
471
+ });
472
+ deepStrictEqual(expanded["@id"], "ap://did:key:z6Mkabc/objects/1");
473
+ });
474
+ test("fromJsonLd() preserves expanded subtype cache types", async () => {
475
+ const expanded = [{
476
+ "@id": "https://example.com/activities/1",
477
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
478
+ "https://www.w3.org/ns/activitystreams#actor": [{ "@id": "https://example.com/actors/alice" }],
479
+ "https://www.w3.org/ns/activitystreams#object": [{ "@id": "https://example.com/objects/1" }]
480
+ }, {
481
+ "@id": "https://example.com/objects/1",
482
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
483
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap://did:key:z6Mkabc/actor" }]
484
+ }];
485
+ deepStrictEqual(await (await Activity.fromJsonLd(expanded, {
486
+ documentLoader: mockDocumentLoader,
487
+ contextLoader: mockDocumentLoader
488
+ })).toJsonLd(), [{
489
+ "@id": "https://example.com/activities/1",
490
+ "@type": ["https://www.w3.org/ns/activitystreams#Create"],
491
+ "https://www.w3.org/ns/activitystreams#actor": [{ "@id": "https://example.com/actors/alice" }],
492
+ "https://www.w3.org/ns/activitystreams#object": [{ "@id": "https://example.com/objects/1" }]
493
+ }, {
494
+ "@id": "https://example.com/objects/1",
495
+ "@type": ["https://www.w3.org/ns/activitystreams#Note"],
496
+ "https://www.w3.org/ns/activitystreams#attributedTo": [{ "@id": "ap+ef61://did:key:z6Mkabc/actor" }]
497
+ }]);
498
+ deepStrictEqual(expanded[0]["@type"], ["https://www.w3.org/ns/activitystreams#Create"]);
499
+ });
500
+ test("fromJsonLd() preserves compact array contexts with portable IRIs", async () => {
501
+ deepStrictEqual(await (await Note.fromJsonLd([{
502
+ "@context": "https://www.w3.org/ns/activitystreams",
503
+ type: "Note",
504
+ id: "ap://did:key:z6Mkabc/objects/1"
505
+ }], {
506
+ documentLoader: mockDocumentLoader,
507
+ contextLoader: mockDocumentLoader
508
+ })).toJsonLd(), [{
509
+ "@context": "https://www.w3.org/ns/activitystreams",
510
+ type: "Note",
511
+ id: "ap+ef61://did:key:z6Mkabc/objects/1"
512
+ }]);
513
+ });
514
+ test("fromJsonLd() preserves compact single-item arrays with portable IRIs", async () => {
515
+ const createJson = {
516
+ "@context": "https://www.w3.org/ns/activitystreams",
517
+ type: "Create",
518
+ actor: ["ap://did:key:z6Mkabc/actor"],
519
+ object: "https://example.com/objects/1"
520
+ };
521
+ deepStrictEqual(await (await Create.fromJsonLd(createJson, {
522
+ documentLoader: mockDocumentLoader,
523
+ contextLoader: mockDocumentLoader
524
+ })).toJsonLd(), {
525
+ "@context": "https://www.w3.org/ns/activitystreams",
526
+ type: "Create",
527
+ actor: ["ap+ef61://did:key:z6Mkabc/actor"],
528
+ object: "https://example.com/objects/1"
529
+ });
530
+ deepStrictEqual(createJson.actor, ["ap://did:key:z6Mkabc/actor"]);
531
+ });
532
+ test("fromJsonLd() preserves compact multi-node arrays with portable IRIs", async () => {
533
+ deepStrictEqual(await (await Activity.fromJsonLd([{
534
+ "@context": "https://www.w3.org/ns/activitystreams",
535
+ type: "Create",
536
+ id: "https://example.com/activities/1",
537
+ actor: "https://example.com/actors/alice",
538
+ object: "https://example.com/objects/1"
539
+ }, {
540
+ "@context": "https://www.w3.org/ns/activitystreams",
541
+ type: "Note",
542
+ id: "https://example.com/objects/1",
543
+ attributedTo: "ap://did:key:z6Mkabc/actor"
544
+ }], {
545
+ documentLoader: mockDocumentLoader,
546
+ contextLoader: mockDocumentLoader
547
+ })).toJsonLd(), [{
548
+ "@context": "https://www.w3.org/ns/activitystreams",
549
+ type: "Create",
550
+ id: "https://example.com/activities/1",
551
+ actor: "https://example.com/actors/alice",
552
+ object: "https://example.com/objects/1"
553
+ }, {
554
+ "@context": "https://www.w3.org/ns/activitystreams",
555
+ type: "Note",
556
+ id: "https://example.com/objects/1",
557
+ attributedTo: "ap+ef61://did:key:z6Mkabc/actor"
558
+ }]);
559
+ });
560
+ test("fromJsonLd() preserves nested unmapped terms with portable IRIs", async () => {
561
+ deepStrictEqual(await (await Note.fromJsonLd({
562
+ "@context": "https://www.w3.org/ns/activitystreams",
563
+ type: "Note",
564
+ id: "ap://did:key:z6Mkabc/objects/1",
565
+ attachment: {
566
+ type: "Object",
567
+ name: "Attachment with an unmapped extension.",
568
+ extra: "This nested unmapped property should stay cached."
569
+ }
570
+ }, {
571
+ documentLoader: mockDocumentLoader,
572
+ contextLoader: mockDocumentLoader
573
+ })).toJsonLd(), {
574
+ "@context": "https://www.w3.org/ns/activitystreams",
575
+ type: "Note",
576
+ id: "ap+ef61://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
+ });
584
+ test("fromJsonLd() preserves compact array item extension contexts with portable IRIs", async () => {
585
+ deepStrictEqual(await (await Activity.fromJsonLd([{
586
+ "@context": "https://www.w3.org/ns/activitystreams",
587
+ type: "Create",
588
+ id: "https://example.com/activities/1",
589
+ actor: "https://example.com/actors/alice",
590
+ object: "https://example.com/objects/1"
591
+ }, {
592
+ "@context": ["https://www.w3.org/ns/activitystreams", { extraRef: {
593
+ "@id": "https://example.com/ns#extraRef",
594
+ "@type": "@id"
595
+ } }],
596
+ type: "Note",
597
+ id: "https://example.com/objects/1",
598
+ extraRef: "ap://did:key:z6Mkabc/extra"
599
+ }], {
600
+ documentLoader: mockDocumentLoader,
601
+ contextLoader: mockDocumentLoader
602
+ })).toJsonLd(), [{
603
+ "@context": "https://www.w3.org/ns/activitystreams",
604
+ type: "Create",
605
+ id: "https://example.com/activities/1",
606
+ actor: "https://example.com/actors/alice",
607
+ object: "https://example.com/objects/1"
608
+ }, {
609
+ "@context": ["https://www.w3.org/ns/activitystreams", { extraRef: {
610
+ "@id": "https://example.com/ns#extraRef",
611
+ "@type": "@id"
612
+ } }],
613
+ type: "Note",
614
+ id: "https://example.com/objects/1",
615
+ extraRef: "ap+ef61://did:key:z6Mkabc/extra"
616
+ }]);
617
+ });
618
+ test("fromJsonLd() formats portable IRIs in JSON-LD containers", async () => {
619
+ const jsonLd = await (await Note.fromJsonLd({
620
+ "@context": "https://www.w3.org/ns/activitystreams",
621
+ type: "Note",
622
+ id: "https://example.com/notes/1",
623
+ attributedTo: { "@list": ["ap://did:key:z6Mkabc/actor"] },
624
+ to: { "@set": ["ap://did:key:z6Mkabc/followers"] }
625
+ }, {
626
+ documentLoader: mockDocumentLoader,
627
+ contextLoader: mockDocumentLoader
628
+ })).toJsonLd();
629
+ deepStrictEqual(jsonLd.attributedTo, { "@list": ["ap+ef61://did:key:z6Mkabc/actor"] });
630
+ deepStrictEqual(jsonLd.to, "ap+ef61://did:key:z6Mkabc/followers");
631
+ });
632
+ test("fromJsonLd() formats portable IRIs hidden behind JSON-LD aliases", async () => {
633
+ const activity = await Activity.fromJsonLd({
634
+ "@context": ["https://www.w3.org/ns/activitystreams", {
635
+ as: {
636
+ "@id": "https://www.w3.org/ns/activitystreams#",
637
+ "@prefix": true
638
+ },
639
+ extra: "https://example.com/ns#extra",
640
+ extraRef: {
641
+ "@id": "https://example.com/ns#extraRef",
642
+ "@type": "@id"
643
+ },
644
+ actorRef: {
645
+ "@id": "as:actor",
646
+ "@type": "@id"
647
+ },
648
+ targetRef: {
649
+ "@id": "as:target",
650
+ "@type": "@id"
651
+ }
652
+ }],
653
+ type: "Create",
654
+ actorRef: "ap://did:key:z6Mkabc/actor",
655
+ object: "https://example.com/objects/1",
656
+ targetRef: "ap://did:key:z6Mkabc/target",
657
+ extra: "This extension property should stay cached.",
658
+ extraRef: "ap://did:key:z6Mkabc/extra"
659
+ }, {
660
+ documentLoader: mockDocumentLoader,
661
+ contextLoader: mockDocumentLoader
662
+ });
663
+ deepStrictEqual(activity.actorId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/actor"));
664
+ deepStrictEqual(activity.targetId, new URL("ap+ef61://did%3Akey%3Az6Mkabc/target"));
665
+ const jsonLd = await activity.toJsonLd({ contextLoader: mockDocumentLoader });
666
+ deepStrictEqual(jsonLd.actor, "ap+ef61://did:key:z6Mkabc/actor");
667
+ deepStrictEqual("actorRef" in jsonLd, false);
668
+ deepStrictEqual(jsonLd.target, "ap+ef61://did:key:z6Mkabc/target");
669
+ deepStrictEqual("targetRef" in jsonLd, false);
670
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
671
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
672
+ });
673
+ test("fromJsonLd() preserves portable IRIs in @id extension terms", async () => {
674
+ deepStrictEqual((await (await Note.fromJsonLd({
675
+ "@context": ["https://www.w3.org/ns/activitystreams", { extraRef: {
676
+ "@id": "https://example.com/ns#extraRef",
677
+ "@type": "@id"
678
+ } }],
679
+ type: "Note",
680
+ id: "https://example.com/notes/1",
681
+ extraRef: "ap://did:key:z6Mkabc/extra"
682
+ }, {
683
+ documentLoader: mockDocumentLoader,
684
+ contextLoader: mockDocumentLoader
685
+ })).toJsonLd({ contextLoader: mockDocumentLoader })).extraRef, "ap+ef61://did:key:z6Mkabc/extra");
686
+ });
687
+ test("fromJsonLd() ignores malformed portable IRIs in extension cache terms", async () => {
688
+ const noteJson = {
689
+ "@context": ["https://www.w3.org/ns/activitystreams", { extraRef: {
690
+ "@id": "https://example.com/ns#extraRef",
691
+ "@type": "@id"
692
+ } }],
693
+ type: "Note",
694
+ id: "https://example.com/notes/1",
695
+ extraRef: "ap://example.com/not-portable"
696
+ };
697
+ deepStrictEqual(await (await Note.fromJsonLd(noteJson, {
698
+ documentLoader: mockDocumentLoader,
699
+ contextLoader: mockDocumentLoader
700
+ })).toJsonLd(), noteJson);
701
+ });
702
+ test("fromJsonLd() preserves portable IRIs in @id typed terms", async () => {
703
+ const jsonLd = await (await Note.fromJsonLd({
704
+ "@context": ["https://www.w3.org/ns/activitystreams", {
705
+ "@vocab": "https://example.com/ns#",
706
+ extraRef: { "@type": "@id" }
707
+ }],
708
+ type: "Note",
709
+ id: "https://example.com/notes/1",
710
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
711
+ extraRef: "ap://did:key:z6Mkabc/extra"
712
+ }, {
713
+ documentLoader: mockDocumentLoader,
714
+ contextLoader: mockDocumentLoader
715
+ })).toJsonLd({ contextLoader: mockDocumentLoader });
716
+ deepStrictEqual(jsonLd.content, "This text mentions ap://did:key:z6Mkabc/text.");
717
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
718
+ });
719
+ test("fromJsonLd() preserves portable IRIs hidden behind remote contexts", async () => {
720
+ const contextUrl = "https://example.com/contexts/portable-iris";
721
+ const contextLoader = async (resource, options) => {
722
+ if (resource === contextUrl) return {
723
+ contextUrl: null,
724
+ documentUrl: resource,
725
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
726
+ "@vocab": "https://example.com/ns#",
727
+ extra: "https://example.com/ns#extra",
728
+ extraRef: { "@type": "@id" }
729
+ }] }
730
+ };
731
+ return await mockDocumentLoader(resource, options);
732
+ };
733
+ const jsonLd = await (await Note.fromJsonLd({
734
+ "@context": contextUrl,
735
+ type: "Note",
736
+ id: "https://example.com/notes/1",
737
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
738
+ extra: "This extension property should stay cached.",
739
+ extraRef: "ap://did:key:z6Mkabc/extra"
740
+ }, {
741
+ documentLoader: mockDocumentLoader,
742
+ contextLoader
743
+ })).toJsonLd({ contextLoader });
744
+ deepStrictEqual(jsonLd.content, "This text mentions ap://did:key:z6Mkabc/text.");
745
+ deepStrictEqual(jsonLd.extra, "This extension property should stay cached.");
746
+ deepStrictEqual(jsonLd.extraRef, "ap+ef61://did:key:z6Mkabc/extra");
747
+ });
748
+ test("fromJsonLd() formats portable IRIs hidden behind nested remote contexts", async () => {
749
+ const rootContextUrl = "https://example.com/contexts/nested-portable-iris";
750
+ const nestedContextUrl = "https://example.com/contexts/nested-portable-ref";
751
+ const contextLoader = async (resource, options) => {
752
+ if (resource === rootContextUrl) return {
753
+ contextUrl: null,
754
+ documentUrl: resource,
755
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
756
+ "@vocab": "https://example.com/ns#",
757
+ extraContainer: "https://example.com/ns#extraContainer"
758
+ }] }
759
+ };
760
+ if (resource === nestedContextUrl) return {
761
+ contextUrl: null,
762
+ documentUrl: resource,
763
+ document: { "@context": {
764
+ "@vocab": "https://example.com/ns#",
765
+ extra: "https://example.com/ns#extra",
766
+ extraRef: { "@type": "@id" }
767
+ } }
768
+ };
769
+ return await mockDocumentLoader(resource, options);
770
+ };
771
+ deepStrictEqual((await (await Note.fromJsonLd({
772
+ "@context": rootContextUrl,
773
+ type: "Note",
774
+ id: "https://example.com/notes/1",
775
+ extraContainer: {
776
+ "@context": nestedContextUrl,
777
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
778
+ extra: "This nested extension object should stay cached.",
779
+ extraRef: "ap://did:key:z6Mkabc/extra"
780
+ }
781
+ }, {
782
+ documentLoader: mockDocumentLoader,
783
+ contextLoader
784
+ })).toJsonLd({ contextLoader })).extraContainer, {
785
+ "@context": nestedContextUrl,
786
+ content: "This text mentions ap://did:key:z6Mkabc/text.",
787
+ extra: "This nested extension object should stay cached.",
788
+ extraRef: { id: "ap+ef61://did:key:z6Mkabc/extra" }
789
+ });
790
+ });
791
+ test("fromJsonLd() formats portable IRIs in sibling remote-context objects", async () => {
792
+ const rootContextUrl = "https://example.com/contexts/sibling-containers";
793
+ const nestedContextUrl = "https://example.com/contexts/sibling-extra-ref";
794
+ const contextLoader = async (resource, options) => {
795
+ if (resource === rootContextUrl) return {
796
+ contextUrl: null,
797
+ documentUrl: resource,
798
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
799
+ "@vocab": "https://example.com/ns#",
800
+ firstExtraContainer: "https://example.com/ns#firstExtraContainer",
801
+ secondExtraContainer: "https://example.com/ns#secondExtraContainer"
802
+ }] }
803
+ };
804
+ if (resource === nestedContextUrl) return {
805
+ contextUrl: null,
806
+ documentUrl: resource,
807
+ document: { "@context": {
808
+ "@vocab": "https://example.com/ns#",
809
+ extraRef: { "@type": "@id" }
810
+ } }
811
+ };
812
+ return await mockDocumentLoader(resource, options);
813
+ };
814
+ const jsonLd = await (await Note.fromJsonLd({
815
+ "@context": rootContextUrl,
816
+ type: "Note",
817
+ id: "https://example.com/notes/1",
818
+ firstExtraContainer: {
819
+ "@context": nestedContextUrl,
820
+ content: "No portable IRI here."
821
+ },
822
+ secondExtraContainer: {
823
+ "@context": nestedContextUrl,
824
+ extraRef: "ap://did:key:z6Mkabc/second"
825
+ }
826
+ }, {
827
+ documentLoader: mockDocumentLoader,
828
+ contextLoader
829
+ })).toJsonLd({ contextLoader });
830
+ deepStrictEqual(jsonLd.firstExtraContainer, {
831
+ "@context": nestedContextUrl,
832
+ content: "No portable IRI here."
833
+ });
834
+ deepStrictEqual(jsonLd.secondExtraContainer, {
835
+ "@context": nestedContextUrl,
836
+ extraRef: { id: "ap+ef61://did:key:z6Mkabc/second" }
837
+ });
838
+ });
839
+ test("fromJsonLd() batches unmapped portable IRI term checks", async () => {
840
+ const contextUrl = "https://example.com/contexts/batched-portable-aliases";
841
+ let contextLoads = 0;
842
+ const contextLoader = async (resource, options) => {
843
+ if (resource === contextUrl) {
844
+ contextLoads++;
845
+ return {
846
+ contextUrl: null,
847
+ documentUrl: resource,
848
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
849
+ as: {
850
+ "@id": "https://www.w3.org/ns/activitystreams#",
851
+ "@prefix": true
852
+ },
853
+ actorRef0: {
854
+ "@id": "as:actor",
855
+ "@type": "@id"
856
+ },
857
+ actorRef1: {
858
+ "@id": "as:actor",
859
+ "@type": "@id"
860
+ },
861
+ targetRef0: {
862
+ "@id": "as:target",
863
+ "@type": "@id"
864
+ },
865
+ targetRef1: {
866
+ "@id": "as:target",
867
+ "@type": "@id"
868
+ },
869
+ objectRef0: {
870
+ "@id": "as:object",
871
+ "@type": "@id"
872
+ },
873
+ objectRef1: {
874
+ "@id": "as:object",
875
+ "@type": "@id"
876
+ }
877
+ }] }
878
+ };
879
+ }
880
+ return await mockDocumentLoader(resource, options);
881
+ };
882
+ await (await Activity.fromJsonLd({
883
+ "@context": contextUrl,
884
+ type: "Create",
885
+ actorRef0: "ap://did:key:z6Mkabc/actor0",
886
+ actorRef1: "ap://did:key:z6Mkabc/actor1",
887
+ targetRef0: "ap://did:key:z6Mkabc/target0",
888
+ targetRef1: "ap://did:key:z6Mkabc/target1",
889
+ objectRef0: "https://example.com/objects/0",
890
+ objectRef1: "https://example.com/objects/1"
891
+ }, {
892
+ documentLoader: mockDocumentLoader,
893
+ contextLoader
894
+ })).toJsonLd({ contextLoader });
895
+ ok(contextLoads <= 5);
896
+ });
897
+ test("fromJsonLd() falls back when portable IRI cache merge fails", async () => {
898
+ const contextUrl = "https://example.com/contexts/failing-cache-merge";
899
+ let contextLoads = 0;
900
+ const contextLoader = async (resource, options) => {
901
+ if (resource === contextUrl) {
902
+ contextLoads++;
903
+ if (contextLoads > 1) throw new Error("merge context unavailable");
904
+ return {
905
+ contextUrl: null,
906
+ documentUrl: resource,
907
+ document: { "@context": ["https://www.w3.org/ns/activitystreams", {
908
+ "@vocab": "https://example.com/ns#",
909
+ extraRef: { "@type": "@id" }
910
+ }] }
911
+ };
912
+ }
913
+ return await mockDocumentLoader(resource, options);
914
+ };
915
+ const jsonLd = await (await Note.fromJsonLd({
916
+ "@context": contextUrl,
917
+ type: "Note",
918
+ id: "ap://did:key:z6Mkabc/objects/1",
919
+ extraRef: "ap://did:key:z6Mkabc/extra"
920
+ }, {
921
+ documentLoader: mockDocumentLoader,
922
+ contextLoader
923
+ })).toJsonLd({ contextLoader: mockDocumentLoader });
924
+ deepStrictEqual(jsonLd.type, "Note");
925
+ deepStrictEqual(jsonLd.id, "ap+ef61://did:key:z6Mkabc/objects/1");
926
+ });
927
+ test("fromJsonLd() formats portable IRIs in scalar URL values", async () => {
928
+ const note = await Note.fromJsonLd({
929
+ "@context": [
930
+ "https://www.w3.org/ns/activitystreams",
931
+ "https://gotosocial.org/ns",
932
+ { quoteUrl: "as:quoteUrl" }
933
+ ],
934
+ type: "Note",
935
+ id: "https://example.com/notes/1",
936
+ quoteUrl: "ap://did:key:z6Mkabc/objects/1"
937
+ }, {
938
+ documentLoader: mockDocumentLoader,
939
+ contextLoader: mockDocumentLoader
940
+ });
941
+ deepStrictEqual(note.quoteUrl, new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"));
942
+ deepStrictEqual((await note.toJsonLd({ contextLoader: mockDocumentLoader })).quoteUrl, "ap+ef61://did:key:z6Mkabc/objects/1");
943
+ });
944
+ test("fromJsonLd() formats portable IRIs in URL value lists", async () => {
945
+ const note = await Note.fromJsonLd({
946
+ "@context": [
947
+ "https://www.w3.org/ns/activitystreams",
948
+ "https://gotosocial.org/ns",
949
+ { quoteUrl: "as:quoteUrl" }
950
+ ],
951
+ type: "Note",
952
+ id: "https://example.com/notes/1",
953
+ quoteUrl: { "@list": [{ "@value": "ap://did:key:z6Mkabc/objects/1" }] }
954
+ }, {
955
+ documentLoader: mockDocumentLoader,
956
+ contextLoader: mockDocumentLoader
957
+ });
958
+ deepStrictEqual(note.quoteUrl, new URL("ap+ef61://did%3Akey%3Az6Mkabc/objects/1"));
959
+ deepStrictEqual((await note.toJsonLd({ contextLoader: mockDocumentLoader })).quoteUrl, { "@list": ["ap+ef61://did:key:z6Mkabc/objects/1"] });
960
+ });
305
961
  test({
306
962
  name: "Activity.getObject()",
307
963
  permissions: {
@@ -357,6 +1013,34 @@ test({
357
1013
  });
358
1014
  }
359
1015
  });
1016
+ test("Activity.getObject() fetches canonical portable IRIs", async () => {
1017
+ const fetchedUrls = [];
1018
+ const documentLoader = async (url) => {
1019
+ fetchedUrls.push(url);
1020
+ return {
1021
+ contextUrl: null,
1022
+ documentUrl: url,
1023
+ document: {
1024
+ "@context": "https://www.w3.org/ns/activitystreams",
1025
+ id: url,
1026
+ type: "Note",
1027
+ content: "Fetched portable object"
1028
+ }
1029
+ };
1030
+ };
1031
+ assertInstanceOf(await (await Activity.fromJsonLd({
1032
+ "@context": "https://www.w3.org/ns/activitystreams",
1033
+ type: "Create",
1034
+ object: "ap://did:key:z6Mkabc/objects/1"
1035
+ }, {
1036
+ documentLoader,
1037
+ contextLoader: mockDocumentLoader
1038
+ })).getObject({
1039
+ documentLoader,
1040
+ contextLoader: mockDocumentLoader
1041
+ }), Note);
1042
+ deepStrictEqual(fetchedUrls, ["ap+ef61://did:key:z6Mkabc/objects/1"]);
1043
+ });
360
1044
  test({
361
1045
  name: "Activity.getObjects()",
362
1046
  permissions: {
@@ -1630,6 +2314,26 @@ test("FEP-fe34: crossOrigin trust behavior", async () => {
1630
2314
  deepStrictEqual(result?.id, new URL("https://malicious.com/fake-note"));
1631
2315
  deepStrictEqual(result?.content, "This is a spoofed note");
1632
2316
  });
2317
+ test("FEP-fe34: id-less owners honor crossOrigin trust", async () => {
2318
+ const result = await (await Create.fromJsonLd({
2319
+ "@context": "https://www.w3.org/ns/activitystreams",
2320
+ "@type": "Create",
2321
+ "actor": "https://example.com/actor",
2322
+ "object": {
2323
+ "@type": "Note",
2324
+ "@id": "https://different-origin.com/note",
2325
+ "content": "Embedded note"
2326
+ }
2327
+ })).getObject({
2328
+ crossOrigin: "trust",
2329
+ documentLoader: async (url) => {
2330
+ throw new Error(`Unexpected fetch: ${url}`);
2331
+ }
2332
+ });
2333
+ assertInstanceOf(result, Note);
2334
+ deepStrictEqual(result.id, new URL("https://different-origin.com/note"));
2335
+ deepStrictEqual(result.content, "Embedded note");
2336
+ });
1633
2337
  test("FEP-fe34: Same origin objects are trusted", async () => {
1634
2338
  const sameOriginDocumentLoader = async (url) => {
1635
2339
  if (url === "https://example.com/note") return {
@@ -1725,6 +2429,35 @@ test("FEP-fe34: Constructor vs JSON-LD parsing trust difference", async () => {
1725
2429
  };
1726
2430
  deepStrictEqual((await jsonLdCreate.getObject({ documentLoader }))?.content, "Fetched from origin");
1727
2431
  });
2432
+ test("FEP-fe34: Portable DID authorities are cross-origin boundaries", async () => {
2433
+ const create = await Create.fromJsonLd({
2434
+ "@context": "https://www.w3.org/ns/activitystreams",
2435
+ "@type": "Create",
2436
+ "@id": "ap://did:key:z6MkOwner/create",
2437
+ "actor": "ap://did:key:z6MkOwner/actor",
2438
+ "object": {
2439
+ "@type": "Note",
2440
+ "@id": "ap://did:key:z6MkOther/note",
2441
+ "content": "Embedded portable note"
2442
+ }
2443
+ });
2444
+ const documentLoader = async (url) => {
2445
+ if (url === "ap+ef61://did:key:z6MkOther/note") return {
2446
+ documentUrl: url,
2447
+ contextUrl: null,
2448
+ document: {
2449
+ "@context": "https://www.w3.org/ns/activitystreams",
2450
+ "@type": "Note",
2451
+ "@id": "ap://did:key:z6MkOther/note",
2452
+ "content": "Fetched portable note"
2453
+ }
2454
+ };
2455
+ throw new Error("Document not found");
2456
+ };
2457
+ const result = await create.getObject({ documentLoader });
2458
+ assertInstanceOf(result, Note);
2459
+ deepStrictEqual(result.content, "Fetched portable note");
2460
+ });
1728
2461
  test("FEP-fe34: Array properties respect cross-origin policy", async () => {
1729
2462
  const crossOriginDocumentLoader = async (url) => {
1730
2463
  if (url === "https://different-origin.com/note1") return {
@@ -1798,6 +2531,60 @@ test("FEP-fe34: Array properties with crossOrigin trust option", async () => {
1798
2531
  deepStrictEqual(items[0].content, "Fake note 1");
1799
2532
  deepStrictEqual(items[1].content, "Legitimate note 2");
1800
2533
  });
2534
+ test("FEP-fe34: id-less arrays honor crossOrigin trust", async () => {
2535
+ const collection = await Collection.fromJsonLd({
2536
+ "@context": "https://www.w3.org/ns/activitystreams",
2537
+ "@type": "Collection",
2538
+ "items": [{
2539
+ "@type": "Note",
2540
+ "@id": "https://different-origin.com/note1",
2541
+ "content": "Embedded note 1"
2542
+ }, {
2543
+ "@type": "Note",
2544
+ "@id": "https://different-origin.com/note2",
2545
+ "content": "Embedded note 2"
2546
+ }]
2547
+ });
2548
+ const items = [];
2549
+ for await (const item of collection.getItems({
2550
+ crossOrigin: "trust",
2551
+ documentLoader: async (url) => {
2552
+ throw new Error(`Unexpected fetch: ${url}`);
2553
+ }
2554
+ })) items.push(item);
2555
+ deepStrictEqual(items.length, 2);
2556
+ assertInstanceOf(items[0], Note);
2557
+ assertInstanceOf(items[1], Note);
2558
+ deepStrictEqual(items[0].content, "Embedded note 1");
2559
+ deepStrictEqual(items[1].content, "Embedded note 2");
2560
+ });
2561
+ test("FEP-fe34: Array properties track trust per item", async () => {
2562
+ const collection = new Collection({
2563
+ id: new URL("https://example.com/collection"),
2564
+ items: [new Note({
2565
+ id: new URL("https://malicious.com/fake-note1"),
2566
+ content: "Trusted constructor note 1"
2567
+ }), new URL("https://different-origin.com/note2")]
2568
+ });
2569
+ const documentLoader = async (url) => {
2570
+ if (url === "https://different-origin.com/note2") return {
2571
+ documentUrl: url,
2572
+ contextUrl: null,
2573
+ document: {
2574
+ "@context": "https://www.w3.org/ns/activitystreams",
2575
+ "@type": "Note",
2576
+ "@id": "https://malicious.com/fake-note2",
2577
+ "content": "Untrusted fetched note 2"
2578
+ }
2579
+ };
2580
+ throw new Error("Document not found");
2581
+ };
2582
+ const items = [];
2583
+ for await (const item of collection.getItems({ documentLoader })) items.push(item);
2584
+ deepStrictEqual(items.length, 1);
2585
+ assertInstanceOf(items[0], Note);
2586
+ deepStrictEqual(items[0].content, "Trusted constructor note 1");
2587
+ });
1801
2588
  test("FEP-fe34: Embedded objects in arrays from JSON-LD respect cross-origin policy", async () => {
1802
2589
  const collectionDocumentLoader = async (url) => {
1803
2590
  if (url === "https://example.com/collection") return {