@fedify/vocab 2.4.0-dev.2233 → 2.4.0-dev.2245
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/mod.cjs +39 -26
- package/dist/mod.d.cts +15 -1
- package/dist/mod.d.ts +15 -1
- package/dist/mod.js +39 -26
- package/dist-tests/{actor-C8V9Eu9k.mjs → actor-CwM3qnho.mjs} +1 -1
- package/dist-tests/actor.test.mjs +1 -1
- package/dist-tests/gateway.test.mjs +3 -3
- package/dist-tests/{lookup-2QrSuQR0.mjs → lookup--QvDSQ8m.mjs} +39 -26
- package/dist-tests/lookup.test.mjs +1 -1
- package/dist-tests/portable-lookup.test.mjs +200 -2
- package/dist-tests/translation.test.mjs +2 -2
- package/dist-tests/vocab.test.mjs +16 -0
- package/package.json +4 -4
- package/src/gateway.test.ts +15 -12
- package/src/lookup.ts +89 -30
- package/src/portable-lookup.test.ts +266 -0
- package/src/vocab.test.ts +25 -0
|
@@ -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 = [
|