@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.
- 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-C-Bvqy6v.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-DiU8zIYx.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
|
@@ -2,8 +2,8 @@ import "temporal-polyfill";
|
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
3
|
import { F as Person } from "./vocab-D3C791xF.mjs";
|
|
4
4
|
import { t as esm_default } from "./esm-2_Uqq_my.mjs";
|
|
5
|
-
import { n as getActorHandle } from "./actor-
|
|
6
|
-
import { t as lookupObject } from "./lookup-
|
|
5
|
+
import { n as getActorHandle } from "./actor-C-Bvqy6v.mjs";
|
|
6
|
+
import { t as lookupObject } from "./lookup-DiU8zIYx.mjs";
|
|
7
7
|
import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
|
|
8
8
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
9
9
|
import { deepStrictEqual, equal, rejects } from "node:assert/strict";
|
|
@@ -98,6 +98,36 @@ test("lookupObject() with FEP-ef61 portable actors", {
|
|
|
98
98
|
}), Person);
|
|
99
99
|
deepStrictEqual(documentLoader.fetched, [compatibleId, hinted]);
|
|
100
100
|
});
|
|
101
|
+
await t.step("explicit gateways replace WebFinger hints", async () => {
|
|
102
|
+
esm_default.removeRoutes();
|
|
103
|
+
esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`));
|
|
104
|
+
const second = `https://second.example${gatewayPath}`;
|
|
105
|
+
const documentLoader = createLoader({ [second]: person() });
|
|
106
|
+
assertInstanceOf(await lookupObject("@alice@example.com", {
|
|
107
|
+
documentLoader,
|
|
108
|
+
contextLoader: mockDocumentLoader,
|
|
109
|
+
verifyPortableObject: createVerifier(),
|
|
110
|
+
gateways: ["https://second.example"]
|
|
111
|
+
}), Person);
|
|
112
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, second]);
|
|
113
|
+
});
|
|
114
|
+
await t.step("WebFinger host counts toward the five attempts", async () => {
|
|
115
|
+
esm_default.removeRoutes();
|
|
116
|
+
esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`));
|
|
117
|
+
const documentLoader = createLoader({});
|
|
118
|
+
equal(await lookupObject("@alice@example.com", {
|
|
119
|
+
documentLoader,
|
|
120
|
+
contextLoader: mockDocumentLoader,
|
|
121
|
+
verifyPortableObject: createVerifier(),
|
|
122
|
+
gateways: Array.from({ length: 6 }, (_, i) => `https://g${i}.example`)
|
|
123
|
+
}), null);
|
|
124
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, ...[
|
|
125
|
+
0,
|
|
126
|
+
1,
|
|
127
|
+
2,
|
|
128
|
+
3
|
|
129
|
+
].map((i) => `https://g${i}.example${gatewayPath}`)]);
|
|
130
|
+
});
|
|
101
131
|
await t.step("tries the next self link after a failure", async () => {
|
|
102
132
|
esm_default.removeRoutes();
|
|
103
133
|
const other = `https://other.example${gatewayPath}`;
|
|
@@ -177,6 +207,13 @@ test("lookupObject() with FEP-ef61 portable actors", {
|
|
|
177
207
|
});
|
|
178
208
|
assertInstanceOf(actor, Person);
|
|
179
209
|
deepStrictEqual(actor.id, new URL(compatibleId));
|
|
210
|
+
const malformed = `${compatibleId}?gateways=x`;
|
|
211
|
+
const ordinaryLoader = createLoader({ [malformed]: person(malformed) });
|
|
212
|
+
assertInstanceOf(await lookupObject(malformed, {
|
|
213
|
+
documentLoader: ordinaryLoader,
|
|
214
|
+
contextLoader: mockDocumentLoader
|
|
215
|
+
}), Person);
|
|
216
|
+
deepStrictEqual(ordinaryLoader.fetched, [malformed]);
|
|
180
217
|
});
|
|
181
218
|
await t.step("refuses portable IRIs changed by URL parsing", async () => {
|
|
182
219
|
esm_default.removeRoutes();
|
|
@@ -191,6 +228,20 @@ test("lookupObject() with FEP-ef61 portable actors", {
|
|
|
191
228
|
equal(await lookupObject(`ap://${did}/x/../actor?@gateway=https%3A%2F%2Fexample.com`, options), null);
|
|
192
229
|
deepStrictEqual(documentLoader.fetched, []);
|
|
193
230
|
});
|
|
231
|
+
await t.step("refuses compatible IDs changed by URL parsing", async () => {
|
|
232
|
+
esm_default.removeRoutes();
|
|
233
|
+
const dotted = compatibleId.replace("/actor", "/x/../actor");
|
|
234
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
235
|
+
const options = {
|
|
236
|
+
documentLoader,
|
|
237
|
+
contextLoader: mockDocumentLoader,
|
|
238
|
+
verifyPortableObject: createVerifier()
|
|
239
|
+
};
|
|
240
|
+
equal(await lookupObject(dotted, options), null);
|
|
241
|
+
esm_default.get(webFingerPrefix, selfLinks(dotted));
|
|
242
|
+
equal(await lookupObject("@alice@example.com", options), null);
|
|
243
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
244
|
+
});
|
|
194
245
|
await t.step("bounds gateway requests", async () => {
|
|
195
246
|
esm_default.removeRoutes();
|
|
196
247
|
const hints = Array.from({ length: 5 }, (_, i) => `@gateway=https%3A%2F%2Fg${i}.example`).join("&");
|
|
@@ -267,6 +318,79 @@ test("lookupObject() looks up portable identifiers directly", async () => {
|
|
|
267
318
|
}), null);
|
|
268
319
|
deepStrictEqual(documentLoader.fetched.length, 5);
|
|
269
320
|
});
|
|
321
|
+
test("lookupObject() uses explicit gateways for portable IDs", async (t) => {
|
|
322
|
+
const first = `https://first.example${gatewayPath}`;
|
|
323
|
+
const second = `https://second.example${gatewayPath}`;
|
|
324
|
+
const hint = `https://hint.example${gatewayPath}`;
|
|
325
|
+
const options = {
|
|
326
|
+
contextLoader: mockDocumentLoader,
|
|
327
|
+
verifyPortableObject: createVerifier()
|
|
328
|
+
};
|
|
329
|
+
await t.step("bare ap: ID and one gateway", async () => {
|
|
330
|
+
const documentLoader = createLoader({ [first]: person() });
|
|
331
|
+
assertInstanceOf(await lookupObject(`ap://${did}/actor`, {
|
|
332
|
+
...options,
|
|
333
|
+
documentLoader,
|
|
334
|
+
gateways: ["https://first.example"]
|
|
335
|
+
}), Person);
|
|
336
|
+
deepStrictEqual(documentLoader.fetched, [first]);
|
|
337
|
+
});
|
|
338
|
+
await t.step("tries several gateways in order", async () => {
|
|
339
|
+
const documentLoader = createLoader({ [second]: person() });
|
|
340
|
+
assertInstanceOf(await lookupObject(actorId, {
|
|
341
|
+
...options,
|
|
342
|
+
documentLoader,
|
|
343
|
+
gateways: ["https://first.example", new URL("https://second.example")]
|
|
344
|
+
}), Person);
|
|
345
|
+
deepStrictEqual(documentLoader.fetched, [first, second]);
|
|
346
|
+
});
|
|
347
|
+
await t.step("explicit gateways replace location hints", async () => {
|
|
348
|
+
const documentLoader = createLoader({
|
|
349
|
+
[second]: person(),
|
|
350
|
+
[hint]: person()
|
|
351
|
+
});
|
|
352
|
+
assertInstanceOf(await lookupObject(`ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`, {
|
|
353
|
+
...options,
|
|
354
|
+
documentLoader,
|
|
355
|
+
gateways: ["https://first.example", "https://second.example"]
|
|
356
|
+
}), Person);
|
|
357
|
+
deepStrictEqual(documentLoader.fetched, [first, second]);
|
|
358
|
+
});
|
|
359
|
+
await t.step("empty list uses a custom document loader", async () => {
|
|
360
|
+
const hintedId = `ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`;
|
|
361
|
+
const canonicalHintedId = `${actorId}?@gateway=https%3A%2F%2Fhint.example`;
|
|
362
|
+
const documentLoader = createLoader({
|
|
363
|
+
[canonicalHintedId]: person(),
|
|
364
|
+
[hint]: person()
|
|
365
|
+
});
|
|
366
|
+
const actor = await lookupObject(hintedId, {
|
|
367
|
+
...options,
|
|
368
|
+
documentLoader,
|
|
369
|
+
gateways: []
|
|
370
|
+
});
|
|
371
|
+
deepStrictEqual(documentLoader.fetched, [canonicalHintedId]);
|
|
372
|
+
assertInstanceOf(actor, Person);
|
|
373
|
+
});
|
|
374
|
+
await t.step("invalid gateway throws before fetching", async () => {
|
|
375
|
+
const documentLoader = createLoader({});
|
|
376
|
+
for (const verifyPortableObject of [void 0, createVerifier()]) await rejects(() => lookupObject(actorId, {
|
|
377
|
+
documentLoader,
|
|
378
|
+
contextLoader: mockDocumentLoader,
|
|
379
|
+
verifyPortableObject,
|
|
380
|
+
gateways: ["https://bad.example/path"]
|
|
381
|
+
}), TypeError);
|
|
382
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
383
|
+
});
|
|
384
|
+
await t.step("a malformed portable ID is refused first", async () => {
|
|
385
|
+
const documentLoader = createLoader({});
|
|
386
|
+
equal(await lookupObject(`ap://${did}/x/../actor`, {
|
|
387
|
+
...options,
|
|
388
|
+
documentLoader,
|
|
389
|
+
gateways: ["https://bad.example/path"]
|
|
390
|
+
}), null);
|
|
391
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
270
394
|
test("getActorHandle() with FEP-ef61 portable actors", {
|
|
271
395
|
sanitizeResources: false,
|
|
272
396
|
sanitizeOps: false
|
|
@@ -362,6 +486,62 @@ test("lookupObject() reports inferred gateways as hints", async () => {
|
|
|
362
486
|
deepStrictEqual(calls[0].gateways, void 0);
|
|
363
487
|
deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
|
|
364
488
|
});
|
|
489
|
+
test("lookupObject() reports explicit gateways to the verifier", async (t) => {
|
|
490
|
+
const calls = [];
|
|
491
|
+
const verifyPortableObject = async (_, options) => {
|
|
492
|
+
calls.push(options);
|
|
493
|
+
return { verified: true };
|
|
494
|
+
};
|
|
495
|
+
await t.step("bare ID with a limited explicit list", async () => {
|
|
496
|
+
const documentLoader = createLoader({ [`https://g4.example${gatewayPath}`]: person() });
|
|
497
|
+
assertInstanceOf(await lookupObject(actorId, {
|
|
498
|
+
documentLoader,
|
|
499
|
+
contextLoader: mockDocumentLoader,
|
|
500
|
+
verifyPortableObject,
|
|
501
|
+
gateways: Array.from({ length: 6 }, (_, i) => `https://g${i}.example`)
|
|
502
|
+
}), Person);
|
|
503
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
504
|
+
0,
|
|
505
|
+
1,
|
|
506
|
+
2,
|
|
507
|
+
3,
|
|
508
|
+
4
|
|
509
|
+
].map((i) => `https://g${i}.example${gatewayPath}`));
|
|
510
|
+
deepStrictEqual(calls.at(-1)?.gateways, [
|
|
511
|
+
0,
|
|
512
|
+
1,
|
|
513
|
+
2,
|
|
514
|
+
3,
|
|
515
|
+
4
|
|
516
|
+
].map((i) => new URL(`https://g${i}.example`)));
|
|
517
|
+
equal(calls.at(-1)?.gatewayHints, void 0);
|
|
518
|
+
});
|
|
519
|
+
await t.step("compatible origin stays first and is deduplicated", async () => {
|
|
520
|
+
const second = `https://second.example${gatewayPath}`;
|
|
521
|
+
const documentLoader = createLoader({ [second]: person() });
|
|
522
|
+
assertInstanceOf(await lookupObject(compatibleId, {
|
|
523
|
+
documentLoader,
|
|
524
|
+
contextLoader: mockDocumentLoader,
|
|
525
|
+
verifyPortableObject,
|
|
526
|
+
gateways: ["https://example.com", "https://second.example"]
|
|
527
|
+
}), Person);
|
|
528
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, second]);
|
|
529
|
+
deepStrictEqual(calls.at(-1)?.gateways, [new URL("https://example.com"), new URL("https://second.example")]);
|
|
530
|
+
equal(calls.at(-1)?.gatewayHints, void 0);
|
|
531
|
+
});
|
|
532
|
+
await t.step("empty list keeps the compatible origin", async () => {
|
|
533
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
534
|
+
assertInstanceOf(await lookupObject(compatibleId, {
|
|
535
|
+
documentLoader,
|
|
536
|
+
contextLoader: mockDocumentLoader,
|
|
537
|
+
verifyPortableObject,
|
|
538
|
+
gateways: []
|
|
539
|
+
}), Person);
|
|
540
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId]);
|
|
541
|
+
deepStrictEqual(calls.at(-1)?.gateways, []);
|
|
542
|
+
equal(calls.at(-1)?.gatewayHints, void 0);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
365
545
|
test("lookupObject() verifies fetched documents that stand for portable objects", async (t) => {
|
|
366
546
|
const plainUrl = "https://example.com/users/alice";
|
|
367
547
|
const redirectingLoader = (documentUrl, document) => {
|
|
@@ -394,6 +574,24 @@ test("lookupObject() verifies fetched documents that stand for portable objects"
|
|
|
394
574
|
deepStrictEqual(calls.length, 1);
|
|
395
575
|
deepStrictEqual(calls[0].documentUrl, new URL(compatibleId));
|
|
396
576
|
deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
|
|
577
|
+
const explicitCalls = [];
|
|
578
|
+
assertInstanceOf(await lookupObject(plainUrl, {
|
|
579
|
+
documentLoader,
|
|
580
|
+
contextLoader: mockDocumentLoader,
|
|
581
|
+
gateways: ["https://other.example"],
|
|
582
|
+
verifyPortableObject: async (_, options) => {
|
|
583
|
+
explicitCalls.push(options);
|
|
584
|
+
return { verified: true };
|
|
585
|
+
}
|
|
586
|
+
}), Person);
|
|
587
|
+
deepStrictEqual(explicitCalls[0].gateways, [new URL("https://other.example")]);
|
|
588
|
+
equal(explicitCalls[0].gatewayHints, void 0);
|
|
589
|
+
await rejects(() => lookupObject(plainUrl, {
|
|
590
|
+
documentLoader,
|
|
591
|
+
contextLoader: mockDocumentLoader,
|
|
592
|
+
verifyPortableObject: createVerifier(),
|
|
593
|
+
gateways: ["https://bad.example/path"]
|
|
594
|
+
}), TypeError);
|
|
397
595
|
equal(await lookupObject(plainUrl, {
|
|
398
596
|
documentLoader,
|
|
399
597
|
contextLoader: mockDocumentLoader,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "temporal-polyfill";
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
3
|
import { A as Object$1, E as Link, F as Person, K as Translation, L as PropertyValue, P as Organization, U as Service, a as Application, b as Group, k as Note, o as Article, q as Update, s as Collection, u as Create } from "./vocab-D3C791xF.mjs";
|
|
4
|
-
import "./actor-
|
|
5
|
-
import "./lookup-
|
|
4
|
+
import "./actor-C-Bvqy6v.mjs";
|
|
5
|
+
import "./lookup-DiU8zIYx.mjs";
|
|
6
6
|
import { test } from "@fedify/fixture";
|
|
7
7
|
import { deepStrictEqual, ok, rejects } from "node:assert/strict";
|
|
8
8
|
import { LanguageString as LanguageString$1, getDocumentLoader, preloadedContexts } from "@fedify/vocab-runtime";
|
|
@@ -411,6 +411,22 @@ test("fromJsonLd() handles portable ActivityPub IRIs", async () => {
|
|
|
411
411
|
deepStrictEqual(pageJson.next, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=2");
|
|
412
412
|
deepStrictEqual(pageJson.prev, "ap+ef61://did:key:z6Mkabc/actor/outbox?page=1");
|
|
413
413
|
});
|
|
414
|
+
test("fromJsonLd() refuses portable IDs changed by URL parsing", async () => {
|
|
415
|
+
const base = "ap://did:key:z6Mkabc";
|
|
416
|
+
for (const id of [
|
|
417
|
+
`${base}/a/../b`,
|
|
418
|
+
`${base}/a/%2e%2E/b`,
|
|
419
|
+
"https://gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b",
|
|
420
|
+
"https:/gw.example/.well-known/apgateway/did:key:z6Mkabc/a/../b"
|
|
421
|
+
]) await rejects(() => Note.fromJsonLd({
|
|
422
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
423
|
+
type: "Note",
|
|
424
|
+
id
|
|
425
|
+
}, {
|
|
426
|
+
documentLoader: mockDocumentLoader,
|
|
427
|
+
contextLoader: mockDocumentLoader
|
|
428
|
+
}), TypeError);
|
|
429
|
+
});
|
|
414
430
|
test("FEP-ef61: actor gateways round-trip as an ordered URI list", async () => {
|
|
415
431
|
const actorClasses = [
|
|
416
432
|
Application,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/vocab",
|
|
3
|
-
"version": "2.4.0-dev.
|
|
3
|
+
"version": "2.4.0-dev.2240+79613fa6",
|
|
4
4
|
"homepage": "https://fedify.dev/",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"jsonld": "^9.0.0",
|
|
44
44
|
"pkijs": "^3.3.3",
|
|
45
45
|
"temporal-polyfill": "^1.0.1",
|
|
46
|
-
"@fedify/vocab-tools": "2.4.0-dev.
|
|
47
|
-
"@fedify/vocab-runtime": "2.4.0-dev.
|
|
48
|
-
"@fedify/webfinger": "2.4.0-dev.
|
|
46
|
+
"@fedify/vocab-tools": "2.4.0-dev.2240+79613fa6",
|
|
47
|
+
"@fedify/vocab-runtime": "2.4.0-dev.2240+79613fa6",
|
|
48
|
+
"@fedify/webfinger": "2.4.0-dev.2240+79613fa6"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.12.2",
|
package/src/gateway.test.ts
CHANGED
|
@@ -1633,18 +1633,21 @@ test("accessors verify fetched documents that stand for portable objects", async
|
|
|
1633
1633
|
deepStrictEqual(object instanceof Note, expected);
|
|
1634
1634
|
}
|
|
1635
1635
|
// A final URL that URL parsing would turn into another ID is refused:
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1636
|
+
await rejects(
|
|
1637
|
+
() =>
|
|
1638
|
+
createHttpActivity(plainUrl).then((activity) =>
|
|
1639
|
+
activity.getObject({
|
|
1640
|
+
documentLoader: createRedirectingLoader({
|
|
1641
|
+
[plainUrl]: {
|
|
1642
|
+
documentUrl: `ap://${did}/objects/x/../1`,
|
|
1643
|
+
document: note(),
|
|
1644
|
+
},
|
|
1645
|
+
}),
|
|
1646
|
+
contextLoader: mockDocumentLoader,
|
|
1647
|
+
verifyPortableObject: createVerifier(),
|
|
1648
|
+
})
|
|
1649
|
+
),
|
|
1650
|
+
TypeError,
|
|
1648
1651
|
);
|
|
1649
1652
|
});
|
|
1650
1653
|
|
package/src/lookup.ts
CHANGED
|
@@ -185,6 +185,19 @@ export interface LookupObjectOptions {
|
|
|
185
185
|
*/
|
|
186
186
|
signal?: AbortSignal;
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The [FEP-ef61] gateways to try when looking up a portable
|
|
190
|
+
* (`ap:`/`ap+ef61:`) object, in order. Each must be an HTTP(S) origin.
|
|
191
|
+
* An explicit list replaces `@gateway` location hints, including when
|
|
192
|
+
* it is empty. The gateway named by a compatible identifier or the
|
|
193
|
+
* WebFinger server is still tried first. At most five gateways are
|
|
194
|
+
* requested per lookup, including those given here.
|
|
195
|
+
*
|
|
196
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
197
|
+
* @since 2.4.0
|
|
198
|
+
*/
|
|
199
|
+
gateways?: readonly (string | URL)[];
|
|
200
|
+
|
|
188
201
|
/**
|
|
189
202
|
* The [FEP-ef61] policy to apply to portable objects, typically
|
|
190
203
|
* `verifyPortableObject()` from `@fedify/fedify`, which
|
|
@@ -252,7 +265,9 @@ const MAX_PORTABLE_ATTEMPTS = 5;
|
|
|
252
265
|
*
|
|
253
266
|
* [FEP-ef61] portable objects, including portable actors found through
|
|
254
267
|
* WebFinger, are looked up only if the `verifyPortableObject` option is
|
|
255
|
-
* given; see {@link LookupObjectOptions.verifyPortableObject}.
|
|
268
|
+
* given; see {@link LookupObjectOptions.verifyPortableObject}. Pass
|
|
269
|
+
* {@link LookupObjectOptions.gateways} to look up a portable ID without
|
|
270
|
+
* `@gateway` location hints.
|
|
256
271
|
*
|
|
257
272
|
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
258
273
|
*
|
|
@@ -345,7 +360,13 @@ async function lookupObjectInternal(
|
|
|
345
360
|
if (candidate == null) return null;
|
|
346
361
|
return await lookupPortableObject(portable, candidate, undefined);
|
|
347
362
|
}
|
|
348
|
-
|
|
363
|
+
// Check the raw spelling before WHATWG URL can erase dot segments from a
|
|
364
|
+
// compatible identifier. It cannot be recovered from the parsed URL.
|
|
365
|
+
if (
|
|
366
|
+
options.verifyPortableObject != null &&
|
|
367
|
+
parseCompatibleEf61Reference(identifier) === null
|
|
368
|
+
) return null;
|
|
369
|
+
identifier = toAcctUrl(identifier) ?? parseIri(identifier);
|
|
349
370
|
}
|
|
350
371
|
if (isPortableIri(identifier)) {
|
|
351
372
|
let candidate: URL;
|
|
@@ -408,7 +429,9 @@ async function lookupObjectInternal(
|
|
|
408
429
|
const candidate = parsePortableCandidate(l.href);
|
|
409
430
|
if (candidate == null) continue;
|
|
410
431
|
const gateways = webFingerGateway == null ? [] : [webFingerGateway];
|
|
411
|
-
gateways
|
|
432
|
+
if (options.gateways == null) {
|
|
433
|
+
gateways.push(...getPortableGatewayCandidates(candidate));
|
|
434
|
+
}
|
|
412
435
|
const object = await lookupPortableObject(
|
|
413
436
|
portable,
|
|
414
437
|
candidate,
|
|
@@ -484,10 +507,14 @@ async function lookupObjectInternal(
|
|
|
484
507
|
);
|
|
485
508
|
return null;
|
|
486
509
|
} else if (claim != null) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
510
|
+
const explicit = getExplicitGateways(portable, claim.id);
|
|
511
|
+
return await dereference(
|
|
512
|
+
portable,
|
|
513
|
+
claim.id,
|
|
514
|
+
claim.inferredGateways,
|
|
515
|
+
explicit,
|
|
516
|
+
{ response: remoteDoc, contextLoader: snapshot.loader },
|
|
517
|
+
);
|
|
491
518
|
}
|
|
492
519
|
}
|
|
493
520
|
if (
|
|
@@ -530,6 +557,19 @@ interface PortableLookup {
|
|
|
530
557
|
readonly attempted: Set<string>;
|
|
531
558
|
/** The number of gateway requests left for this lookup. */
|
|
532
559
|
remaining: number;
|
|
560
|
+
/** Validated only when the lookup reaches a portable object. */
|
|
561
|
+
explicitGateways?: URL[];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function getExplicitGateways(
|
|
565
|
+
lookup: PortableLookup,
|
|
566
|
+
id: URL,
|
|
567
|
+
): URL[] | undefined {
|
|
568
|
+
if (lookup.options.gateways == null) return undefined;
|
|
569
|
+
return lookup.explicitGateways ??= getPortableGatewayCandidates(
|
|
570
|
+
id,
|
|
571
|
+
lookup.options.gateways,
|
|
572
|
+
);
|
|
533
573
|
}
|
|
534
574
|
|
|
535
575
|
/**
|
|
@@ -600,17 +640,18 @@ function getWebFingerGateway(identifier: URL): URL | undefined {
|
|
|
600
640
|
|
|
601
641
|
/**
|
|
602
642
|
* Looks up a portable object through FEP-ef61 gateways.
|
|
603
|
-
* @param
|
|
604
|
-
*
|
|
605
|
-
*
|
|
643
|
+
* @param inferredGateways Gateways inferred from a compatible identifier or
|
|
644
|
+
* WebFinger. If omitted, location hints in the IRI
|
|
645
|
+
* are used unless explicit gateways were given.
|
|
606
646
|
*/
|
|
607
647
|
async function lookupPortableObject(
|
|
608
648
|
lookup: PortableLookup,
|
|
609
649
|
id: URL,
|
|
610
|
-
|
|
650
|
+
inferredGateways: readonly URL[] | undefined,
|
|
611
651
|
): Promise<Object | null> {
|
|
612
652
|
const { options } = lookup;
|
|
613
653
|
const iri = formatIri(id);
|
|
654
|
+
const explicitGateways = getExplicitGateways(lookup, id);
|
|
614
655
|
if (options.verifyPortableObject == null) {
|
|
615
656
|
logger.debug(
|
|
616
657
|
"Cannot look up the portable object {iri}, as the " +
|
|
@@ -620,20 +661,27 @@ async function lookupPortableObject(
|
|
|
620
661
|
return null;
|
|
621
662
|
}
|
|
622
663
|
const canonicalId = canonicalizePortableUri(iri);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const key = `${canonicalId} `;
|
|
630
|
-
if (lookup.remaining < 1 || lookup.attempted.has(key)) return null;
|
|
631
|
-
lookup.attempted.add(key);
|
|
632
|
-
lookup.remaining--;
|
|
633
|
-
return await dereference(lookup, id, []);
|
|
664
|
+
const inferred = inferredGateways ??
|
|
665
|
+
(explicitGateways == null ? getPortableGatewayCandidates(id) : []);
|
|
666
|
+
const candidates = [...inferred];
|
|
667
|
+
for (const gateway of explicitGateways ?? []) {
|
|
668
|
+
if (!candidates.some((candidate) => candidate.href === gateway.href)) {
|
|
669
|
+
candidates.push(gateway);
|
|
634
670
|
}
|
|
635
|
-
}
|
|
636
|
-
|
|
671
|
+
}
|
|
672
|
+
if (candidates.length < 1 && inferredGateways == null) {
|
|
673
|
+
// No gateway to ask; a custom document loader may know how to
|
|
674
|
+
// retrieve the portable IRI itself:
|
|
675
|
+
const key = `${canonicalId} `;
|
|
676
|
+
if (lookup.remaining < 1 || lookup.attempted.has(key)) return null;
|
|
677
|
+
lookup.attempted.add(key);
|
|
678
|
+
lookup.remaining--;
|
|
679
|
+
return await dereference(
|
|
680
|
+
lookup,
|
|
681
|
+
id,
|
|
682
|
+
[],
|
|
683
|
+
explicitGateways == null ? undefined : [],
|
|
684
|
+
);
|
|
637
685
|
}
|
|
638
686
|
const selected: URL[] = [];
|
|
639
687
|
for (const gateway of candidates) {
|
|
@@ -645,13 +693,25 @@ async function lookupPortableObject(
|
|
|
645
693
|
}
|
|
646
694
|
if (selected.length < 1) return null;
|
|
647
695
|
lookup.remaining -= selected.length;
|
|
648
|
-
|
|
696
|
+
const selectedInferred = selected.filter((candidate) =>
|
|
697
|
+
inferred.some((gateway) => gateway.href === candidate.href)
|
|
698
|
+
);
|
|
699
|
+
const selectedExplicit = explicitGateways?.filter((gateway) =>
|
|
700
|
+
selected.some((candidate) => candidate.href === gateway.href)
|
|
701
|
+
);
|
|
702
|
+
return await dereference(
|
|
703
|
+
lookup,
|
|
704
|
+
id,
|
|
705
|
+
selectedInferred,
|
|
706
|
+
selectedExplicit,
|
|
707
|
+
);
|
|
649
708
|
}
|
|
650
709
|
|
|
651
710
|
async function dereference(
|
|
652
711
|
{ options, documentLoader }: PortableLookup,
|
|
653
712
|
id: URL,
|
|
654
|
-
|
|
713
|
+
inferredGateways: readonly URL[],
|
|
714
|
+
gateways?: readonly URL[],
|
|
655
715
|
extra: { response?: RemoteDocument; contextLoader?: DocumentLoader } = {},
|
|
656
716
|
): Promise<Object | null> {
|
|
657
717
|
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
@@ -661,10 +721,9 @@ async function dereference(
|
|
|
661
721
|
contextLoader: extra.contextLoader ?? options.contextLoader ??
|
|
662
722
|
getDocumentLoader({ userAgent: options.userAgent }),
|
|
663
723
|
tracerProvider,
|
|
664
|
-
//
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
inferredGateways: gateways,
|
|
724
|
+
// Preserve where the gateways came from for the verifier:
|
|
725
|
+
inferredGateways,
|
|
726
|
+
...(gateways == null ? {} : { gateways }),
|
|
668
727
|
response: extra.response,
|
|
669
728
|
verifyPortableObject: options.verifyPortableObject,
|
|
670
729
|
crossOrigin: options.crossOrigin === "throw" ? "throw" : "ignore",
|