@fedify/vocab 2.0.0-dev.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/LICENSE +20 -0
- package/deno.json +31 -0
- package/dist/accept.yaml +15 -0
- package/dist/activity.yaml +98 -0
- package/dist/actor.test.d.ts +2 -0
- package/dist/actor.test.js +6095 -0
- package/dist/add.yaml +16 -0
- package/dist/announce.yaml +30 -0
- package/dist/application.yaml +324 -0
- package/dist/arrive.yaml +15 -0
- package/dist/article.yaml +46 -0
- package/dist/audio.yaml +11 -0
- package/dist/block.yaml +16 -0
- package/dist/chatmessage.yaml +50 -0
- package/dist/collection.yaml +154 -0
- package/dist/collectionpage.yaml +55 -0
- package/dist/create.yaml +28 -0
- package/dist/dataintegrityproof.yaml +56 -0
- package/dist/delete.yaml +27 -0
- package/dist/deno-B-ypIMwF.js +1282 -0
- package/dist/didservice.yaml +22 -0
- package/dist/dislike.yaml +14 -0
- package/dist/document.yaml +31 -0
- package/dist/emoji.yaml +12 -0
- package/dist/emojireact.yaml +17 -0
- package/dist/endpoints.yaml +85 -0
- package/dist/event.yaml +11 -0
- package/dist/export.yaml +9 -0
- package/dist/flag.yaml +15 -0
- package/dist/follow.yaml +19 -0
- package/dist/group.yaml +324 -0
- package/dist/hashtag.yaml +14 -0
- package/dist/ignore.yaml +14 -0
- package/dist/image.yaml +9 -0
- package/dist/intransitiveactivity.yaml +15 -0
- package/dist/invite.yaml +14 -0
- package/dist/join.yaml +14 -0
- package/dist/key.yaml +28 -0
- package/dist/leave.yaml +14 -0
- package/dist/like.yaml +16 -0
- package/dist/link.yaml +101 -0
- package/dist/listen.yaml +12 -0
- package/dist/lookup.test.d.ts +2 -0
- package/dist/lookup.test.js +690 -0
- package/dist/mention.yaml +9 -0
- package/dist/mod.cjs +42036 -0
- package/dist/mod.d.cts +15329 -0
- package/dist/mod.d.ts +15330 -0
- package/dist/mod.js +41936 -0
- package/dist/move.yaml +15 -0
- package/dist/multikey.yaml +36 -0
- package/dist/note.yaml +48 -0
- package/dist/object.yaml +404 -0
- package/dist/offer.yaml +15 -0
- package/dist/orderedcollection.yaml +39 -0
- package/dist/orderedcollectionpage.yaml +50 -0
- package/dist/organization.yaml +324 -0
- package/dist/page.yaml +11 -0
- package/dist/person.yaml +324 -0
- package/dist/place.yaml +75 -0
- package/dist/profile.yaml +26 -0
- package/dist/propertyvalue.yaml +32 -0
- package/dist/question.yaml +103 -0
- package/dist/read.yaml +13 -0
- package/dist/reject.yaml +14 -0
- package/dist/relationship.yaml +52 -0
- package/dist/remove.yaml +14 -0
- package/dist/service.yaml +324 -0
- package/dist/source.yaml +26 -0
- package/dist/tentativeaccept.yaml +14 -0
- package/dist/tentativereject.yaml +14 -0
- package/dist/tombstone.yaml +24 -0
- package/dist/travel.yaml +16 -0
- package/dist/type-CNuABalk.js +13 -0
- package/dist/type.test.d.ts +2 -0
- package/dist/type.test.js +24 -0
- package/dist/undo.yaml +26 -0
- package/dist/update.yaml +58 -0
- package/dist/utils-BSWXlrig.js +13 -0
- package/dist/video.yaml +11 -0
- package/dist/view.yaml +13 -0
- package/dist/vocab-DBispxj5.js +41603 -0
- package/dist/vocab.test.d.ts +2 -0
- package/dist/vocab.test.js +1304 -0
- package/package.json +79 -0
- package/scripts/codegen.ts +20 -0
- package/src/__snapshots__/vocab.test.ts.snap +7903 -0
- package/src/accept.yaml +15 -0
- package/src/activity.yaml +98 -0
- package/src/actor.test.ts +263 -0
- package/src/actor.ts +293 -0
- package/src/add.yaml +16 -0
- package/src/announce.yaml +30 -0
- package/src/application.yaml +324 -0
- package/src/arrive.yaml +15 -0
- package/src/article.yaml +46 -0
- package/src/audio.yaml +11 -0
- package/src/block.yaml +16 -0
- package/src/chatmessage.yaml +50 -0
- package/src/collection.yaml +154 -0
- package/src/collectionpage.yaml +55 -0
- package/src/constants.ts +11 -0
- package/src/create.yaml +28 -0
- package/src/dataintegrityproof.yaml +56 -0
- package/src/delete.yaml +27 -0
- package/src/didservice.yaml +22 -0
- package/src/dislike.yaml +14 -0
- package/src/document.yaml +31 -0
- package/src/emoji.yaml +12 -0
- package/src/emojireact.yaml +17 -0
- package/src/endpoints.yaml +85 -0
- package/src/event.yaml +11 -0
- package/src/export.yaml +9 -0
- package/src/flag.yaml +15 -0
- package/src/follow.yaml +19 -0
- package/src/group.yaml +324 -0
- package/src/handle.ts +104 -0
- package/src/hashtag.yaml +14 -0
- package/src/ignore.yaml +14 -0
- package/src/image.yaml +9 -0
- package/src/intransitiveactivity.yaml +15 -0
- package/src/invite.yaml +14 -0
- package/src/join.yaml +14 -0
- package/src/key.yaml +28 -0
- package/src/keys.ts +50 -0
- package/src/leave.yaml +14 -0
- package/src/like.yaml +16 -0
- package/src/link.yaml +101 -0
- package/src/listen.yaml +12 -0
- package/src/lookup.test.ts +681 -0
- package/src/lookup.ts +318 -0
- package/src/mention.yaml +9 -0
- package/src/mod.ts +57 -0
- package/src/move.yaml +15 -0
- package/src/multikey.yaml +36 -0
- package/src/note.yaml +48 -0
- package/src/object.yaml +404 -0
- package/src/offer.yaml +15 -0
- package/src/orderedcollection.yaml +39 -0
- package/src/orderedcollectionpage.yaml +50 -0
- package/src/organization.yaml +324 -0
- package/src/page.yaml +11 -0
- package/src/person.yaml +324 -0
- package/src/place.yaml +75 -0
- package/src/profile.yaml +26 -0
- package/src/propertyvalue.yaml +32 -0
- package/src/question.yaml +103 -0
- package/src/read.yaml +13 -0
- package/src/reject.yaml +14 -0
- package/src/relationship.yaml +52 -0
- package/src/remove.yaml +14 -0
- package/src/service.yaml +324 -0
- package/src/source.yaml +26 -0
- package/src/tentativeaccept.yaml +14 -0
- package/src/tentativereject.yaml +14 -0
- package/src/tombstone.yaml +24 -0
- package/src/travel.yaml +16 -0
- package/src/type.test.ts +20 -0
- package/src/type.ts +102 -0
- package/src/undo.yaml +26 -0
- package/src/update.yaml +58 -0
- package/src/utils.ts +9 -0
- package/src/video.yaml +11 -0
- package/src/view.yaml +13 -0
- package/src/vocab.bench.ts +204 -0
- package/src/vocab.test.ts +2014 -0
- package/tsdown.config.ts +65 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTestTracerProvider,
|
|
3
|
+
mockDocumentLoader,
|
|
4
|
+
test,
|
|
5
|
+
} from "@fedify/fixture";
|
|
6
|
+
import fetchMock from "fetch-mock";
|
|
7
|
+
import { deepStrictEqual, equal, ok, rejects } from "node:assert/strict";
|
|
8
|
+
import { lookupObject, traverseCollection } from "./lookup.ts";
|
|
9
|
+
import { assertInstanceOf } from "./utils.ts";
|
|
10
|
+
import { Collection, Note, Object, Person } from "./vocab.ts";
|
|
11
|
+
|
|
12
|
+
test("lookupObject()", {
|
|
13
|
+
sanitizeResources: false,
|
|
14
|
+
sanitizeOps: false,
|
|
15
|
+
}, async (t) => {
|
|
16
|
+
fetchMock.spyGlobal();
|
|
17
|
+
|
|
18
|
+
fetchMock.get(
|
|
19
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
20
|
+
{
|
|
21
|
+
subject: "acct:johndoe@example.com",
|
|
22
|
+
links: [
|
|
23
|
+
{
|
|
24
|
+
rel: "alternate",
|
|
25
|
+
href: "https://example.com/object",
|
|
26
|
+
type: "application/activity+json",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
rel: "self",
|
|
30
|
+
href: "https://example.com/html/person",
|
|
31
|
+
type: "text/html",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
rel: "self",
|
|
35
|
+
href: "https://example.com/person",
|
|
36
|
+
type: "application/activity+json",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const options = {
|
|
43
|
+
documentLoader: mockDocumentLoader,
|
|
44
|
+
contextLoader: mockDocumentLoader,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
await t.step("actor", async () => {
|
|
48
|
+
const person = await lookupObject("@johndoe@example.com", options);
|
|
49
|
+
assertInstanceOf(person, Person);
|
|
50
|
+
deepStrictEqual(person.id, new URL("https://example.com/person"));
|
|
51
|
+
equal(person.name, "John Doe");
|
|
52
|
+
const person2 = await lookupObject("johndoe@example.com", options);
|
|
53
|
+
deepStrictEqual(person2, person);
|
|
54
|
+
const person3 = await lookupObject("acct:johndoe@example.com", options);
|
|
55
|
+
deepStrictEqual(person3, person);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await t.step("object", async () => {
|
|
59
|
+
const object = await lookupObject("https://example.com/object", options);
|
|
60
|
+
assertInstanceOf(object, Object);
|
|
61
|
+
deepStrictEqual(
|
|
62
|
+
object,
|
|
63
|
+
new Object({
|
|
64
|
+
id: new URL("https://example.com/object"),
|
|
65
|
+
name: "Fetched object",
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
const person = await lookupObject(
|
|
69
|
+
"https://example.com/hong-gildong",
|
|
70
|
+
options,
|
|
71
|
+
);
|
|
72
|
+
assertInstanceOf(person, Person);
|
|
73
|
+
deepStrictEqual(
|
|
74
|
+
person,
|
|
75
|
+
new Person({
|
|
76
|
+
id: new URL("https://example.com/hong-gildong"),
|
|
77
|
+
name: "Hong Gildong",
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
fetchMock.removeRoutes();
|
|
83
|
+
fetchMock.get("begin:https://example.com/.well-known/webfinger", {
|
|
84
|
+
subject: "acct:janedoe@example.com",
|
|
85
|
+
links: [
|
|
86
|
+
{
|
|
87
|
+
rel: "self",
|
|
88
|
+
href: "https://example.com/404",
|
|
89
|
+
type: "application/activity+json",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await t.step("not found", async () => {
|
|
95
|
+
deepStrictEqual(await lookupObject("janedoe@example.com", options), null);
|
|
96
|
+
deepStrictEqual(
|
|
97
|
+
await lookupObject("https://example.com/404", options),
|
|
98
|
+
null,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
fetchMock.removeRoutes();
|
|
103
|
+
fetchMock.get(
|
|
104
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
105
|
+
() =>
|
|
106
|
+
new Promise((resolve) => {
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
resolve({
|
|
109
|
+
subject: "acct:johndoe@example.com",
|
|
110
|
+
links: [
|
|
111
|
+
{
|
|
112
|
+
rel: "self",
|
|
113
|
+
href: "https://example.com/person",
|
|
114
|
+
type: "application/activity+json",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
}, 1000);
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
await t.step("request cancellation", async () => {
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const promise = lookupObject("johndoe@example.com", {
|
|
125
|
+
...options,
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
controller.abort();
|
|
130
|
+
deepStrictEqual(await promise, null);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
fetchMock.removeRoutes();
|
|
134
|
+
fetchMock.get(
|
|
135
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
136
|
+
{
|
|
137
|
+
subject: "acct:johndoe@example.com",
|
|
138
|
+
links: [
|
|
139
|
+
{
|
|
140
|
+
rel: "self",
|
|
141
|
+
href: "https://example.com/person",
|
|
142
|
+
type: "application/activity+json",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await t.step("successful request with signal", async () => {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const person = await lookupObject("johndoe@example.com", {
|
|
151
|
+
...options,
|
|
152
|
+
signal: controller.signal,
|
|
153
|
+
});
|
|
154
|
+
assertInstanceOf(person, Person);
|
|
155
|
+
deepStrictEqual(person.id, new URL("https://example.com/person"));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
fetchMock.removeRoutes();
|
|
159
|
+
fetchMock.get(
|
|
160
|
+
"begin:https://example.com/.well-known/webfinger",
|
|
161
|
+
() =>
|
|
162
|
+
new Promise((resolve) => {
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
resolve({
|
|
165
|
+
subject: "acct:johndoe@example.com",
|
|
166
|
+
links: [
|
|
167
|
+
{
|
|
168
|
+
rel: "self",
|
|
169
|
+
href: "https://example.com/person",
|
|
170
|
+
type: "application/activity+json",
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
}, 500);
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
await t.step("cancellation with immediate abort", async () => {
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
controller.abort();
|
|
181
|
+
|
|
182
|
+
const result = await lookupObject("johndoe@example.com", {
|
|
183
|
+
...options,
|
|
184
|
+
signal: controller.signal,
|
|
185
|
+
});
|
|
186
|
+
deepStrictEqual(result, null);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
fetchMock.removeRoutes();
|
|
190
|
+
fetchMock.get(
|
|
191
|
+
"https://example.com/slow-object",
|
|
192
|
+
() =>
|
|
193
|
+
new Promise((resolve) => {
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
resolve({
|
|
196
|
+
status: 200,
|
|
197
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
198
|
+
body: {
|
|
199
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
200
|
+
type: "Note",
|
|
201
|
+
content: "Slow response",
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}, 1000);
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await t.step("direct object fetch cancellation", async () => {
|
|
209
|
+
const controller = new AbortController();
|
|
210
|
+
const promise = lookupObject("https://example.com/slow-object", {
|
|
211
|
+
contextLoader: mockDocumentLoader,
|
|
212
|
+
signal: controller.signal,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
controller.abort();
|
|
216
|
+
deepStrictEqual(await promise, null);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
fetchMock.hardReset();
|
|
220
|
+
fetchMock.removeRoutes();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("traverseCollection()", {
|
|
224
|
+
sanitizeResources: false,
|
|
225
|
+
sanitizeOps: false,
|
|
226
|
+
}, async () => {
|
|
227
|
+
const options = {
|
|
228
|
+
documentLoader: mockDocumentLoader,
|
|
229
|
+
contextLoader: mockDocumentLoader,
|
|
230
|
+
};
|
|
231
|
+
const collection = await lookupObject(
|
|
232
|
+
"https://example.com/collection",
|
|
233
|
+
options,
|
|
234
|
+
);
|
|
235
|
+
assertInstanceOf(collection, Collection);
|
|
236
|
+
deepStrictEqual(
|
|
237
|
+
await Array.fromAsync(traverseCollection(collection, options)),
|
|
238
|
+
[
|
|
239
|
+
new Note({ content: "This is a simple note" }),
|
|
240
|
+
new Note({ content: "This is another simple note" }),
|
|
241
|
+
new Note({ content: "This is a third simple note" }),
|
|
242
|
+
],
|
|
243
|
+
);
|
|
244
|
+
const pagedCollection = await lookupObject(
|
|
245
|
+
"https://example.com/paged-collection",
|
|
246
|
+
options,
|
|
247
|
+
);
|
|
248
|
+
assertInstanceOf(pagedCollection, Collection);
|
|
249
|
+
deepStrictEqual(
|
|
250
|
+
await Array.fromAsync(traverseCollection(pagedCollection, options)),
|
|
251
|
+
[
|
|
252
|
+
new Note({ content: "This is a simple note" }),
|
|
253
|
+
new Note({ content: "This is another simple note" }),
|
|
254
|
+
new Note({ content: "This is a third simple note" }),
|
|
255
|
+
],
|
|
256
|
+
);
|
|
257
|
+
deepStrictEqual(
|
|
258
|
+
await Array.fromAsync(
|
|
259
|
+
traverseCollection(pagedCollection, {
|
|
260
|
+
...options,
|
|
261
|
+
interval: { milliseconds: 250 },
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
[
|
|
265
|
+
new Note({ content: "This is a simple note" }),
|
|
266
|
+
new Note({ content: "This is another simple note" }),
|
|
267
|
+
new Note({ content: "This is a third simple note" }),
|
|
268
|
+
],
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("FEP-fe34: lookupObject() cross-origin security", {
|
|
273
|
+
sanitizeResources: false,
|
|
274
|
+
sanitizeOps: false,
|
|
275
|
+
}, async (t) => {
|
|
276
|
+
await t.step(
|
|
277
|
+
"crossOrigin: ignore (default) - returns null for cross-origin objects",
|
|
278
|
+
async () => {
|
|
279
|
+
// Create a mock document loader that returns an object with different origin
|
|
280
|
+
// deno-lint-ignore require-await
|
|
281
|
+
const crossOriginDocumentLoader = async (url: string) => {
|
|
282
|
+
if (url === "https://example.com/note") {
|
|
283
|
+
return {
|
|
284
|
+
documentUrl: url,
|
|
285
|
+
contextUrl: null,
|
|
286
|
+
document: {
|
|
287
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
288
|
+
type: "Note",
|
|
289
|
+
id: "https://malicious.com/fake-note", // Different origin!
|
|
290
|
+
content: "This is a spoofed note from a different origin",
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const result = await lookupObject("https://example.com/note", {
|
|
298
|
+
documentLoader: crossOriginDocumentLoader,
|
|
299
|
+
contextLoader: mockDocumentLoader,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Should return null and log a warning (default behavior)
|
|
303
|
+
deepStrictEqual(result, null);
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
await t.step(
|
|
308
|
+
"crossOrigin: throw - throws error for cross-origin objects",
|
|
309
|
+
async () => {
|
|
310
|
+
// deno-lint-ignore require-await
|
|
311
|
+
const crossOriginDocumentLoader = async (url: string) => {
|
|
312
|
+
if (url === "https://example.com/note") {
|
|
313
|
+
return {
|
|
314
|
+
documentUrl: url,
|
|
315
|
+
contextUrl: null,
|
|
316
|
+
document: {
|
|
317
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
318
|
+
type: "Note",
|
|
319
|
+
id: "https://malicious.com/fake-note", // Different origin!
|
|
320
|
+
content: "This is a spoofed note from a different origin",
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
await rejects(
|
|
328
|
+
() =>
|
|
329
|
+
lookupObject("https://example.com/note", {
|
|
330
|
+
documentLoader: crossOriginDocumentLoader,
|
|
331
|
+
contextLoader: mockDocumentLoader,
|
|
332
|
+
crossOrigin: "throw",
|
|
333
|
+
}),
|
|
334
|
+
Error,
|
|
335
|
+
"The object's @id (https://malicious.com/fake-note) has a different origin than the document URL (https://example.com/note)",
|
|
336
|
+
);
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
await t.step("crossOrigin: trust - allows cross-origin objects", async () => {
|
|
341
|
+
// deno-lint-ignore require-await
|
|
342
|
+
const crossOriginDocumentLoader = async (url: string) => {
|
|
343
|
+
if (url === "https://example.com/note") {
|
|
344
|
+
return {
|
|
345
|
+
documentUrl: url,
|
|
346
|
+
contextUrl: null,
|
|
347
|
+
document: {
|
|
348
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
349
|
+
type: "Note",
|
|
350
|
+
id: "https://malicious.com/fake-note", // Different origin!
|
|
351
|
+
content: "This is a spoofed note from a different origin",
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const result = await lookupObject("https://example.com/note", {
|
|
359
|
+
documentLoader: crossOriginDocumentLoader,
|
|
360
|
+
contextLoader: mockDocumentLoader,
|
|
361
|
+
crossOrigin: "trust",
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
assertInstanceOf(result, Note);
|
|
365
|
+
deepStrictEqual(result.id, new URL("https://malicious.com/fake-note"));
|
|
366
|
+
deepStrictEqual(
|
|
367
|
+
result.content,
|
|
368
|
+
"This is a spoofed note from a different origin",
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await t.step("same-origin objects are always trusted", async () => {
|
|
373
|
+
// deno-lint-ignore require-await
|
|
374
|
+
const sameOriginDocumentLoader = async (url: string) => {
|
|
375
|
+
if (url === "https://example.com/note") {
|
|
376
|
+
return {
|
|
377
|
+
documentUrl: url,
|
|
378
|
+
contextUrl: null,
|
|
379
|
+
document: {
|
|
380
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
381
|
+
type: "Note",
|
|
382
|
+
id: "https://example.com/note", // Same origin
|
|
383
|
+
content: "This is a legitimate note from the same origin",
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const result = await lookupObject("https://example.com/note", {
|
|
391
|
+
documentLoader: sameOriginDocumentLoader,
|
|
392
|
+
contextLoader: mockDocumentLoader,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
assertInstanceOf(result, Note);
|
|
396
|
+
deepStrictEqual(result.id, new URL("https://example.com/note"));
|
|
397
|
+
deepStrictEqual(
|
|
398
|
+
result.content,
|
|
399
|
+
"This is a legitimate note from the same origin",
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await t.step("objects without @id are trusted", async () => {
|
|
404
|
+
// deno-lint-ignore require-await
|
|
405
|
+
const noIdDocumentLoader = async (url: string) => {
|
|
406
|
+
if (url === "https://example.com/note") {
|
|
407
|
+
return {
|
|
408
|
+
documentUrl: url,
|
|
409
|
+
contextUrl: null,
|
|
410
|
+
document: {
|
|
411
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
412
|
+
type: "Note",
|
|
413
|
+
// No @id field
|
|
414
|
+
content: "This is a note without an ID",
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const result = await lookupObject("https://example.com/note", {
|
|
422
|
+
documentLoader: noIdDocumentLoader,
|
|
423
|
+
contextLoader: mockDocumentLoader,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
assertInstanceOf(result, Note);
|
|
427
|
+
deepStrictEqual(result.id, null);
|
|
428
|
+
deepStrictEqual(result.content, "This is a note without an ID");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
await t.step("WebFinger lookup with cross-origin actor URL", async () => {
|
|
432
|
+
fetchMock.spyGlobal();
|
|
433
|
+
|
|
434
|
+
// Mock WebFinger response
|
|
435
|
+
fetchMock.get("begin:https://example.com/.well-known/webfinger", {
|
|
436
|
+
subject: "acct:user@example.com",
|
|
437
|
+
links: [
|
|
438
|
+
{
|
|
439
|
+
rel: "self",
|
|
440
|
+
href: "https://different-origin.com/actor", // Cross-origin actor URL
|
|
441
|
+
type: "application/activity+json",
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Mock document loader for the cross-origin actor
|
|
447
|
+
// deno-lint-ignore require-await
|
|
448
|
+
const webfingerDocumentLoader = async (url: string) => {
|
|
449
|
+
if (url === "https://different-origin.com/actor") {
|
|
450
|
+
return {
|
|
451
|
+
documentUrl: url,
|
|
452
|
+
contextUrl: null,
|
|
453
|
+
document: {
|
|
454
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
455
|
+
type: "Person",
|
|
456
|
+
id: "https://malicious.com/fake-actor", // Different origin than document URL!
|
|
457
|
+
name: "Fake Actor",
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Default behavior should return null
|
|
465
|
+
const result1 = await lookupObject("@user@example.com", {
|
|
466
|
+
documentLoader: webfingerDocumentLoader,
|
|
467
|
+
contextLoader: mockDocumentLoader,
|
|
468
|
+
});
|
|
469
|
+
deepStrictEqual(result1, null);
|
|
470
|
+
|
|
471
|
+
// With crossOrigin: throw, should throw error
|
|
472
|
+
await rejects(
|
|
473
|
+
() =>
|
|
474
|
+
lookupObject("@user@example.com", {
|
|
475
|
+
documentLoader: webfingerDocumentLoader,
|
|
476
|
+
contextLoader: mockDocumentLoader,
|
|
477
|
+
crossOrigin: "throw",
|
|
478
|
+
}),
|
|
479
|
+
Error,
|
|
480
|
+
"The object's @id (https://malicious.com/fake-actor) has a different origin than the document URL (https://different-origin.com/actor)",
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// With crossOrigin: trust, should return the object
|
|
484
|
+
const result2 = await lookupObject("@user@example.com", {
|
|
485
|
+
documentLoader: webfingerDocumentLoader,
|
|
486
|
+
contextLoader: mockDocumentLoader,
|
|
487
|
+
crossOrigin: "trust",
|
|
488
|
+
});
|
|
489
|
+
assertInstanceOf(result2, Person);
|
|
490
|
+
deepStrictEqual(result2.id, new URL("https://malicious.com/fake-actor"));
|
|
491
|
+
|
|
492
|
+
fetchMock.removeRoutes();
|
|
493
|
+
fetchMock.hardReset();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
await t.step("subdomain same-origin check", async () => {
|
|
497
|
+
// Test that different subdomains are considered different origins
|
|
498
|
+
// deno-lint-ignore require-await
|
|
499
|
+
const subdomainDocumentLoader = async (url: string) => {
|
|
500
|
+
if (url === "https://api.example.com/note") {
|
|
501
|
+
return {
|
|
502
|
+
documentUrl: url,
|
|
503
|
+
contextUrl: null,
|
|
504
|
+
document: {
|
|
505
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
506
|
+
type: "Note",
|
|
507
|
+
id: "https://www.example.com/note", // Different subdomain = different origin
|
|
508
|
+
content: "Cross-subdomain note",
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const result = await lookupObject("https://api.example.com/note", {
|
|
516
|
+
documentLoader: subdomainDocumentLoader,
|
|
517
|
+
contextLoader: mockDocumentLoader,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
deepStrictEqual(result, null); // Should be blocked
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await t.step("different port same-origin check", async () => {
|
|
524
|
+
// Test that different ports are considered different origins
|
|
525
|
+
// deno-lint-ignore require-await
|
|
526
|
+
const differentPortDocumentLoader = async (url: string) => {
|
|
527
|
+
if (url === "https://example.com:8080/note") {
|
|
528
|
+
return {
|
|
529
|
+
documentUrl: url,
|
|
530
|
+
contextUrl: null,
|
|
531
|
+
document: {
|
|
532
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
533
|
+
type: "Note",
|
|
534
|
+
id: "https://example.com:9090/note", // Different port = different origin
|
|
535
|
+
content: "Cross-port note",
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const result = await lookupObject("https://example.com:8080/note", {
|
|
543
|
+
documentLoader: differentPortDocumentLoader,
|
|
544
|
+
contextLoader: mockDocumentLoader,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
deepStrictEqual(result, null); // Should be blocked
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await t.step("protocol difference same-origin check", async () => {
|
|
551
|
+
// Test that different protocols are considered different origins
|
|
552
|
+
// deno-lint-ignore require-await
|
|
553
|
+
const differentProtocolDocumentLoader = async (url: string) => {
|
|
554
|
+
if (url === "https://example.com/note") {
|
|
555
|
+
return {
|
|
556
|
+
documentUrl: url,
|
|
557
|
+
contextUrl: null,
|
|
558
|
+
document: {
|
|
559
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
560
|
+
type: "Note",
|
|
561
|
+
id: "http://example.com/note", // Different protocol = different origin
|
|
562
|
+
content: "Cross-protocol note",
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const result = await lookupObject("https://example.com/note", {
|
|
570
|
+
documentLoader: differentProtocolDocumentLoader,
|
|
571
|
+
contextLoader: mockDocumentLoader,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
deepStrictEqual(result, null); // Should be blocked
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
await t.step("error handling with crossOrigin throw option", async () => {
|
|
578
|
+
// Test that other errors (not cross-origin) are still thrown normally
|
|
579
|
+
// deno-lint-ignore require-await
|
|
580
|
+
const errorDocumentLoader = async (_url: string) => {
|
|
581
|
+
throw new Error("Network error");
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// Network errors should not be confused with cross-origin errors
|
|
585
|
+
const result = await lookupObject("https://example.com/note", {
|
|
586
|
+
documentLoader: errorDocumentLoader,
|
|
587
|
+
contextLoader: mockDocumentLoader,
|
|
588
|
+
crossOrigin: "throw",
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Should return null because the document loader failed,
|
|
592
|
+
// not because of cross-origin policy
|
|
593
|
+
deepStrictEqual(result, null);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
await t.step("malformed JSON handling with cross-origin policy", async () => {
|
|
597
|
+
// deno-lint-ignore require-await
|
|
598
|
+
const malformedJsonDocumentLoader = async (url: string) => {
|
|
599
|
+
if (url === "https://example.com/note") {
|
|
600
|
+
return {
|
|
601
|
+
documentUrl: url,
|
|
602
|
+
contextUrl: null,
|
|
603
|
+
document: "invalid json", // Malformed document
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
throw new Error(`Unexpected URL: ${url}`);
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// Should return null for malformed JSON regardless of crossOrigin setting
|
|
610
|
+
deepStrictEqual(
|
|
611
|
+
await lookupObject("https://example.com/note", {
|
|
612
|
+
documentLoader: malformedJsonDocumentLoader,
|
|
613
|
+
contextLoader: mockDocumentLoader,
|
|
614
|
+
crossOrigin: "ignore",
|
|
615
|
+
}),
|
|
616
|
+
null,
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
deepStrictEqual(
|
|
620
|
+
await lookupObject("https://example.com/note", {
|
|
621
|
+
documentLoader: malformedJsonDocumentLoader,
|
|
622
|
+
contextLoader: mockDocumentLoader,
|
|
623
|
+
crossOrigin: "throw",
|
|
624
|
+
}),
|
|
625
|
+
null,
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
deepStrictEqual(
|
|
629
|
+
await lookupObject("https://example.com/note", {
|
|
630
|
+
documentLoader: malformedJsonDocumentLoader,
|
|
631
|
+
contextLoader: mockDocumentLoader,
|
|
632
|
+
crossOrigin: "trust",
|
|
633
|
+
}),
|
|
634
|
+
null,
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("lookupObject() records OpenTelemetry span events", async () => {
|
|
640
|
+
const [tracerProvider, exporter] = createTestTracerProvider();
|
|
641
|
+
|
|
642
|
+
const object = await lookupObject("https://example.com/object", {
|
|
643
|
+
documentLoader: mockDocumentLoader,
|
|
644
|
+
contextLoader: mockDocumentLoader,
|
|
645
|
+
tracerProvider,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
assertInstanceOf(object, Object);
|
|
649
|
+
|
|
650
|
+
// Check that the span was recorded
|
|
651
|
+
const spans = exporter.getSpans("activitypub.lookup_object");
|
|
652
|
+
deepStrictEqual(spans.length, 1);
|
|
653
|
+
const span = spans[0];
|
|
654
|
+
|
|
655
|
+
// Check span attributes
|
|
656
|
+
deepStrictEqual(
|
|
657
|
+
span.attributes["activitypub.object.id"],
|
|
658
|
+
"https://example.com/object",
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
// Check that the object.fetched event was recorded
|
|
662
|
+
const events = exporter.getEvents(
|
|
663
|
+
"activitypub.lookup_object",
|
|
664
|
+
"activitypub.object.fetched",
|
|
665
|
+
);
|
|
666
|
+
deepStrictEqual(events.length, 1);
|
|
667
|
+
const event = events[0];
|
|
668
|
+
|
|
669
|
+
// Verify event attributes
|
|
670
|
+
ok(event.attributes != null);
|
|
671
|
+
ok(typeof event.attributes["activitypub.object.type"] === "string");
|
|
672
|
+
ok(typeof event.attributes["activitypub.object.json"] === "string");
|
|
673
|
+
|
|
674
|
+
// Verify the JSON contains the object
|
|
675
|
+
const recordedObject = JSON.parse(
|
|
676
|
+
event.attributes["activitypub.object.json"] as string,
|
|
677
|
+
);
|
|
678
|
+
deepStrictEqual(recordedObject.id, "https://example.com/object");
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// cSpell: ignore gildong
|