@fedify/vocab 2.4.0-pr.934.40 → 2.4.0
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 +31713 -8698
- package/dist/mod.d.cts +4708 -7
- package/dist/mod.d.ts +4708 -7
- package/dist/mod.js +31606 -8598
- package/dist-tests/actor-D7_iX4I4.mjs +276 -0
- package/dist-tests/actor.test.mjs +55 -49
- package/dist-tests/application.yaml +15 -0
- package/dist-tests/audio.yaml +2 -1
- package/dist-tests/document.yaml +14 -1
- package/dist-tests/{actor-CnnyTI7i.mjs → esm-DP2Spt0u.mjs} +15 -211
- package/dist-tests/gateway.test.mjs +1906 -0
- package/dist-tests/group.yaml +15 -0
- package/dist-tests/image.yaml +2 -1
- package/dist-tests/link.yaml +14 -1
- package/dist-tests/lookup-IiNFWuXL.mjs +480 -0
- package/dist-tests/lookup.test.mjs +10 -268
- package/dist-tests/object.yaml +15 -0
- package/dist-tests/organization.yaml +15 -0
- package/dist-tests/page.yaml +2 -1
- package/dist-tests/person.yaml +15 -0
- package/dist-tests/portable-lookup.test.mjs +653 -0
- package/dist-tests/service.yaml +15 -0
- package/dist-tests/signed-representation.test.mjs +189 -0
- package/dist-tests/translation.test.mjs +404 -0
- package/dist-tests/translation.yaml +91 -0
- package/dist-tests/type.test.mjs +2 -2
- package/dist-tests/video.yaml +2 -1
- package/dist-tests/{vocab-OU5GYbNg.mjs → vocab-COZcxf00.mjs} +31227 -8501
- package/dist-tests/vocab.test.mjs +824 -6
- package/package.json +5 -5
- package/src/__snapshots__/vocab.test.ts.snap +345 -30
- package/src/actor.test.ts +41 -40
- package/src/actor.ts +120 -1
- package/src/application.yaml +15 -0
- package/src/audio.yaml +2 -1
- package/src/document.yaml +14 -1
- package/src/gateway.test.ts +2637 -0
- package/src/group.yaml +15 -0
- package/src/image.yaml +2 -1
- package/src/link.yaml +14 -1
- package/src/lookup.test.ts +6 -5
- package/src/lookup.ts +460 -33
- package/src/mod.ts +9 -0
- package/src/object.yaml +15 -0
- package/src/organization.yaml +15 -0
- package/src/page.yaml +2 -1
- package/src/person.yaml +15 -0
- package/src/portable-lookup.test.ts +889 -0
- package/src/preprocessors.ts +4 -2
- package/src/service.yaml +15 -0
- package/src/signed-representation.test.ts +277 -0
- package/src/translation.test.ts +518 -0
- package/src/translation.yaml +91 -0
- package/src/video.yaml +2 -1
- package/src/vocab.test.ts +1155 -2
- /package/dist-tests/{type-_BlsfT3S.mjs → type-BMdjm4Eg.mjs} +0 -0
- /package/dist-tests/{utils-CQjrpUkt.mjs → utils-Df2odC27.mjs} +0 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
globalThis.addEventListener = () => {};
|
|
3
|
+
import { F as Person } from "./vocab-COZcxf00.mjs";
|
|
4
|
+
import { t as esm_default } from "./esm-DP2Spt0u.mjs";
|
|
5
|
+
import { n as getActorHandle } from "./actor-D7_iX4I4.mjs";
|
|
6
|
+
import { t as lookupObject } from "./lookup-IiNFWuXL.mjs";
|
|
7
|
+
import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
|
|
8
|
+
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
9
|
+
import { deepStrictEqual, equal, rejects } from "node:assert/strict";
|
|
10
|
+
import { FetchError, parseIri } from "@fedify/vocab-runtime";
|
|
11
|
+
//#region src/portable-lookup.test.ts
|
|
12
|
+
const did = "did:key:z6Mkabc";
|
|
13
|
+
const actorId = `ap+ef61://${did}/actor`;
|
|
14
|
+
const gatewayPath = `/.well-known/apgateway/${did}/actor`;
|
|
15
|
+
const compatibleId = `https://example.com${gatewayPath}`;
|
|
16
|
+
const webFingerPrefix = "begin:https://example.com/.well-known/webfinger";
|
|
17
|
+
const AS_TYPE = "application/activity+json";
|
|
18
|
+
function person(id = `ap://${did}/actor`, extra = {}) {
|
|
19
|
+
return {
|
|
20
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
21
|
+
id,
|
|
22
|
+
type: "Person",
|
|
23
|
+
name: "Alice",
|
|
24
|
+
...extra
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createLoader(responses) {
|
|
28
|
+
const fetched = [];
|
|
29
|
+
const loader = async (url, options) => {
|
|
30
|
+
fetched.push(url);
|
|
31
|
+
options?.signal?.throwIfAborted();
|
|
32
|
+
const document = responses[url];
|
|
33
|
+
if (document == null) throw new FetchError(url, "HTTP 404", new globalThis.Response(null, { status: 404 }));
|
|
34
|
+
return {
|
|
35
|
+
contextUrl: null,
|
|
36
|
+
documentUrl: url,
|
|
37
|
+
document
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
return Object.assign(loader, { fetched });
|
|
41
|
+
}
|
|
42
|
+
function createVerifier(verified = true) {
|
|
43
|
+
const documents = [];
|
|
44
|
+
const verifier = async (document) => {
|
|
45
|
+
documents.push(document);
|
|
46
|
+
return { verified };
|
|
47
|
+
};
|
|
48
|
+
return Object.assign(verifier, { documents });
|
|
49
|
+
}
|
|
50
|
+
function selfLinks(...hrefs) {
|
|
51
|
+
return {
|
|
52
|
+
subject: "acct:alice@example.com",
|
|
53
|
+
links: hrefs.map((href) => ({
|
|
54
|
+
rel: "self",
|
|
55
|
+
type: AS_TYPE,
|
|
56
|
+
href
|
|
57
|
+
}))
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
test("lookupObject() with FEP-ef61 portable actors", {
|
|
61
|
+
sanitizeResources: false,
|
|
62
|
+
sanitizeOps: false
|
|
63
|
+
}, async (t) => {
|
|
64
|
+
esm_default.spyGlobal();
|
|
65
|
+
try {
|
|
66
|
+
await t.step("compatible self link, alias-free JRD", async () => {
|
|
67
|
+
esm_default.removeRoutes();
|
|
68
|
+
esm_default.get(webFingerPrefix, selfLinks(compatibleId));
|
|
69
|
+
const document = person();
|
|
70
|
+
const documentLoader = createLoader({ [compatibleId]: document });
|
|
71
|
+
const verifyPortableObject = createVerifier();
|
|
72
|
+
const actor = await lookupObject("@alice@example.com", {
|
|
73
|
+
documentLoader,
|
|
74
|
+
contextLoader: mockDocumentLoader,
|
|
75
|
+
verifyPortableObject
|
|
76
|
+
});
|
|
77
|
+
assertInstanceOf(actor, Person);
|
|
78
|
+
deepStrictEqual(actor.id, parseIri(actorId));
|
|
79
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId]);
|
|
80
|
+
deepStrictEqual(verifyPortableObject.documents, [document]);
|
|
81
|
+
});
|
|
82
|
+
await t.step("canonical self link asks the WebFinger host first", async () => {
|
|
83
|
+
esm_default.removeRoutes();
|
|
84
|
+
esm_default.get(webFingerPrefix, {
|
|
85
|
+
subject: "acct:alice@example.com",
|
|
86
|
+
links: [{
|
|
87
|
+
rel: "self",
|
|
88
|
+
type: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
|
89
|
+
href: `ap://${did}/actor?@gateway=https%3A%2F%2Fhint.example`
|
|
90
|
+
}]
|
|
91
|
+
});
|
|
92
|
+
const hinted = `https://hint.example${gatewayPath}`;
|
|
93
|
+
const documentLoader = createLoader({ [hinted]: person() });
|
|
94
|
+
assertInstanceOf(await lookupObject("@alice@example.com", {
|
|
95
|
+
documentLoader,
|
|
96
|
+
contextLoader: mockDocumentLoader,
|
|
97
|
+
verifyPortableObject: createVerifier()
|
|
98
|
+
}), Person);
|
|
99
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, hinted]);
|
|
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
|
+
});
|
|
131
|
+
await t.step("tries the next self link after a failure", async () => {
|
|
132
|
+
esm_default.removeRoutes();
|
|
133
|
+
const other = `https://other.example${gatewayPath}`;
|
|
134
|
+
esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`, compatibleId, other));
|
|
135
|
+
const documentLoader = createLoader({ [other]: person() });
|
|
136
|
+
assertInstanceOf(await lookupObject("@alice@example.com", {
|
|
137
|
+
documentLoader,
|
|
138
|
+
contextLoader: mockDocumentLoader,
|
|
139
|
+
verifyPortableObject: createVerifier()
|
|
140
|
+
}), Person);
|
|
141
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, other]);
|
|
142
|
+
});
|
|
143
|
+
await t.step("accepts a compatible identifier as the ID", async () => {
|
|
144
|
+
esm_default.removeRoutes();
|
|
145
|
+
esm_default.get(webFingerPrefix, selfLinks(compatibleId));
|
|
146
|
+
const document = person(compatibleId);
|
|
147
|
+
const documentLoader = createLoader({ [compatibleId]: document });
|
|
148
|
+
const verifyPortableObject = createVerifier();
|
|
149
|
+
const actor = await lookupObject("@alice@example.com", {
|
|
150
|
+
documentLoader,
|
|
151
|
+
contextLoader: mockDocumentLoader,
|
|
152
|
+
verifyPortableObject
|
|
153
|
+
});
|
|
154
|
+
assertInstanceOf(actor, Person);
|
|
155
|
+
deepStrictEqual(actor.id, new URL(compatibleId));
|
|
156
|
+
deepStrictEqual(verifyPortableObject.documents, [document]);
|
|
157
|
+
});
|
|
158
|
+
await t.step("rejects objects that fail the checks", async () => {
|
|
159
|
+
esm_default.removeRoutes();
|
|
160
|
+
esm_default.get(webFingerPrefix, selfLinks(compatibleId));
|
|
161
|
+
for (const [document, verified] of [
|
|
162
|
+
[person(), false],
|
|
163
|
+
[person(`ap://${did}/other`), true],
|
|
164
|
+
[person(`https://example.com/.well-known/apgateway/${did}/other`), true],
|
|
165
|
+
[person(compatibleId), false]
|
|
166
|
+
]) {
|
|
167
|
+
const documentLoader = createLoader({ [compatibleId]: document });
|
|
168
|
+
const verifyPortableObject = createVerifier(verified);
|
|
169
|
+
equal(await lookupObject("@alice@example.com", {
|
|
170
|
+
documentLoader,
|
|
171
|
+
contextLoader: mockDocumentLoader,
|
|
172
|
+
verifyPortableObject
|
|
173
|
+
}), null);
|
|
174
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId]);
|
|
175
|
+
await rejects(() => lookupObject("@alice@example.com", {
|
|
176
|
+
documentLoader,
|
|
177
|
+
contextLoader: mockDocumentLoader,
|
|
178
|
+
verifyPortableObject,
|
|
179
|
+
crossOrigin: "throw"
|
|
180
|
+
}), { name: "PortableObjectRejectedError" });
|
|
181
|
+
equal(await lookupObject("@alice@example.com", {
|
|
182
|
+
documentLoader,
|
|
183
|
+
contextLoader: mockDocumentLoader,
|
|
184
|
+
verifyPortableObject,
|
|
185
|
+
crossOrigin: "trust"
|
|
186
|
+
}), null);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
await t.step("without verifyPortableObject", async () => {
|
|
190
|
+
esm_default.removeRoutes();
|
|
191
|
+
esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`, compatibleId));
|
|
192
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
193
|
+
for (const crossOrigin of ["ignore", "trust"]) equal(await lookupObject("@alice@example.com", {
|
|
194
|
+
documentLoader,
|
|
195
|
+
contextLoader: mockDocumentLoader,
|
|
196
|
+
crossOrigin
|
|
197
|
+
}), null);
|
|
198
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, compatibleId]);
|
|
199
|
+
await rejects(() => lookupObject("@alice@example.com", {
|
|
200
|
+
documentLoader,
|
|
201
|
+
contextLoader: mockDocumentLoader,
|
|
202
|
+
crossOrigin: "throw"
|
|
203
|
+
}), /has a different origin than the document URL/);
|
|
204
|
+
const actor = await lookupObject("@alice@example.com", {
|
|
205
|
+
documentLoader: createLoader({ [compatibleId]: person(compatibleId) }),
|
|
206
|
+
contextLoader: mockDocumentLoader
|
|
207
|
+
});
|
|
208
|
+
assertInstanceOf(actor, Person);
|
|
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]);
|
|
217
|
+
});
|
|
218
|
+
await t.step("refuses portable IRIs changed by URL parsing", async () => {
|
|
219
|
+
esm_default.removeRoutes();
|
|
220
|
+
esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/x/../actor`, `ap://${did}/x/%2e%2e/actor`));
|
|
221
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
222
|
+
const options = {
|
|
223
|
+
documentLoader,
|
|
224
|
+
contextLoader: mockDocumentLoader,
|
|
225
|
+
verifyPortableObject: createVerifier()
|
|
226
|
+
};
|
|
227
|
+
equal(await lookupObject("@alice@example.com", options), null);
|
|
228
|
+
equal(await lookupObject(`ap://${did}/x/../actor?@gateway=https%3A%2F%2Fexample.com`, options), null);
|
|
229
|
+
deepStrictEqual(documentLoader.fetched, []);
|
|
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
|
+
});
|
|
245
|
+
await t.step("bounds gateway requests", async () => {
|
|
246
|
+
esm_default.removeRoutes();
|
|
247
|
+
const hints = Array.from({ length: 5 }, (_, i) => `@gateway=https%3A%2F%2Fg${i}.example`).join("&");
|
|
248
|
+
esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor?${hints}`, `ap://${did}/actor2?${hints}`, `https://g9.example/.well-known/apgateway/${did}/actor`));
|
|
249
|
+
const documentLoader = createLoader({});
|
|
250
|
+
equal(await lookupObject("@alice@example.com", {
|
|
251
|
+
documentLoader,
|
|
252
|
+
contextLoader: mockDocumentLoader,
|
|
253
|
+
verifyPortableObject: createVerifier()
|
|
254
|
+
}), null);
|
|
255
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId, ...[
|
|
256
|
+
0,
|
|
257
|
+
1,
|
|
258
|
+
2,
|
|
259
|
+
3
|
|
260
|
+
].map((i) => `https://g${i}.example${gatewayPath}`)]);
|
|
261
|
+
});
|
|
262
|
+
await t.step("cancellation", async () => {
|
|
263
|
+
esm_default.removeRoutes();
|
|
264
|
+
esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`, `https://other.example${gatewayPath}`));
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const verifyPortableObject = () => {
|
|
267
|
+
controller.abort();
|
|
268
|
+
return Promise.resolve({ verified: true });
|
|
269
|
+
};
|
|
270
|
+
const documentLoader = createLoader({ [compatibleId]: person() });
|
|
271
|
+
equal(await lookupObject("@alice@example.com", {
|
|
272
|
+
documentLoader,
|
|
273
|
+
contextLoader: mockDocumentLoader,
|
|
274
|
+
verifyPortableObject,
|
|
275
|
+
signal: controller.signal
|
|
276
|
+
}), null);
|
|
277
|
+
deepStrictEqual(documentLoader.fetched, [compatibleId]);
|
|
278
|
+
});
|
|
279
|
+
} finally {
|
|
280
|
+
esm_default.removeRoutes();
|
|
281
|
+
esm_default.hardReset();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
test("lookupObject() looks up portable identifiers directly", async () => {
|
|
285
|
+
const verifyPortableObject = createVerifier();
|
|
286
|
+
const hinted = `https://gw.example${gatewayPath}`;
|
|
287
|
+
const documentLoader = createLoader({
|
|
288
|
+
[hinted]: person(),
|
|
289
|
+
[`https://example.com${gatewayPath}`]: person(),
|
|
290
|
+
[actorId]: person()
|
|
291
|
+
});
|
|
292
|
+
const options = {
|
|
293
|
+
documentLoader,
|
|
294
|
+
contextLoader: mockDocumentLoader,
|
|
295
|
+
verifyPortableObject
|
|
296
|
+
};
|
|
297
|
+
for (const identifier of [
|
|
298
|
+
`ap://${did}/actor?@gateway=https%3A%2F%2Fgw.example`,
|
|
299
|
+
compatibleId,
|
|
300
|
+
new URL(compatibleId),
|
|
301
|
+
`ap://${did}/actor`,
|
|
302
|
+
parseIri(`ap://${did}/actor`)
|
|
303
|
+
]) {
|
|
304
|
+
const actor = await lookupObject(identifier, options);
|
|
305
|
+
assertInstanceOf(actor, Person);
|
|
306
|
+
deepStrictEqual(actor.id, parseIri(actorId));
|
|
307
|
+
}
|
|
308
|
+
deepStrictEqual(documentLoader.fetched, [
|
|
309
|
+
hinted,
|
|
310
|
+
compatibleId,
|
|
311
|
+
compatibleId,
|
|
312
|
+
actorId,
|
|
313
|
+
actorId
|
|
314
|
+
]);
|
|
315
|
+
equal(await lookupObject(`ap://${did}/actor`, {
|
|
316
|
+
documentLoader,
|
|
317
|
+
contextLoader: mockDocumentLoader
|
|
318
|
+
}), null);
|
|
319
|
+
deepStrictEqual(documentLoader.fetched.length, 5);
|
|
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
|
+
});
|
|
394
|
+
test("getActorHandle() with FEP-ef61 portable actors", {
|
|
395
|
+
sanitizeResources: false,
|
|
396
|
+
sanitizeOps: false
|
|
397
|
+
}, async (t) => {
|
|
398
|
+
const actor = new Person({
|
|
399
|
+
id: parseIri(actorId),
|
|
400
|
+
preferredUsername: "alice",
|
|
401
|
+
gateways: [new URL("https://example.com"), new URL("https://example.org")]
|
|
402
|
+
});
|
|
403
|
+
esm_default.spyGlobal();
|
|
404
|
+
try {
|
|
405
|
+
await t.step("verified through the first gateway", async () => {
|
|
406
|
+
esm_default.removeRoutes();
|
|
407
|
+
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Aalice%40example.com", selfLinks(compatibleId));
|
|
408
|
+
deepStrictEqual(await getActorHandle(actor), "@alice@example.com");
|
|
409
|
+
});
|
|
410
|
+
await t.step("canonical self link", async () => {
|
|
411
|
+
esm_default.removeRoutes();
|
|
412
|
+
esm_default.get(webFingerPrefix, selfLinks(`ap://${did}/actor`));
|
|
413
|
+
deepStrictEqual(await getActorHandle(actor), "@alice@example.com");
|
|
414
|
+
});
|
|
415
|
+
await t.step("follows a verified subject", async () => {
|
|
416
|
+
esm_default.removeRoutes();
|
|
417
|
+
esm_default.get(webFingerPrefix, {
|
|
418
|
+
...selfLinks(compatibleId),
|
|
419
|
+
subject: "acct:alice@example.org"
|
|
420
|
+
});
|
|
421
|
+
esm_default.get("begin:https://example.org/.well-known/webfinger", {
|
|
422
|
+
...selfLinks(compatibleId),
|
|
423
|
+
subject: "acct:alice@example.org"
|
|
424
|
+
});
|
|
425
|
+
deepStrictEqual(await getActorHandle(actor), "@alice@example.org");
|
|
426
|
+
esm_default.removeRoutes();
|
|
427
|
+
esm_default.get(webFingerPrefix, {
|
|
428
|
+
...selfLinks(compatibleId),
|
|
429
|
+
subject: "acct:alice@example.org"
|
|
430
|
+
});
|
|
431
|
+
esm_default.get("begin:https://example.org/.well-known/webfinger", selfLinks(`ap://${did}/other`));
|
|
432
|
+
deepStrictEqual(await getActorHandle(actor), "@alice@example.com");
|
|
433
|
+
});
|
|
434
|
+
await t.step("rejects a WebFinger response for another actor", async () => {
|
|
435
|
+
for (const jrd of [
|
|
436
|
+
selfLinks(`https://example.com/.well-known/apgateway/${did}/other`),
|
|
437
|
+
selfLinks("https://example.com/users/alice"),
|
|
438
|
+
{
|
|
439
|
+
subject: "acct:alice@example.com",
|
|
440
|
+
links: []
|
|
441
|
+
}
|
|
442
|
+
]) {
|
|
443
|
+
esm_default.removeRoutes();
|
|
444
|
+
esm_default.get(webFingerPrefix, jrd);
|
|
445
|
+
await rejects(() => getActorHandle(actor), TypeError);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
await t.step("does not fall back when WebFinger fails", async () => {
|
|
449
|
+
esm_default.removeRoutes();
|
|
450
|
+
esm_default.get(webFingerPrefix, 404);
|
|
451
|
+
await rejects(() => getActorHandle(actor), TypeError);
|
|
452
|
+
});
|
|
453
|
+
await t.step("not enough information", async () => {
|
|
454
|
+
esm_default.removeRoutes();
|
|
455
|
+
esm_default.clearHistory();
|
|
456
|
+
for (const target of [
|
|
457
|
+
parseIri(actorId),
|
|
458
|
+
new Person({
|
|
459
|
+
id: parseIri(actorId),
|
|
460
|
+
preferredUsername: "alice"
|
|
461
|
+
}),
|
|
462
|
+
new Person({
|
|
463
|
+
id: parseIri(actorId),
|
|
464
|
+
gateways: [new URL("https://example.com")]
|
|
465
|
+
})
|
|
466
|
+
]) await rejects(() => getActorHandle(target), TypeError);
|
|
467
|
+
deepStrictEqual(esm_default.callHistory.calls().length, 0);
|
|
468
|
+
});
|
|
469
|
+
} finally {
|
|
470
|
+
esm_default.removeRoutes();
|
|
471
|
+
esm_default.hardReset();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
test("lookupObject() reports inferred gateways as hints", async () => {
|
|
475
|
+
const calls = [];
|
|
476
|
+
const verifyPortableObject = async (_, options) => {
|
|
477
|
+
calls.push(options);
|
|
478
|
+
return { verified: true };
|
|
479
|
+
};
|
|
480
|
+
assertInstanceOf(await lookupObject(compatibleId, {
|
|
481
|
+
documentLoader: createLoader({ [compatibleId]: person() }),
|
|
482
|
+
contextLoader: mockDocumentLoader,
|
|
483
|
+
verifyPortableObject
|
|
484
|
+
}), Person);
|
|
485
|
+
deepStrictEqual(calls.length, 1);
|
|
486
|
+
deepStrictEqual(calls[0].gateways, void 0);
|
|
487
|
+
deepStrictEqual(calls[0].gatewayHints, [new URL("https://example.com")]);
|
|
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
|
+
});
|
|
545
|
+
test("lookupObject() verifies fetched documents that stand for portable objects", async (t) => {
|
|
546
|
+
const plainUrl = "https://example.com/users/alice";
|
|
547
|
+
const redirectingLoader = (documentUrl, document) => {
|
|
548
|
+
const fetched = [];
|
|
549
|
+
const loader = async (url) => {
|
|
550
|
+
fetched.push(url);
|
|
551
|
+
if (url !== plainUrl) throw new FetchError(url, "HTTP 404", new globalThis.Response(null, { status: 404 }));
|
|
552
|
+
return {
|
|
553
|
+
contextUrl: null,
|
|
554
|
+
documentUrl,
|
|
555
|
+
document
|
|
556
|
+
};
|
|
557
|
+
};
|
|
558
|
+
return Object.assign(loader, { fetched });
|
|
559
|
+
};
|
|
560
|
+
await t.step("a redirect to a compatible identifier", async () => {
|
|
561
|
+
const calls = [];
|
|
562
|
+
const documentLoader = redirectingLoader(compatibleId, person());
|
|
563
|
+
const actor = await lookupObject(plainUrl, {
|
|
564
|
+
documentLoader,
|
|
565
|
+
contextLoader: mockDocumentLoader,
|
|
566
|
+
verifyPortableObject: async (_, options) => {
|
|
567
|
+
calls.push(options);
|
|
568
|
+
return { verified: true };
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
assertInstanceOf(actor, Person);
|
|
572
|
+
deepStrictEqual(actor.id, parseIri(actorId));
|
|
573
|
+
deepStrictEqual(documentLoader.fetched, [plainUrl]);
|
|
574
|
+
deepStrictEqual(calls.length, 1);
|
|
575
|
+
deepStrictEqual(calls[0].documentUrl, new URL(compatibleId));
|
|
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);
|
|
595
|
+
equal(await lookupObject(plainUrl, {
|
|
596
|
+
documentLoader,
|
|
597
|
+
contextLoader: mockDocumentLoader,
|
|
598
|
+
verifyPortableObject: createVerifier(false)
|
|
599
|
+
}), null);
|
|
600
|
+
equal(await lookupObject(plainUrl, {
|
|
601
|
+
documentLoader: redirectingLoader(compatibleId, person(`ap://${did}/other`)),
|
|
602
|
+
contextLoader: mockDocumentLoader,
|
|
603
|
+
verifyPortableObject: createVerifier()
|
|
604
|
+
}), null);
|
|
605
|
+
});
|
|
606
|
+
await t.step("a compatible @id", async () => {
|
|
607
|
+
const document = person(compatibleId);
|
|
608
|
+
const documentLoader = redirectingLoader(plainUrl, document);
|
|
609
|
+
const verifyPortableObject = createVerifier();
|
|
610
|
+
const verified = await lookupObject(plainUrl, {
|
|
611
|
+
documentLoader,
|
|
612
|
+
contextLoader: mockDocumentLoader,
|
|
613
|
+
verifyPortableObject
|
|
614
|
+
});
|
|
615
|
+
assertInstanceOf(verified, Person);
|
|
616
|
+
deepStrictEqual(verified.id, new URL(compatibleId));
|
|
617
|
+
deepStrictEqual(verifyPortableObject.documents, [document]);
|
|
618
|
+
equal(await lookupObject(plainUrl, {
|
|
619
|
+
documentLoader,
|
|
620
|
+
contextLoader: mockDocumentLoader,
|
|
621
|
+
verifyPortableObject: createVerifier(false)
|
|
622
|
+
}), null);
|
|
623
|
+
const actor = await lookupObject(plainUrl, {
|
|
624
|
+
documentLoader,
|
|
625
|
+
contextLoader: mockDocumentLoader
|
|
626
|
+
});
|
|
627
|
+
assertInstanceOf(actor, Person);
|
|
628
|
+
deepStrictEqual(actor.id, new URL(compatibleId));
|
|
629
|
+
});
|
|
630
|
+
await t.step("a portable @id", async () => {
|
|
631
|
+
const documentLoader = redirectingLoader(plainUrl, person());
|
|
632
|
+
const actor = await lookupObject(plainUrl, {
|
|
633
|
+
documentLoader,
|
|
634
|
+
contextLoader: mockDocumentLoader,
|
|
635
|
+
verifyPortableObject: createVerifier()
|
|
636
|
+
});
|
|
637
|
+
assertInstanceOf(actor, Person);
|
|
638
|
+
deepStrictEqual(actor.id, parseIri(actorId));
|
|
639
|
+
equal(await lookupObject(plainUrl, {
|
|
640
|
+
documentLoader,
|
|
641
|
+
contextLoader: mockDocumentLoader,
|
|
642
|
+
verifyPortableObject: createVerifier(false)
|
|
643
|
+
}), null);
|
|
644
|
+
await rejects(() => lookupObject(plainUrl, {
|
|
645
|
+
documentLoader,
|
|
646
|
+
contextLoader: mockDocumentLoader,
|
|
647
|
+
verifyPortableObject: createVerifier(false),
|
|
648
|
+
crossOrigin: "throw"
|
|
649
|
+
}), /No gateway returned a valid portable object/);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
//#endregion
|
|
653
|
+
export {};
|
package/dist-tests/service.yaml
CHANGED
|
@@ -7,6 +7,7 @@ entity: true
|
|
|
7
7
|
description: Represents a service of any kind.
|
|
8
8
|
defaultContext:
|
|
9
9
|
- "https://www.w3.org/ns/activitystreams"
|
|
10
|
+
- "https://w3id.org/fep/ef61"
|
|
10
11
|
- "https://w3id.org/security/v1"
|
|
11
12
|
- "https://w3id.org/security/data-integrity/v1"
|
|
12
13
|
- "https://www.w3.org/ns/did/v1"
|
|
@@ -78,6 +79,20 @@ properties:
|
|
|
78
79
|
range:
|
|
79
80
|
- "https://w3id.org/security#Multikey"
|
|
80
81
|
|
|
82
|
+
- pluralName: gateways
|
|
83
|
+
singularName: gateway
|
|
84
|
+
singularAccessor: true
|
|
85
|
+
compactName: gateways
|
|
86
|
+
uri: "https://w3id.org/fep/ef61/gateways"
|
|
87
|
+
container: list
|
|
88
|
+
description: |
|
|
89
|
+
Gateways where the latest version of this portable actor object can be
|
|
90
|
+
retrieved.
|
|
91
|
+
|
|
92
|
+
See [FEP-ef61](https://w3id.org/fep/ef61) for details.
|
|
93
|
+
range:
|
|
94
|
+
- "fedify:gatewayUrl"
|
|
95
|
+
|
|
81
96
|
- singularName: manuallyApprovesFollowers
|
|
82
97
|
functional: true
|
|
83
98
|
compactName: manuallyApprovesFollowers
|