@fedify/vocab 2.5.0-dev.2446 → 2.5.0-dev.2492
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 +115 -5
- package/dist/mod.d.cts +110 -1
- package/dist/mod.d.ts +110 -1
- package/dist/mod.js +116 -7
- package/dist-tests/{actor-C89Bex4F.mjs → actor-BjUFWXfi.mjs} +6 -4
- package/dist-tests/actor.test.mjs +4 -3
- package/dist-tests/{esm-DtAfYdqR.mjs → esm-BIqRhjGA.mjs} +1 -1
- package/dist-tests/gateway.test.mjs +4 -4
- package/dist-tests/license.test.mjs +2 -2
- package/dist-tests/{lookup-BehNGq4X.mjs → lookup-BQqIkZ5j.mjs} +38 -6
- package/dist-tests/lookup.test.mjs +134 -15
- package/dist-tests/portable-lookup.test.mjs +52 -13
- package/dist-tests/rolldown-runtime-BWJg4WE8.mjs +37 -0
- package/dist-tests/signed-representation.test.mjs +1 -1
- package/dist-tests/{src-fpZm9WyF.mjs → src-DQiQQv6h.mjs} +3 -3
- package/dist-tests/translation.test.mjs +2 -2
- package/dist-tests/type.test.mjs +1 -1
- package/dist-tests/undici-D88PC0XH.mjs +22898 -0
- package/dist-tests/{vocab-jHKm_LJ6.mjs → vocab-BMEyHiOV.mjs} +77 -34
- package/dist-tests/vocab.test.mjs +6064 -8
- package/package.json +5 -5
- package/src/actor.ts +4 -0
- package/src/gateway.test.ts +1 -1
- package/src/lookup.test.ts +161 -12
- package/src/lookup.ts +67 -7
- package/src/portable-lookup.test.ts +72 -7
- package/src/vocab.test.ts +40 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/vocab",
|
|
3
|
-
"version": "2.5.0-dev.
|
|
3
|
+
"version": "2.5.0-dev.2492+4648c04e",
|
|
4
4
|
"homepage": "https://fedify.dev/",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,15 +37,15 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@multiformats/base-x": "^4.0.1",
|
|
39
39
|
"@logtape/logtape": "^2.2.0",
|
|
40
|
-
"@opentelemetry/api": "
|
|
40
|
+
"@opentelemetry/api": "~1.9.1",
|
|
41
41
|
"asn1js": "^3.0.10",
|
|
42
42
|
"es-toolkit": "1.46.1",
|
|
43
43
|
"jsonld": "^9.0.0",
|
|
44
44
|
"pkijs": "^3.3.3",
|
|
45
45
|
"temporal-polyfill": "^1.0.1",
|
|
46
|
-
"@fedify/
|
|
47
|
-
"@fedify/vocab-
|
|
48
|
-
"@fedify/
|
|
46
|
+
"@fedify/vocab-tools": "2.5.0-dev.2492+4648c04e",
|
|
47
|
+
"@fedify/vocab-runtime": "2.5.0-dev.2492+4648c04e",
|
|
48
|
+
"@fedify/webfinger": "2.5.0-dev.2492+4648c04e"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.12.2",
|
package/src/actor.ts
CHANGED
|
@@ -394,6 +394,8 @@ async function getPortableActorHandle(
|
|
|
394
394
|
userAgent: options.userAgent,
|
|
395
395
|
tracerProvider: options.tracerProvider,
|
|
396
396
|
meterProvider: options.meterProvider,
|
|
397
|
+
allowPrivateAddress: "allowPrivateAddress" in options &&
|
|
398
|
+
options.allowPrivateAddress === true,
|
|
397
399
|
});
|
|
398
400
|
// Unlike an ordinary actor, whose ID vouches for its host, a portable
|
|
399
401
|
// actor's gateways are claimed by the actor itself, so there is no
|
|
@@ -409,6 +411,8 @@ async function getPortableActorHandle(
|
|
|
409
411
|
userAgent: options.userAgent,
|
|
410
412
|
tracerProvider: options.tracerProvider,
|
|
411
413
|
meterProvider: options.meterProvider,
|
|
414
|
+
allowPrivateAddress: "allowPrivateAddress" in options &&
|
|
415
|
+
options.allowPrivateAddress === true,
|
|
412
416
|
});
|
|
413
417
|
if (
|
|
414
418
|
subjectResult != null && linksToPortableActor(subjectResult, canonicalId)
|
package/src/gateway.test.ts
CHANGED
|
@@ -876,7 +876,7 @@ test("getObject() sends the ActivityStreams Accept header to gateways", {
|
|
|
876
876
|
});
|
|
877
877
|
try {
|
|
878
878
|
const object = await (await createActivity()).getObject({
|
|
879
|
-
documentLoader: getDocumentLoader(),
|
|
879
|
+
documentLoader: getDocumentLoader({ allowPrivateAddress: true }),
|
|
880
880
|
contextLoader: mockDocumentLoader,
|
|
881
881
|
gateways: ["https://example.com"],
|
|
882
882
|
verifyPortableObject: createVerifier(),
|
package/src/lookup.test.ts
CHANGED
|
@@ -6,14 +6,29 @@ import {
|
|
|
6
6
|
} from "@fedify/fixture";
|
|
7
7
|
import fetchMock from "fetch-mock";
|
|
8
8
|
import { deepStrictEqual, equal, ok, rejects } from "node:assert/strict";
|
|
9
|
-
import {
|
|
9
|
+
import type {
|
|
10
|
+
DocumentLoader,
|
|
11
|
+
PortableObjectVerifier,
|
|
12
|
+
} from "@fedify/vocab-runtime";
|
|
13
|
+
import type {
|
|
14
|
+
DereferenceCandidate,
|
|
15
|
+
DereferenceFailure,
|
|
16
|
+
} from "@fedify/vocab-runtime/internal/portable-dereference";
|
|
17
|
+
import { lookupObject, lookupObserver, traverseCollection } from "./lookup.ts";
|
|
10
18
|
import { assertInstanceOf } from "./utils.ts";
|
|
11
19
|
import { Collection, Note, Object, Person } from "./vocab.ts";
|
|
12
20
|
|
|
21
|
+
// lookupObject() accepts this option without declaring it, since Fedify's
|
|
22
|
+
// Context passes it. It makes WebFinger lookups use the global fetch()
|
|
23
|
+
// (fetch-mock here) without resolving hostnames, so that the tests do not
|
|
24
|
+
// depend on the real DNS:
|
|
25
|
+
const allowPrivateAddress = { allowPrivateAddress: true };
|
|
26
|
+
|
|
13
27
|
test("lookupObject()", {
|
|
14
28
|
sanitizeResources: false,
|
|
15
29
|
sanitizeOps: false,
|
|
16
30
|
}, async (t) => {
|
|
31
|
+
const responseTimers: ReturnType<typeof setTimeout>[] = [];
|
|
17
32
|
fetchMock.spyGlobal();
|
|
18
33
|
|
|
19
34
|
fetchMock.get(
|
|
@@ -43,6 +58,7 @@ test("lookupObject()", {
|
|
|
43
58
|
const options = {
|
|
44
59
|
documentLoader: mockDocumentLoader,
|
|
45
60
|
contextLoader: mockDocumentLoader,
|
|
61
|
+
...allowPrivateAddress,
|
|
46
62
|
};
|
|
47
63
|
|
|
48
64
|
await t.step("actor", async () => {
|
|
@@ -106,7 +122,7 @@ test("lookupObject()", {
|
|
|
106
122
|
"begin:https://example.com/.well-known/webfinger",
|
|
107
123
|
() =>
|
|
108
124
|
new Promise((resolve) => {
|
|
109
|
-
setTimeout(() => {
|
|
125
|
+
responseTimers.push(setTimeout(() => {
|
|
110
126
|
resolve({
|
|
111
127
|
subject: "acct:johndoe@example.com",
|
|
112
128
|
links: [
|
|
@@ -117,7 +133,7 @@ test("lookupObject()", {
|
|
|
117
133
|
},
|
|
118
134
|
],
|
|
119
135
|
});
|
|
120
|
-
}, 1000);
|
|
136
|
+
}, 1000));
|
|
121
137
|
}),
|
|
122
138
|
);
|
|
123
139
|
|
|
@@ -162,7 +178,7 @@ test("lookupObject()", {
|
|
|
162
178
|
"begin:https://example.com/.well-known/webfinger",
|
|
163
179
|
() =>
|
|
164
180
|
new Promise((resolve) => {
|
|
165
|
-
setTimeout(() => {
|
|
181
|
+
responseTimers.push(setTimeout(() => {
|
|
166
182
|
resolve({
|
|
167
183
|
subject: "acct:johndoe@example.com",
|
|
168
184
|
links: [
|
|
@@ -173,7 +189,7 @@ test("lookupObject()", {
|
|
|
173
189
|
},
|
|
174
190
|
],
|
|
175
191
|
});
|
|
176
|
-
}, 500);
|
|
192
|
+
}, 500));
|
|
177
193
|
}),
|
|
178
194
|
);
|
|
179
195
|
|
|
@@ -193,7 +209,7 @@ test("lookupObject()", {
|
|
|
193
209
|
"https://example.com/slow-object",
|
|
194
210
|
() =>
|
|
195
211
|
new Promise((resolve) => {
|
|
196
|
-
setTimeout(() => {
|
|
212
|
+
responseTimers.push(setTimeout(() => {
|
|
197
213
|
resolve({
|
|
198
214
|
status: 200,
|
|
199
215
|
headers: { "Content-Type": "application/activity+json" },
|
|
@@ -203,7 +219,7 @@ test("lookupObject()", {
|
|
|
203
219
|
content: "Slow response",
|
|
204
220
|
},
|
|
205
221
|
});
|
|
206
|
-
}, 1000);
|
|
222
|
+
}, 1000));
|
|
207
223
|
}),
|
|
208
224
|
);
|
|
209
225
|
|
|
@@ -218,6 +234,7 @@ test("lookupObject()", {
|
|
|
218
234
|
deepStrictEqual(await promise, null);
|
|
219
235
|
});
|
|
220
236
|
|
|
237
|
+
for (const timer of responseTimers) clearTimeout(timer);
|
|
221
238
|
fetchMock.hardReset();
|
|
222
239
|
fetchMock.removeRoutes();
|
|
223
240
|
});
|
|
@@ -601,6 +618,7 @@ test("FEP-fe34: lookupObject() cross-origin security", {
|
|
|
601
618
|
|
|
602
619
|
// Default behavior should return null
|
|
603
620
|
const result1 = await lookupObject("@user@example.com", {
|
|
621
|
+
...allowPrivateAddress,
|
|
604
622
|
documentLoader: webfingerDocumentLoader,
|
|
605
623
|
contextLoader: mockDocumentLoader,
|
|
606
624
|
});
|
|
@@ -610,6 +628,7 @@ test("FEP-fe34: lookupObject() cross-origin security", {
|
|
|
610
628
|
await rejects(
|
|
611
629
|
() =>
|
|
612
630
|
lookupObject("@user@example.com", {
|
|
631
|
+
...allowPrivateAddress,
|
|
613
632
|
documentLoader: webfingerDocumentLoader,
|
|
614
633
|
contextLoader: mockDocumentLoader,
|
|
615
634
|
crossOrigin: "throw",
|
|
@@ -620,6 +639,7 @@ test("FEP-fe34: lookupObject() cross-origin security", {
|
|
|
620
639
|
|
|
621
640
|
// With crossOrigin: trust, should return the object
|
|
622
641
|
const result2 = await lookupObject("@user@example.com", {
|
|
642
|
+
...allowPrivateAddress,
|
|
623
643
|
documentLoader: webfingerDocumentLoader,
|
|
624
644
|
contextLoader: mockDocumentLoader,
|
|
625
645
|
crossOrigin: "trust",
|
|
@@ -719,12 +739,22 @@ test("FEP-fe34: lookupObject() cross-origin security", {
|
|
|
719
739
|
throw new Error("Network error");
|
|
720
740
|
};
|
|
721
741
|
|
|
742
|
+
// The WebFinger fallback must not reach the network:
|
|
743
|
+
fetchMock.spyGlobal();
|
|
744
|
+
fetchMock.get("begin:https://example.com/.well-known/webfinger", 404);
|
|
745
|
+
|
|
722
746
|
// Network errors should not be confused with cross-origin errors
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
747
|
+
let result;
|
|
748
|
+
try {
|
|
749
|
+
result = await lookupObject("https://example.com/note", {
|
|
750
|
+
...allowPrivateAddress,
|
|
751
|
+
documentLoader: errorDocumentLoader,
|
|
752
|
+
contextLoader: mockDocumentLoader,
|
|
753
|
+
crossOrigin: "throw",
|
|
754
|
+
});
|
|
755
|
+
} finally {
|
|
756
|
+
fetchMock.hardReset();
|
|
757
|
+
}
|
|
728
758
|
|
|
729
759
|
// Should return null because the document loader failed,
|
|
730
760
|
// not because of cross-origin policy
|
|
@@ -842,6 +872,7 @@ test("lookupObject() records activitypub.object.lookup counter", {
|
|
|
842
872
|
await t.step("records kind=actor for an actor result", async () => {
|
|
843
873
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
844
874
|
const person = await lookupObject("@johndoe@example.com", {
|
|
875
|
+
...allowPrivateAddress,
|
|
845
876
|
documentLoader: mockDocumentLoader,
|
|
846
877
|
contextLoader: mockDocumentLoader,
|
|
847
878
|
meterProvider,
|
|
@@ -928,6 +959,7 @@ test("lookupObject() records activitypub.object.lookup counter", {
|
|
|
928
959
|
});
|
|
929
960
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
930
961
|
const result = await lookupObject("janedoe@example.com", {
|
|
962
|
+
...allowPrivateAddress,
|
|
931
963
|
documentLoader: mockDocumentLoader,
|
|
932
964
|
contextLoader: mockDocumentLoader,
|
|
933
965
|
meterProvider,
|
|
@@ -979,6 +1011,7 @@ test("lookupObject() records activitypub.object.lookup counter", {
|
|
|
979
1011
|
);
|
|
980
1012
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
981
1013
|
await lookupObject("@johndoe@example.com", {
|
|
1014
|
+
...allowPrivateAddress,
|
|
982
1015
|
documentLoader: mockDocumentLoader,
|
|
983
1016
|
contextLoader: mockDocumentLoader,
|
|
984
1017
|
meterProvider,
|
|
@@ -1012,6 +1045,7 @@ test("lookupObject() records activitypub.object.lookup counter", {
|
|
|
1012
1045
|
});
|
|
1013
1046
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1014
1047
|
await lookupObject(new URL("acct:johndoe@example.com"), {
|
|
1048
|
+
...allowPrivateAddress,
|
|
1015
1049
|
documentLoader: mockDocumentLoader,
|
|
1016
1050
|
contextLoader: mockDocumentLoader,
|
|
1017
1051
|
meterProvider,
|
|
@@ -1051,3 +1085,118 @@ test("lookupObject() records activitypub.object.lookup counter", {
|
|
|
1051
1085
|
});
|
|
1052
1086
|
|
|
1053
1087
|
// cSpell: ignore gildong
|
|
1088
|
+
|
|
1089
|
+
const OBSERVER_DID = "did:key:z6MkhVPuyvgG1RkMv67azDqDCDERPXVrUg1i3qchXY5EACE3";
|
|
1090
|
+
|
|
1091
|
+
test("lookupObject() offers each candidate to the observer", async () => {
|
|
1092
|
+
const note = {
|
|
1093
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1094
|
+
id: "https://example.com/notes/1",
|
|
1095
|
+
type: "Note",
|
|
1096
|
+
content: "Hello",
|
|
1097
|
+
};
|
|
1098
|
+
const fetched: string[] = [];
|
|
1099
|
+
const documentLoader: DocumentLoader = (url) => {
|
|
1100
|
+
fetched.push(url);
|
|
1101
|
+
if (url === "https://example.com/notes/1") {
|
|
1102
|
+
return Promise.resolve({
|
|
1103
|
+
contextUrl: null,
|
|
1104
|
+
document: note,
|
|
1105
|
+
documentUrl: "https://example.com/notes/1?redirected",
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
return Promise.reject(new Error(`Not found: ${url}`));
|
|
1109
|
+
};
|
|
1110
|
+
const candidates: DereferenceCandidate<Object>[] = [];
|
|
1111
|
+
const failures: DereferenceFailure[] = [];
|
|
1112
|
+
const accepted = await lookupObject("https://example.com/notes/1", {
|
|
1113
|
+
documentLoader,
|
|
1114
|
+
contextLoader: mockDocumentLoader,
|
|
1115
|
+
[lookupObserver]: {
|
|
1116
|
+
accept: (candidate) => {
|
|
1117
|
+
candidates.push(candidate);
|
|
1118
|
+
return Promise.resolve(true);
|
|
1119
|
+
},
|
|
1120
|
+
failed: (failure) => failures.push(failure),
|
|
1121
|
+
},
|
|
1122
|
+
});
|
|
1123
|
+
ok(accepted instanceof Note);
|
|
1124
|
+
equal(candidates.length, 1);
|
|
1125
|
+
deepStrictEqual(candidates[0].document, note);
|
|
1126
|
+
equal(candidates[0].documentUrl, "https://example.com/notes/1?redirected");
|
|
1127
|
+
equal(candidates[0].baseUrl.href, "https://example.com/notes/1?redirected");
|
|
1128
|
+
equal(candidates[0].object, accepted);
|
|
1129
|
+
deepStrictEqual(failures, []);
|
|
1130
|
+
|
|
1131
|
+
// A refused candidate is not returned, and neither is an observer error:
|
|
1132
|
+
for (
|
|
1133
|
+
const accept of [
|
|
1134
|
+
() => Promise.resolve(false),
|
|
1135
|
+
() => Promise.reject(new Error("Oops")),
|
|
1136
|
+
]
|
|
1137
|
+
) {
|
|
1138
|
+
const refusals: DereferenceFailure[] = [];
|
|
1139
|
+
equal(
|
|
1140
|
+
await lookupObject("https://example.com/notes/1", {
|
|
1141
|
+
documentLoader,
|
|
1142
|
+
contextLoader: mockDocumentLoader,
|
|
1143
|
+
[lookupObserver]: { accept, failed: (f) => refusals.push(f) },
|
|
1144
|
+
}),
|
|
1145
|
+
null,
|
|
1146
|
+
);
|
|
1147
|
+
deepStrictEqual(refusals.map((f) => f.operation), ["accept"]);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Without WebFinger, a URL that cannot be fetched is not looked up further:
|
|
1151
|
+
fetched.length = 0;
|
|
1152
|
+
const missing: DereferenceFailure[] = [];
|
|
1153
|
+
equal(
|
|
1154
|
+
await lookupObject("https://example.com/notes/2", {
|
|
1155
|
+
documentLoader,
|
|
1156
|
+
contextLoader: mockDocumentLoader,
|
|
1157
|
+
[lookupObserver]: { webFinger: false, failed: (f) => missing.push(f) },
|
|
1158
|
+
}),
|
|
1159
|
+
null,
|
|
1160
|
+
);
|
|
1161
|
+
deepStrictEqual(fetched, ["https://example.com/notes/2"]);
|
|
1162
|
+
deepStrictEqual(missing.map((f) => f.operation), ["fetch"]);
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
test("lookupObject() keeps looking after the observer refuses a portable candidate", async () => {
|
|
1166
|
+
const id = `ap://${OBSERVER_DID}/notes/1`;
|
|
1167
|
+
const gateways = ["https://gw1.example", "https://gw2.example"];
|
|
1168
|
+
const documentLoader: DocumentLoader = (url) =>
|
|
1169
|
+
Promise.resolve({
|
|
1170
|
+
contextUrl: null,
|
|
1171
|
+
document: {
|
|
1172
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1173
|
+
id,
|
|
1174
|
+
type: "Note",
|
|
1175
|
+
content: url.startsWith("https://gw1.example/") ? "first" : "second",
|
|
1176
|
+
},
|
|
1177
|
+
documentUrl: url,
|
|
1178
|
+
});
|
|
1179
|
+
const verifier: PortableObjectVerifier = () =>
|
|
1180
|
+
Promise.resolve({ verified: true });
|
|
1181
|
+
const offered: string[] = [];
|
|
1182
|
+
const failures: DereferenceFailure[] = [];
|
|
1183
|
+
const object = await lookupObject(id, {
|
|
1184
|
+
documentLoader,
|
|
1185
|
+
contextLoader: mockDocumentLoader,
|
|
1186
|
+
gateways,
|
|
1187
|
+
verifyPortableObject: verifier,
|
|
1188
|
+
[lookupObserver]: {
|
|
1189
|
+
accept: (candidate) => {
|
|
1190
|
+
offered.push(candidate.documentUrl);
|
|
1191
|
+
return Promise.resolve(offered.length > 1);
|
|
1192
|
+
},
|
|
1193
|
+
failed: (failure) => failures.push(failure),
|
|
1194
|
+
},
|
|
1195
|
+
});
|
|
1196
|
+
ok(object instanceof Note);
|
|
1197
|
+
equal(object.content, "second");
|
|
1198
|
+
equal(offered.length, 2);
|
|
1199
|
+
ok(offered[0].startsWith("https://gw1.example/"));
|
|
1200
|
+
ok(offered[1].startsWith("https://gw2.example/"));
|
|
1201
|
+
deepStrictEqual(failures.map((f) => f.operation), ["accept"]);
|
|
1202
|
+
});
|
package/src/lookup.ts
CHANGED
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
type RemoteDocument,
|
|
14
14
|
} from "@fedify/vocab-runtime";
|
|
15
15
|
import {
|
|
16
|
+
acceptCandidate,
|
|
16
17
|
createSnapshotContextLoader,
|
|
18
|
+
type DereferenceObserver,
|
|
17
19
|
dereferencePortableIri,
|
|
18
20
|
getPortableGatewayCandidates,
|
|
19
21
|
getPortableResponseClaim,
|
|
@@ -123,6 +125,38 @@ const logger = getLogger(["fedify", "vocab", "lookup"]);
|
|
|
123
125
|
*
|
|
124
126
|
* @since 0.2.0
|
|
125
127
|
*/
|
|
128
|
+
/**
|
|
129
|
+
* The key of the {@link LookupObjectOptions} property that observes how
|
|
130
|
+
* {@link lookupObject} selects the object it returns.
|
|
131
|
+
*
|
|
132
|
+
* @internal Technically exported for other Fedify packages, but not part of
|
|
133
|
+
* the public API contract. This is not considered public API for Semantic
|
|
134
|
+
* Versioning decisions.
|
|
135
|
+
*/
|
|
136
|
+
export const lookupObserver: unique symbol = Symbol.for(
|
|
137
|
+
"@fedify/vocab/lookupObserver",
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Observes how {@link lookupObject} selects the object it returns: each
|
|
142
|
+
* candidate is offered to {@link DereferenceObserver.accept} at the boundary
|
|
143
|
+
* that would otherwise return it, both for ordinary documents and for
|
|
144
|
+
* FEP-ef61 portable objects, and each failure that does not propagate is
|
|
145
|
+
* reported to {@link DereferenceObserver.failed}. The observer is not passed
|
|
146
|
+
* on to the returned object.
|
|
147
|
+
*
|
|
148
|
+
* @internal Technically exported for other Fedify packages, but not part of
|
|
149
|
+
* the public API contract. This is not considered public API for Semantic
|
|
150
|
+
* Versioning decisions.
|
|
151
|
+
*/
|
|
152
|
+
export interface LookupObserver extends DereferenceObserver<Object> {
|
|
153
|
+
/**
|
|
154
|
+
* Whether to fall back to WebFinger when an HTTP(S) URL cannot be fetched.
|
|
155
|
+
* Defaults to `true`.
|
|
156
|
+
*/
|
|
157
|
+
readonly webFinger?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
126
160
|
export interface LookupObjectOptions {
|
|
127
161
|
/**
|
|
128
162
|
* The document loader for loading remote JSON-LD documents.
|
|
@@ -226,6 +260,12 @@ export interface LookupObjectOptions {
|
|
|
226
260
|
* @since 2.4.0
|
|
227
261
|
*/
|
|
228
262
|
verifyPortableObject?: PortableObjectVerifier;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* See {@link LookupObserver}.
|
|
266
|
+
* @internal
|
|
267
|
+
*/
|
|
268
|
+
[lookupObserver]?: LookupObserver;
|
|
229
269
|
}
|
|
230
270
|
|
|
231
271
|
/**
|
|
@@ -395,8 +435,12 @@ async function lookupObjectInternal(
|
|
|
395
435
|
});
|
|
396
436
|
} catch (error) {
|
|
397
437
|
logger.debug("Failed to fetch remote document:\n{error}", { error });
|
|
438
|
+
options[lookupObserver]?.failed?.({ operation: "fetch", error });
|
|
398
439
|
}
|
|
399
440
|
}
|
|
441
|
+
if (remoteDoc == null && options[lookupObserver]?.webFinger === false) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
400
444
|
if (remoteDoc == null) {
|
|
401
445
|
const jrd = await lookupWebFinger(identifier, {
|
|
402
446
|
userAgent: options.userAgent,
|
|
@@ -467,12 +511,13 @@ async function lookupObjectInternal(
|
|
|
467
511
|
if (remoteDoc == null) return null;
|
|
468
512
|
// With verifyPortableObject, the document may turn out to be a portable
|
|
469
513
|
// object, which has to be verified with the same context documents:
|
|
470
|
-
const snapshot =
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
514
|
+
const snapshot =
|
|
515
|
+
options.verifyPortableObject == null && options[lookupObserver] == null
|
|
516
|
+
? null
|
|
517
|
+
: createSnapshotContextLoader(
|
|
518
|
+
options.contextLoader ??
|
|
519
|
+
getDocumentLoader({ userAgent: options.userAgent }),
|
|
520
|
+
);
|
|
476
521
|
try {
|
|
477
522
|
let object: Object;
|
|
478
523
|
let documentUrl: URL;
|
|
@@ -486,6 +531,7 @@ async function lookupObjectInternal(
|
|
|
486
531
|
baseUrl: documentUrl,
|
|
487
532
|
});
|
|
488
533
|
} catch (error) {
|
|
534
|
+
options[lookupObserver]?.failed?.({ operation: "parse", error });
|
|
489
535
|
if (error instanceof TypeError) {
|
|
490
536
|
logger.debug(
|
|
491
537
|
"Failed to parse JSON-LD document: {error}\n{document}",
|
|
@@ -495,7 +541,7 @@ async function lookupObjectInternal(
|
|
|
495
541
|
}
|
|
496
542
|
throw error;
|
|
497
543
|
}
|
|
498
|
-
if (snapshot != null) {
|
|
544
|
+
if (options.verifyPortableObject != null && snapshot != null) {
|
|
499
545
|
// A document whose final URL or @id stands for a portable object is
|
|
500
546
|
// verified as one instead of being trusted because of its origin:
|
|
501
547
|
const claim = getPortableResponseClaim(remoteDoc.documentUrl, object.id);
|
|
@@ -540,6 +586,19 @@ async function lookupObjectInternal(
|
|
|
540
586
|
'implications, set the crossOrigin option to "trust".',
|
|
541
587
|
{ ...remoteDoc, objectId: object.id.href },
|
|
542
588
|
);
|
|
589
|
+
options[lookupObserver]?.failed?.({ operation: "origin" });
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
if (
|
|
593
|
+
!await acceptCandidate(options[lookupObserver], {
|
|
594
|
+
document: remoteDoc.document,
|
|
595
|
+
documentUrl: remoteDoc.documentUrl,
|
|
596
|
+
baseUrl: documentUrl,
|
|
597
|
+
contextLoader: snapshot?.loader ?? options.contextLoader ??
|
|
598
|
+
getDocumentLoader({ userAgent: options.userAgent }),
|
|
599
|
+
object,
|
|
600
|
+
})
|
|
601
|
+
) {
|
|
543
602
|
return null;
|
|
544
603
|
}
|
|
545
604
|
return object;
|
|
@@ -728,6 +787,7 @@ async function dereference(
|
|
|
728
787
|
verifyPortableObject: options.verifyPortableObject,
|
|
729
788
|
crossOrigin: options.crossOrigin === "throw" ? "throw" : "ignore",
|
|
730
789
|
signal: options.signal,
|
|
790
|
+
observer: options[lookupObserver],
|
|
731
791
|
parse: (document, { contextLoader, baseUrl }) =>
|
|
732
792
|
Object.fromJsonLd(document, {
|
|
733
793
|
documentLoader,
|