@fedify/vocab 2.4.0-dev.2233 → 2.4.0-dev.2240

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.
@@ -125,6 +125,48 @@ test("lookupObject() with FEP-ef61 portable actors", {
125
125
  },
126
126
  );
127
127
 
128
+ await t.step("explicit gateways replace WebFinger hints", async () => {
129
+ fetchMock.removeRoutes();
130
+ fetchMock.get(
131
+ webFingerPrefix,
132
+ selfLinks(
133
+ `ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`,
134
+ ),
135
+ );
136
+ const second = `https://second.example${gatewayPath}`;
137
+ const documentLoader = createLoader({ [second]: person() });
138
+ const actor = await lookupObject("@alice@example.com", {
139
+ documentLoader,
140
+ contextLoader: mockDocumentLoader,
141
+ verifyPortableObject: createVerifier(),
142
+ gateways: ["https://second.example"],
143
+ });
144
+ assertInstanceOf(actor, Person);
145
+ deepStrictEqual(documentLoader.fetched, [compatibleId, second]);
146
+ });
147
+
148
+ await t.step("WebFinger host counts toward the five attempts", async () => {
149
+ fetchMock.removeRoutes();
150
+ fetchMock.get(webFingerPrefix, selfLinks(`ap://${did}/actor`));
151
+ const documentLoader = createLoader({});
152
+ equal(
153
+ await lookupObject("@alice@example.com", {
154
+ documentLoader,
155
+ contextLoader: mockDocumentLoader,
156
+ verifyPortableObject: createVerifier(),
157
+ gateways: Array.from(
158
+ { length: 6 },
159
+ (_, i) => `https://g${i}.example`,
160
+ ),
161
+ }),
162
+ null,
163
+ );
164
+ deepStrictEqual(documentLoader.fetched, [
165
+ compatibleId,
166
+ ...[0, 1, 2, 3].map((i) => `https://g${i}.example${gatewayPath}`),
167
+ ]);
168
+ });
169
+
128
170
  await t.step("tries the next self link after a failure", async () => {
129
171
  fetchMock.removeRoutes();
130
172
  const other = `https://other.example${gatewayPath}`;
@@ -253,6 +295,17 @@ test("lookupObject() with FEP-ef61 portable actors", {
253
295
  });
254
296
  assertInstanceOf(actor, Person);
255
297
  deepStrictEqual(actor.id, new URL(compatibleId));
298
+
299
+ const malformed = `${compatibleId}?gateways=x`;
300
+ const ordinaryLoader = createLoader({
301
+ [malformed]: person(malformed),
302
+ });
303
+ const ordinary = await lookupObject(malformed, {
304
+ documentLoader: ordinaryLoader,
305
+ contextLoader: mockDocumentLoader,
306
+ });
307
+ assertInstanceOf(ordinary, Person);
308
+ deepStrictEqual(ordinaryLoader.fetched, [malformed]);
256
309
  });
257
310
 
258
311
  await t.step("refuses portable IRIs changed by URL parsing", async () => {
@@ -281,6 +334,21 @@ test("lookupObject() with FEP-ef61 portable actors", {
281
334
  deepStrictEqual(documentLoader.fetched, []);
282
335
  });
283
336
 
337
+ await t.step("refuses compatible IDs changed by URL parsing", async () => {
338
+ fetchMock.removeRoutes();
339
+ const dotted = compatibleId.replace("/actor", "/x/../actor");
340
+ const documentLoader = createLoader({ [compatibleId]: person() });
341
+ const options = {
342
+ documentLoader,
343
+ contextLoader: mockDocumentLoader,
344
+ verifyPortableObject: createVerifier(),
345
+ };
346
+ equal(await lookupObject(dotted, options), null);
347
+ fetchMock.get(webFingerPrefix, selfLinks(dotted));
348
+ equal(await lookupObject("@alice@example.com", options), null);
349
+ deepStrictEqual(documentLoader.fetched, []);
350
+ });
351
+
284
352
  await t.step("bounds gateway requests", async () => {
285
353
  fetchMock.removeRoutes();
286
354
  const hints = Array.from(
@@ -387,6 +455,107 @@ test("lookupObject() looks up portable identifiers directly", async () => {
387
455
  deepStrictEqual(documentLoader.fetched.length, 5);
388
456
  });
389
457
 
458
+ test("lookupObject() uses explicit gateways for portable IDs", async (t) => {
459
+ const first = `https://first.example${gatewayPath}`;
460
+ const second = `https://second.example${gatewayPath}`;
461
+ const hint = `https://hint.example${gatewayPath}`;
462
+ const options = {
463
+ contextLoader: mockDocumentLoader,
464
+ verifyPortableObject: createVerifier(),
465
+ };
466
+
467
+ await t.step("bare ap: ID and one gateway", async () => {
468
+ const documentLoader = createLoader({ [first]: person() });
469
+ assertInstanceOf(
470
+ await lookupObject(`ap://${did}/actor`, {
471
+ ...options,
472
+ documentLoader,
473
+ gateways: ["https://first.example"],
474
+ }),
475
+ Person,
476
+ );
477
+ deepStrictEqual(documentLoader.fetched, [first]);
478
+ });
479
+
480
+ await t.step("tries several gateways in order", async () => {
481
+ const documentLoader = createLoader({ [second]: person() });
482
+ assertInstanceOf(
483
+ await lookupObject(actorId, {
484
+ ...options,
485
+ documentLoader,
486
+ gateways: ["https://first.example", new URL("https://second.example")],
487
+ }),
488
+ Person,
489
+ );
490
+ deepStrictEqual(documentLoader.fetched, [first, second]);
491
+ });
492
+
493
+ await t.step("explicit gateways replace location hints", async () => {
494
+ const documentLoader = createLoader({
495
+ [second]: person(),
496
+ [hint]: person(),
497
+ });
498
+ assertInstanceOf(
499
+ await lookupObject(
500
+ `ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`,
501
+ {
502
+ ...options,
503
+ documentLoader,
504
+ gateways: ["https://first.example", "https://second.example"],
505
+ },
506
+ ),
507
+ Person,
508
+ );
509
+ deepStrictEqual(documentLoader.fetched, [first, second]);
510
+ });
511
+
512
+ await t.step("empty list uses a custom document loader", async () => {
513
+ const hintedId = `ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`;
514
+ const canonicalHintedId = `${actorId}?@gateway=https%3A%2F%2Fhint.example`;
515
+ const documentLoader = createLoader({
516
+ [canonicalHintedId]: person(),
517
+ [hint]: person(),
518
+ });
519
+ const actor = await lookupObject(hintedId, {
520
+ ...options,
521
+ documentLoader,
522
+ gateways: [],
523
+ });
524
+ deepStrictEqual(documentLoader.fetched, [canonicalHintedId]);
525
+ assertInstanceOf(actor, Person);
526
+ });
527
+
528
+ await t.step("invalid gateway throws before fetching", async () => {
529
+ const documentLoader = createLoader({});
530
+ for (const verifyPortableObject of [undefined, createVerifier()]) {
531
+ await rejects(
532
+ () =>
533
+ lookupObject(actorId, {
534
+ documentLoader,
535
+ contextLoader: mockDocumentLoader,
536
+ verifyPortableObject,
537
+ gateways: ["https://bad.example/path"],
538
+ }),
539
+ TypeError,
540
+ );
541
+ }
542
+ deepStrictEqual(documentLoader.fetched, []);
543
+ });
544
+
545
+ await t.step("a malformed portable ID is refused first", async () => {
546
+ const documentLoader = createLoader({});
547
+ equal(
548
+ await lookupObject(`ap://${did}/x/../actor`, {
549
+ ...options,
550
+ documentLoader,
551
+ gateways: ["https://bad.example/path"],
552
+ }),
553
+ null,
554
+ );
555
+ deepStrictEqual(documentLoader.fetched, []);
556
+ });
557
+ });
558
+
390
559
  test("getActorHandle() with FEP-ef61 portable actors", {
391
560
  sanitizeResources: false,
392
561
  sanitizeOps: false,
@@ -499,6 +668,77 @@ test("lookupObject() reports inferred gateways as hints", async () => {
499
668
  deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
500
669
  });
501
670
 
671
+ test("lookupObject() reports explicit gateways to the verifier", async (t) => {
672
+ const calls: PortableObjectVerifierOptions[] = [];
673
+ // deno-lint-ignore require-await
674
+ const verifyPortableObject: PortableObjectVerifier = async (_, options) => {
675
+ calls.push(options);
676
+ return { verified: true };
677
+ };
678
+
679
+ await t.step("bare ID with a limited explicit list", async () => {
680
+ const fifth = `https://g4.example${gatewayPath}`;
681
+ const documentLoader = createLoader({ [fifth]: person() });
682
+ assertInstanceOf(
683
+ await lookupObject(actorId, {
684
+ documentLoader,
685
+ contextLoader: mockDocumentLoader,
686
+ verifyPortableObject,
687
+ gateways: Array.from({ length: 6 }, (_, i) => `https://g${i}.example`),
688
+ }),
689
+ Person,
690
+ );
691
+ deepStrictEqual(
692
+ documentLoader.fetched,
693
+ [0, 1, 2, 3, 4].map((i) => `https://g${i}.example${gatewayPath}`),
694
+ );
695
+ deepStrictEqual(
696
+ calls.at(-1)?.gateways,
697
+ [0, 1, 2, 3, 4].map((i) => new URL(`https://g${i}.example`)),
698
+ );
699
+ equal(calls.at(-1)?.gatewayHints, undefined);
700
+ });
701
+
702
+ await t.step(
703
+ "compatible origin stays first and is deduplicated",
704
+ async () => {
705
+ const second = `https://second.example${gatewayPath}`;
706
+ const documentLoader = createLoader({ [second]: person() });
707
+ assertInstanceOf(
708
+ await lookupObject(compatibleId, {
709
+ documentLoader,
710
+ contextLoader: mockDocumentLoader,
711
+ verifyPortableObject,
712
+ gateways: ["https://example.com", "https://second.example"],
713
+ }),
714
+ Person,
715
+ );
716
+ deepStrictEqual(documentLoader.fetched, [compatibleId, second]);
717
+ deepStrictEqual(calls.at(-1)?.gateways, [
718
+ new URL("https://example.com"),
719
+ new URL("https://second.example"),
720
+ ]);
721
+ equal(calls.at(-1)?.gatewayHints, undefined);
722
+ },
723
+ );
724
+
725
+ await t.step("empty list keeps the compatible origin", async () => {
726
+ const documentLoader = createLoader({ [compatibleId]: person() });
727
+ assertInstanceOf(
728
+ await lookupObject(compatibleId, {
729
+ documentLoader,
730
+ contextLoader: mockDocumentLoader,
731
+ verifyPortableObject,
732
+ gateways: [],
733
+ }),
734
+ Person,
735
+ );
736
+ deepStrictEqual(documentLoader.fetched, [compatibleId]);
737
+ deepStrictEqual(calls.at(-1)?.gateways, []);
738
+ equal(calls.at(-1)?.gatewayHints, undefined);
739
+ });
740
+ });
741
+
502
742
  test("lookupObject() verifies fetched documents that stand for portable objects", async (t) => {
503
743
  const plainUrl = "https://example.com/users/alice";
504
744
  const redirectingLoader = (
@@ -540,6 +780,32 @@ test("lookupObject() verifies fetched documents that stand for portable objects"
540
780
  deepStrictEqual(calls.length, 1);
541
781
  deepStrictEqual(calls[0].documentUrl, new URL(compatibleId));
542
782
  deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
783
+ const explicitCalls: PortableObjectVerifierOptions[] = [];
784
+ const explicitActor = await lookupObject(plainUrl, {
785
+ documentLoader,
786
+ contextLoader: mockDocumentLoader,
787
+ gateways: ["https://other.example"],
788
+ // deno-lint-ignore require-await
789
+ verifyPortableObject: async (_, options) => {
790
+ explicitCalls.push(options);
791
+ return { verified: true };
792
+ },
793
+ });
794
+ assertInstanceOf(explicitActor, Person);
795
+ deepStrictEqual(explicitCalls[0].gateways, [
796
+ new URL("https://other.example"),
797
+ ]);
798
+ equal(explicitCalls[0].gatewayHints, undefined);
799
+ await rejects(
800
+ () =>
801
+ lookupObject(plainUrl, {
802
+ documentLoader,
803
+ contextLoader: mockDocumentLoader,
804
+ verifyPortableObject: createVerifier(),
805
+ gateways: ["https://bad.example/path"],
806
+ }),
807
+ TypeError,
808
+ );
543
809
  equal(
544
810
  await lookupObject(plainUrl, {
545
811
  documentLoader,
package/src/vocab.test.ts CHANGED
@@ -667,6 +667,31 @@ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
667
667
  );
668
668
  });
669
669
 
670
+ test("fromJsonLd() refuses portable IDs changed by URL parsing", async () => {
671
+ const base = "ap://did:key:z6Mkabc";
672
+ for (
673
+ const id of [
674
+ `${base}/a/../b`,
675
+ `${base}/a/%2e%2E/b`,
676
+ "https://gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b",
677
+ "https:/gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b",
678
+ ]
679
+ ) {
680
+ await rejects(
681
+ () =>
682
+ Note.fromJsonLd({
683
+ "@context": "https://www.w3.org/ns/activitystreams",
684
+ type: "Note",
685
+ id,
686
+ }, {
687
+ documentLoader: mockDocumentLoader,
688
+ contextLoader: mockDocumentLoader,
689
+ }),
690
+ TypeError,
691
+ );
692
+ }
693
+ });
694
+
670
695
  test("FEP-ef61: actor gateways round-trip as an ordered URI list", async () => {
671
696
  const actorClasses = [Application, Group, Organization, Person, Service];
672
697
  const gateways = [