@sdxc/activitypub 0.0.0-pre.1
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.md +21 -0
- package/README.md +711 -0
- package/dist/collections.d.ts +67 -0
- package/dist/collections.js +38 -0
- package/dist/conformance.d.ts +80 -0
- package/dist/conformance.js +313 -0
- package/dist/discovery.d.ts +46 -0
- package/dist/discovery.js +105 -0
- package/dist/errors.d.ts +115 -0
- package/dist/errors.js +124 -0
- package/dist/federation.d.ts +281 -0
- package/dist/federation.js +513 -0
- package/dist/fixtures/gotosocial.d.ts +114 -0
- package/dist/fixtures/gotosocial.js +115 -0
- package/dist/fixtures/index.d.ts +15 -0
- package/dist/fixtures/index.js +50 -0
- package/dist/fixtures/mastodon.d.ts +489 -0
- package/dist/fixtures/mastodon.js +334 -0
- package/dist/fixtures/misskey.d.ts +254 -0
- package/dist/fixtures/misskey.js +163 -0
- package/dist/fixtures/pixelfed.d.ts +118 -0
- package/dist/fixtures/pixelfed.js +126 -0
- package/dist/fixtures/shared.d.ts +14 -0
- package/dist/fixtures/shared.js +14 -0
- package/dist/fixtures/threads.d.ts +82 -0
- package/dist/fixtures/threads.js +88 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +22 -0
- package/dist/keys.d.ts +100 -0
- package/dist/keys.js +147 -0
- package/dist/lib/availability.d.ts +37 -0
- package/dist/lib/availability.js +52 -0
- package/dist/lib/compact.d.ts +145 -0
- package/dist/lib/compact.js +514 -0
- package/dist/lib/constants.d.ts +35 -0
- package/dist/lib/constants.js +65 -0
- package/dist/lib/delivery-fan-out.d.ts +72 -0
- package/dist/lib/delivery-fan-out.js +202 -0
- package/dist/lib/delivery-post.d.ts +108 -0
- package/dist/lib/delivery-post.js +318 -0
- package/dist/lib/fetch.d.ts +64 -0
- package/dist/lib/fetch.js +103 -0
- package/dist/lib/inbox-error.d.ts +45 -0
- package/dist/lib/inbox-error.js +74 -0
- package/dist/lib/inbox-handle.d.ts +119 -0
- package/dist/lib/inbox-handle.js +587 -0
- package/dist/lib/inbox-receive.d.ts +117 -0
- package/dist/lib/inbox-receive.js +233 -0
- package/dist/lib/inbox-signature.d.ts +61 -0
- package/dist/lib/inbox-signature.js +156 -0
- package/dist/lib/inbox-summarize.d.ts +41 -0
- package/dist/lib/inbox-summarize.js +97 -0
- package/dist/lib/messages.d.ts +36 -0
- package/dist/lib/messages.js +35 -0
- package/dist/lib/parse.d.ts +42 -0
- package/dist/lib/parse.js +348 -0
- package/dist/lib/response.d.ts +45 -0
- package/dist/lib/response.js +59 -0
- package/dist/lib/stringify.d.ts +51 -0
- package/dist/lib/stringify.js +122 -0
- package/dist/lib/tombstone.d.ts +24 -0
- package/dist/lib/tombstone.js +15 -0
- package/dist/lib/types.d.ts +249 -0
- package/dist/lib/types.js +0 -0
- package/dist/memory.d.ts +77 -0
- package/dist/memory.js +146 -0
- package/dist/remote.d.ts +105 -0
- package/dist/remote.js +262 -0
- package/dist/store.d.ts +78 -0
- package/dist/store.js +8 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +10 -0
- package/package.json +54 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { createIssue, createSchema, fail } from "remix/data-schema";
|
|
2
|
+
import { PUBLIC } from "./constants.js";
|
|
3
|
+
/** The compact spellings senders use for the public collection, besides the full IRI. */
|
|
4
|
+
const PUBLIC_ALIASES = new Set(["as:Public", "Public", PUBLIC]);
|
|
5
|
+
/** The attachment types that carry a file; Mastodon sends `Document`, others the specific type. */
|
|
6
|
+
const MEDIA_TYPES = new Set(["Document", "Image", "Video", "Audio"]);
|
|
7
|
+
/**
|
|
8
|
+
* Whether a decoded value is a plain JSON object.
|
|
9
|
+
*
|
|
10
|
+
* @param value - Any decoded JSON value.
|
|
11
|
+
*/
|
|
12
|
+
export function isObject(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Runs a schema on a member, so its issues point at that member.
|
|
17
|
+
*
|
|
18
|
+
* @param schema - The member's schema.
|
|
19
|
+
* @param value - The member's value.
|
|
20
|
+
* @param context - The enclosing value's context.
|
|
21
|
+
* @param key - The member name or array index.
|
|
22
|
+
* @template Output - What the schema produces.
|
|
23
|
+
*/
|
|
24
|
+
export function runAt(schema, value, context, key) {
|
|
25
|
+
let result = schema["~run"](value, { path: [...context.path, key], options: context.options });
|
|
26
|
+
if (result.issues !== undefined)
|
|
27
|
+
return { issues: result.issues };
|
|
28
|
+
return { value: result.value };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The public collection's IRI for any spelling of it; every other IRI unchanged.
|
|
32
|
+
*
|
|
33
|
+
* @param iri - An addressing entry.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizePublic(iri) {
|
|
36
|
+
return PUBLIC_ALIASES.has(iri) ? PUBLIC : iri;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The values of a member that may be a single value or an array, as an array.
|
|
40
|
+
*
|
|
41
|
+
* @param value - The member's value; `undefined` and `null` read as empty.
|
|
42
|
+
*/
|
|
43
|
+
function many(value) {
|
|
44
|
+
if (value === undefined || value === null)
|
|
45
|
+
return [];
|
|
46
|
+
return Array.isArray(value) ? value : [value];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The IRI an entry names: itself when a string, its `id` when an embedded object, or its
|
|
50
|
+
* `href` when a `Link`.
|
|
51
|
+
*
|
|
52
|
+
* @param value - An IRI, an embedded object, or a Link.
|
|
53
|
+
*/
|
|
54
|
+
function iriOf(value) {
|
|
55
|
+
if (typeof value === "string")
|
|
56
|
+
return value;
|
|
57
|
+
if (!isObject(value))
|
|
58
|
+
return null;
|
|
59
|
+
if (typeof value.id === "string")
|
|
60
|
+
return value.id;
|
|
61
|
+
if (typeof value.href === "string")
|
|
62
|
+
return value.href;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* An absolute IRI, which a document's `id` must be: every ownership check compares its
|
|
67
|
+
* origin, and a relative one has none.
|
|
68
|
+
*
|
|
69
|
+
* @param label - The member, for the message.
|
|
70
|
+
*/
|
|
71
|
+
export function absoluteIri(label) {
|
|
72
|
+
return createSchema((value, context) => {
|
|
73
|
+
if (typeof value !== "string")
|
|
74
|
+
return fail(`"${label}" must be a string IRI.`, context.path);
|
|
75
|
+
if (!URL.canParse(value))
|
|
76
|
+
return fail(`"${label}" must be an absolute IRI.`, context.path);
|
|
77
|
+
return { value };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* An absolute IRI that may be absent, `null` then.
|
|
82
|
+
*
|
|
83
|
+
* @param label - The member, for the message.
|
|
84
|
+
*/
|
|
85
|
+
export function optionalAbsoluteIri(label) {
|
|
86
|
+
let required = absoluteIri(label);
|
|
87
|
+
return createSchema((value, context) => {
|
|
88
|
+
if (value === undefined || value === null)
|
|
89
|
+
return { value: null };
|
|
90
|
+
return required["~run"](value, context);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A `type` sent as a string or an array, read as one name: the first a caller knows,
|
|
95
|
+
* else the first listed. An `as:` prefix is dropped, since it names the same term.
|
|
96
|
+
*
|
|
97
|
+
* @param known - Type names to prefer when several are listed.
|
|
98
|
+
*/
|
|
99
|
+
export function documentType(known) {
|
|
100
|
+
return createSchema((value, context) => {
|
|
101
|
+
let names = many(value)
|
|
102
|
+
.filter((entry) => typeof entry === "string")
|
|
103
|
+
.map((name) => (name.startsWith("as:") ? name.slice(3) : name));
|
|
104
|
+
let first = names.find((name) => known.has(name)) ?? names[0];
|
|
105
|
+
if (first === undefined)
|
|
106
|
+
return fail('"type" must name the document\'s type.', context.path);
|
|
107
|
+
return { value: first };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* A list of IRIs sent as one entry or an array, each an IRI or an embedded object, read
|
|
112
|
+
* as the IRIs alone. With `addressing`, every spelling of the public collection reads
|
|
113
|
+
* as `PUBLIC`.
|
|
114
|
+
*
|
|
115
|
+
* @param options - Whether the list addresses an audience.
|
|
116
|
+
*/
|
|
117
|
+
export function iris(options = {}) {
|
|
118
|
+
return createSchema((value, context) => {
|
|
119
|
+
let issues = [];
|
|
120
|
+
let result = [];
|
|
121
|
+
let entries = many(value);
|
|
122
|
+
for (let [index, entry] of entries.entries()) {
|
|
123
|
+
let iri = iriOf(entry);
|
|
124
|
+
if (iri === null) {
|
|
125
|
+
let at = Array.isArray(value) ? [...context.path, index] : context.path;
|
|
126
|
+
issues.push(createIssue("An entry must be an IRI or an object with an id.", at));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
result.push(options.addressing ? normalizePublic(iri) : iri);
|
|
130
|
+
}
|
|
131
|
+
if (issues.length > 0)
|
|
132
|
+
return { issues };
|
|
133
|
+
return { value: result };
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export function iri(options = {}) {
|
|
137
|
+
return createSchema((value, context) => {
|
|
138
|
+
let entries = many(value);
|
|
139
|
+
if (entries.length === 0) {
|
|
140
|
+
if (!options.required)
|
|
141
|
+
return { value: null };
|
|
142
|
+
return fail(`"${options.label ?? "member"}" is required.`, context.path);
|
|
143
|
+
}
|
|
144
|
+
let first = iriOf(entries[0]);
|
|
145
|
+
if (first === null) {
|
|
146
|
+
return fail("The reference must be an IRI or an object with an id.", context.path);
|
|
147
|
+
}
|
|
148
|
+
return { value: first };
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Text that may be absent: a string, or `null` when absent. */
|
|
152
|
+
export function text() {
|
|
153
|
+
return createSchema((value, context) => {
|
|
154
|
+
if (value === undefined || value === null)
|
|
155
|
+
return { value: null };
|
|
156
|
+
if (typeof value !== "string")
|
|
157
|
+
return fail("The value must be a string.", context.path);
|
|
158
|
+
return { value };
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** A language map (`contentMap`, `nameMap`, `summaryMap`), empty when absent. */
|
|
162
|
+
export function languageMap() {
|
|
163
|
+
return createSchema((value, context) => {
|
|
164
|
+
if (value === undefined || value === null)
|
|
165
|
+
return { value: {} };
|
|
166
|
+
if (!isObject(value))
|
|
167
|
+
return fail("A language map must be an object.", context.path);
|
|
168
|
+
let issues = [];
|
|
169
|
+
let result = {};
|
|
170
|
+
for (let [language, entry] of Object.entries(value)) {
|
|
171
|
+
if (typeof entry === "string")
|
|
172
|
+
result[language] = entry;
|
|
173
|
+
else
|
|
174
|
+
issues.push(createIssue("A language map value must be a string.", [...context.path, language]));
|
|
175
|
+
}
|
|
176
|
+
if (issues.length > 0)
|
|
177
|
+
return { issues };
|
|
178
|
+
return { value: result };
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The plain value of a text member, falling back to the first entry of its language map
|
|
183
|
+
* when a sender sent only the map.
|
|
184
|
+
*
|
|
185
|
+
* @param plain - The plain value.
|
|
186
|
+
* @param map - The language map.
|
|
187
|
+
*/
|
|
188
|
+
export function withMapFallback(plain, map) {
|
|
189
|
+
if (plain !== null)
|
|
190
|
+
return plain;
|
|
191
|
+
return Object.values(map)[0] ?? null;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The links a `url` member offers, each with its media type when the sender gave one.
|
|
195
|
+
*
|
|
196
|
+
* @param value - A string, a `Link`, or an array of either.
|
|
197
|
+
*/
|
|
198
|
+
function linksOf(value) {
|
|
199
|
+
let links = [];
|
|
200
|
+
for (let entry of many(value)) {
|
|
201
|
+
if (typeof entry === "string")
|
|
202
|
+
links.push({ href: entry, mediaType: null });
|
|
203
|
+
else if (isObject(entry) && typeof entry.href === "string") {
|
|
204
|
+
let mediaType = typeof entry.mediaType === "string" ? entry.mediaType : null;
|
|
205
|
+
links.push({ href: entry.href, mediaType });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return links;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* A `url` sent as a string, a `Link` or an array of them, read as the page a person
|
|
212
|
+
* opens: the first `text/html` link, else the first link. A member that names no link
|
|
213
|
+
* at all reads as `null`.
|
|
214
|
+
*/
|
|
215
|
+
export function htmlUrl() {
|
|
216
|
+
return createSchema((value, context) => {
|
|
217
|
+
if (value === undefined || value === null)
|
|
218
|
+
return { value: null };
|
|
219
|
+
if (typeof value !== "string" && typeof value !== "object") {
|
|
220
|
+
return fail('"url" must be a string, a Link or an array of them.', context.path);
|
|
221
|
+
}
|
|
222
|
+
let links = linksOf(value);
|
|
223
|
+
let html = links.find((link) => link.mediaType?.split(";")[0]?.trim() === "text/html");
|
|
224
|
+
return { value: (html ?? links[0])?.href ?? null };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/** An ISO 8601 date, `null` when absent. */
|
|
228
|
+
export function date() {
|
|
229
|
+
return createSchema((value, context) => {
|
|
230
|
+
if (value === undefined || value === null)
|
|
231
|
+
return { value: null };
|
|
232
|
+
if (typeof value !== "string")
|
|
233
|
+
return fail("A date must be an ISO 8601 string.", context.path);
|
|
234
|
+
let parsed = new Date(value);
|
|
235
|
+
if (Number.isNaN(parsed.getTime()))
|
|
236
|
+
return fail("The date is not valid.", context.path);
|
|
237
|
+
return { value: parsed };
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/** A boolean flag, `false` when absent. */
|
|
241
|
+
export function flag() {
|
|
242
|
+
return createSchema((value, context) => {
|
|
243
|
+
if (value === undefined || value === null)
|
|
244
|
+
return { value: false };
|
|
245
|
+
if (typeof value !== "boolean")
|
|
246
|
+
return fail("The flag must be a boolean.", context.path);
|
|
247
|
+
return { value };
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/** A count such as `totalItems`, `null` when absent. */
|
|
251
|
+
export function count() {
|
|
252
|
+
return createSchema((value, context) => {
|
|
253
|
+
if (value === undefined || value === null)
|
|
254
|
+
return { value: null };
|
|
255
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
256
|
+
return fail("A count must be a non-negative integer.", context.path);
|
|
257
|
+
}
|
|
258
|
+
return { value };
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* A string member of an embedded object, or `null`.
|
|
263
|
+
*
|
|
264
|
+
* @param body - The embedded object.
|
|
265
|
+
* @param member - The member name.
|
|
266
|
+
*/
|
|
267
|
+
function stringOf(body, member) {
|
|
268
|
+
let value = body[member];
|
|
269
|
+
return typeof value === "string" ? value : null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* A positive integer member of an embedded object, or `null`.
|
|
273
|
+
*
|
|
274
|
+
* @param body - The embedded object.
|
|
275
|
+
* @param member - The member name.
|
|
276
|
+
*/
|
|
277
|
+
function dimensionOf(body, member) {
|
|
278
|
+
let value = body[member];
|
|
279
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* A date member of an embedded object, or `null` when absent or invalid.
|
|
283
|
+
*
|
|
284
|
+
* @param body - The embedded object.
|
|
285
|
+
* @param member - The member name.
|
|
286
|
+
*/
|
|
287
|
+
function dateOf(body, member) {
|
|
288
|
+
let value = body[member];
|
|
289
|
+
if (typeof value !== "string")
|
|
290
|
+
return null;
|
|
291
|
+
let parsed = new Date(value);
|
|
292
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* The type names an embedded value lists, `as:` prefixes dropped.
|
|
296
|
+
*
|
|
297
|
+
* @param body - The embedded object.
|
|
298
|
+
*/
|
|
299
|
+
function typesOf(body) {
|
|
300
|
+
return many(body.type)
|
|
301
|
+
.filter((entry) => typeof entry === "string")
|
|
302
|
+
.map((name) => (name.startsWith("as:") ? name.slice(3) : name));
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* An image sent as a string URL or an `Image` object (or an array of them), read as an
|
|
306
|
+
* `Image`. A value that names no URL reads as `null`.
|
|
307
|
+
*
|
|
308
|
+
* @param value - The member's value.
|
|
309
|
+
*/
|
|
310
|
+
export function readImage(value) {
|
|
311
|
+
for (let entry of many(value)) {
|
|
312
|
+
if (typeof entry === "string")
|
|
313
|
+
return { type: "Image", url: entry, mediaType: null };
|
|
314
|
+
if (!isObject(entry))
|
|
315
|
+
continue;
|
|
316
|
+
let link = linksOf(entry.url)[0];
|
|
317
|
+
if (link === undefined)
|
|
318
|
+
continue;
|
|
319
|
+
let mediaType = stringOf(entry, "mediaType") ?? link.mediaType;
|
|
320
|
+
return { type: "Image", url: link.href, mediaType };
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
/** An `icon` or `image`, `null` when absent or naming no URL. */
|
|
325
|
+
export function image() {
|
|
326
|
+
return createSchema((value) => ({ value: readImage(value) }));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* One `tag` entry, or `null` for a type this package does not model or an entry that
|
|
330
|
+
* lacks what its type requires.
|
|
331
|
+
*
|
|
332
|
+
* @param entry - The decoded entry.
|
|
333
|
+
*/
|
|
334
|
+
function readTag(entry) {
|
|
335
|
+
if (!isObject(entry))
|
|
336
|
+
return null;
|
|
337
|
+
let types = typesOf(entry);
|
|
338
|
+
if (types.includes("Hashtag")) {
|
|
339
|
+
let name = stringOf(entry, "name");
|
|
340
|
+
if (name === null)
|
|
341
|
+
return null;
|
|
342
|
+
return { type: "Hashtag", name, href: stringOf(entry, "href") };
|
|
343
|
+
}
|
|
344
|
+
if (types.includes("Mention")) {
|
|
345
|
+
let href = stringOf(entry, "href");
|
|
346
|
+
if (href === null)
|
|
347
|
+
return null;
|
|
348
|
+
return { type: "Mention", href, name: stringOf(entry, "name") };
|
|
349
|
+
}
|
|
350
|
+
if (types.includes("Emoji")) {
|
|
351
|
+
let name = stringOf(entry, "name");
|
|
352
|
+
let icon = readImage(entry.icon);
|
|
353
|
+
if (name === null || icon === null)
|
|
354
|
+
return null;
|
|
355
|
+
return {
|
|
356
|
+
type: "Emoji",
|
|
357
|
+
id: stringOf(entry, "id"),
|
|
358
|
+
name,
|
|
359
|
+
icon,
|
|
360
|
+
updated: dateOf(entry, "updated"),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* `tag` entries this package models: hashtags, mentions and custom emoji. Entries of
|
|
367
|
+
* other types, and entries missing what their type requires, are dropped, since a tag
|
|
368
|
+
* decorates an object and never decides whether it can be read.
|
|
369
|
+
*/
|
|
370
|
+
export function tags() {
|
|
371
|
+
return createSchema((value) => ({
|
|
372
|
+
value: many(value)
|
|
373
|
+
.map(readTag)
|
|
374
|
+
.filter((tag) => tag !== null),
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* A `focalPoint` as two finite coordinates, or `null`.
|
|
379
|
+
*
|
|
380
|
+
* @param value - The decoded member.
|
|
381
|
+
*/
|
|
382
|
+
function focalPointOf(value) {
|
|
383
|
+
if (!Array.isArray(value) || value.length !== 2)
|
|
384
|
+
return null;
|
|
385
|
+
let [x, y] = value;
|
|
386
|
+
if (typeof x !== "number" || typeof y !== "number")
|
|
387
|
+
return null;
|
|
388
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
389
|
+
return null;
|
|
390
|
+
return [x, y];
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* One `attachment` entry, or `null` for a type this package does not model or an entry
|
|
394
|
+
* that lacks what its type requires.
|
|
395
|
+
*
|
|
396
|
+
* @param entry - The decoded entry.
|
|
397
|
+
*/
|
|
398
|
+
function readAttachment(entry) {
|
|
399
|
+
if (!isObject(entry))
|
|
400
|
+
return null;
|
|
401
|
+
let types = typesOf(entry);
|
|
402
|
+
if (types.includes("PropertyValue")) {
|
|
403
|
+
let name = stringOf(entry, "name");
|
|
404
|
+
let value = stringOf(entry, "value");
|
|
405
|
+
if (name === null || value === null)
|
|
406
|
+
return null;
|
|
407
|
+
return { type: "PropertyValue", name, value };
|
|
408
|
+
}
|
|
409
|
+
let type = types.find((name) => MEDIA_TYPES.has(name));
|
|
410
|
+
if (type === undefined)
|
|
411
|
+
return null;
|
|
412
|
+
let links = linksOf(entry.url);
|
|
413
|
+
let link = links[0];
|
|
414
|
+
if (link === undefined)
|
|
415
|
+
return null;
|
|
416
|
+
return {
|
|
417
|
+
type,
|
|
418
|
+
url: link.href,
|
|
419
|
+
mediaType: stringOf(entry, "mediaType") ?? link.mediaType,
|
|
420
|
+
name: stringOf(entry, "name"),
|
|
421
|
+
blurhash: stringOf(entry, "blurhash"),
|
|
422
|
+
width: dimensionOf(entry, "width"),
|
|
423
|
+
height: dimensionOf(entry, "height"),
|
|
424
|
+
focalPoint: focalPointOf(entry.focalPoint),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* `attachment` entries this package models: media files and profile fields. Other
|
|
429
|
+
* entries are dropped, for the same reason as tags.
|
|
430
|
+
*/
|
|
431
|
+
export function attachments() {
|
|
432
|
+
return createSchema((value) => ({
|
|
433
|
+
value: many(value)
|
|
434
|
+
.map(readAttachment)
|
|
435
|
+
.filter((attachment) => attachment !== null),
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* FEP-8b32 proofs, sent as one object or a set. An entry that is not a complete
|
|
440
|
+
* `DataIntegrityProof` is dropped, so verification treats the document as unproven.
|
|
441
|
+
*/
|
|
442
|
+
export function proofs() {
|
|
443
|
+
return createSchema((value) => {
|
|
444
|
+
let result = [];
|
|
445
|
+
for (let entry of many(value)) {
|
|
446
|
+
if (!isObject(entry) || !typesOf(entry).includes("DataIntegrityProof"))
|
|
447
|
+
continue;
|
|
448
|
+
let cryptosuite = stringOf(entry, "cryptosuite");
|
|
449
|
+
let verificationMethod = iriOf(entry.verificationMethod);
|
|
450
|
+
let proofPurpose = stringOf(entry, "proofPurpose");
|
|
451
|
+
let proofValue = stringOf(entry, "proofValue");
|
|
452
|
+
if (!cryptosuite || !verificationMethod || !proofPurpose || !proofValue)
|
|
453
|
+
continue;
|
|
454
|
+
result.push({
|
|
455
|
+
type: "DataIntegrityProof",
|
|
456
|
+
cryptosuite,
|
|
457
|
+
verificationMethod,
|
|
458
|
+
proofPurpose,
|
|
459
|
+
proofValue,
|
|
460
|
+
created: dateOf(entry, "created"),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return { value: result };
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/** FEP-521a `Multikey` entries of `assertionMethod`; other verification methods are dropped. */
|
|
467
|
+
export function multikeys() {
|
|
468
|
+
return createSchema((value) => {
|
|
469
|
+
let result = [];
|
|
470
|
+
for (let entry of many(value)) {
|
|
471
|
+
if (!isObject(entry) || !typesOf(entry).includes("Multikey"))
|
|
472
|
+
continue;
|
|
473
|
+
let id = stringOf(entry, "id");
|
|
474
|
+
let controller = stringOf(entry, "controller");
|
|
475
|
+
let publicKeyMultibase = stringOf(entry, "publicKeyMultibase");
|
|
476
|
+
if (!id || !controller || !publicKeyMultibase)
|
|
477
|
+
continue;
|
|
478
|
+
result.push({ id, type: "Multikey", controller, publicKeyMultibase });
|
|
479
|
+
}
|
|
480
|
+
return { value: result };
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* An actor's `publicKey`, sent as one object or an array whose first complete entry is
|
|
485
|
+
* read. A key present but missing `id`, `owner` or `publicKeyPem` fails, since no
|
|
486
|
+
* signature from the actor could verify against it.
|
|
487
|
+
*/
|
|
488
|
+
export function publicKey() {
|
|
489
|
+
return createSchema((value, context) => {
|
|
490
|
+
let entries = many(value);
|
|
491
|
+
if (entries.length === 0)
|
|
492
|
+
return { value: null };
|
|
493
|
+
for (let entry of entries) {
|
|
494
|
+
if (!isObject(entry))
|
|
495
|
+
continue;
|
|
496
|
+
let id = stringOf(entry, "id");
|
|
497
|
+
let owner = iriOf(entry.owner);
|
|
498
|
+
let publicKeyPem = stringOf(entry, "publicKeyPem");
|
|
499
|
+
if (id && owner && publicKeyPem)
|
|
500
|
+
return { value: { id, owner, publicKeyPem } };
|
|
501
|
+
}
|
|
502
|
+
return fail('"publicKey" must have an id, an owner and a publicKeyPem.', context.path);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
/** An actor's `endpoints`, whose `sharedInbox` is `null` when absent. */
|
|
506
|
+
export function endpoints() {
|
|
507
|
+
return createSchema((value, context) => {
|
|
508
|
+
if (value === undefined || value === null)
|
|
509
|
+
return { value: { sharedInbox: null } };
|
|
510
|
+
if (!isObject(value))
|
|
511
|
+
return fail('"endpoints" must be an object.', context.path);
|
|
512
|
+
return { value: { sharedInbox: iriOf(value.sharedInbox) } };
|
|
513
|
+
});
|
|
514
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed IRIs and media types ActivityPub documents are read and written with: the
|
|
3
|
+
* public collection, the JSON-LD contexts `stringify` writes, and the AS2 media types
|
|
4
|
+
* content negotiation and delivery name.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The special collection that makes an object public. Addressed in `to` it is a public
|
|
11
|
+
* post; in `cc` alone, an unlisted one. Every compact spelling reads as this IRI.
|
|
12
|
+
*/
|
|
13
|
+
export declare const PUBLIC = "https://www.w3.org/ns/activitystreams#Public";
|
|
14
|
+
/** The normative ActivityStreams 2.0 context, which every document is read against. */
|
|
15
|
+
export declare const AS2_CONTEXT = "https://www.w3.org/ns/activitystreams";
|
|
16
|
+
/** Defines `publicKey`, `owner` and `publicKeyPem`, which Mastodon reads from an actor. */
|
|
17
|
+
export declare const SECURITY_CONTEXT = "https://w3id.org/security/v1";
|
|
18
|
+
/** Defines `DataIntegrityProof` (FEP-8b32), written only when a document carries a proof. */
|
|
19
|
+
export declare const DATA_INTEGRITY_CONTEXT = "https://w3id.org/security/data-integrity/v1";
|
|
20
|
+
/** Defines `Multikey` (FEP-521a), written only when an actor lists `assertionMethod` keys. */
|
|
21
|
+
export declare const MULTIKEY_CONTEXT = "https://w3id.org/security/multikey/v1";
|
|
22
|
+
/** The media type ActivityPub servers serve and accept, as ActivityPub §3.2 recommends. */
|
|
23
|
+
export declare const ACTIVITY_JSON = "application/activity+json";
|
|
24
|
+
/** The JSON-LD media type ActivityPub §3.2 requires a server to treat as `ACTIVITY_JSON`. */
|
|
25
|
+
export declare const LD_JSON = "application/ld+json";
|
|
26
|
+
/** The JSON-LD form with the AS2 profile, as a client sends it in `Accept`. */
|
|
27
|
+
export declare const LD_JSON_ACTIVITY = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
|
|
28
|
+
/** The `Content-Type` every document this package serves is answered with. */
|
|
29
|
+
export declare const ACTIVITY_CONTENT_TYPE = "application/activity+json; charset=utf-8";
|
|
30
|
+
/** The `Accept` a client sends to ask a server for the AS2 representation of a URL. */
|
|
31
|
+
export declare const ACTIVITY_ACCEPT = "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
|
|
32
|
+
/** The ActivityStreams 2.0 activity types, plus `EmojiReact`, which Pleroma and Akkoma send. */
|
|
33
|
+
export declare const ACTIVITY_TYPES: readonly ["Accept", "Add", "Announce", "Arrive", "Block", "Create", "Delete", "Dislike", "EmojiReact", "Flag", "Follow", "Ignore", "Invite", "Join", "Leave", "Like", "Listen", "Move", "Offer", "Question", "Read", "Reject", "Remove", "TentativeAccept", "TentativeReject", "Travel", "Undo", "Update", "View"];
|
|
34
|
+
/** The ActivityStreams 2.0 actor types. */
|
|
35
|
+
export declare const ACTOR_TYPES: readonly ["Application", "Group", "Organization", "Person", "Service"];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed IRIs and media types ActivityPub documents are read and written with: the
|
|
3
|
+
* public collection, the JSON-LD contexts `stringify` writes, and the AS2 media types
|
|
4
|
+
* content negotiation and delivery name.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The special collection that makes an object public. Addressed in `to` it is a public
|
|
11
|
+
* post; in `cc` alone, an unlisted one. Every compact spelling reads as this IRI.
|
|
12
|
+
*/
|
|
13
|
+
export const PUBLIC = "https://www.w3.org/ns/activitystreams#Public";
|
|
14
|
+
/** The normative ActivityStreams 2.0 context, which every document is read against. */
|
|
15
|
+
export const AS2_CONTEXT = "https://www.w3.org/ns/activitystreams";
|
|
16
|
+
/** Defines `publicKey`, `owner` and `publicKeyPem`, which Mastodon reads from an actor. */
|
|
17
|
+
export const SECURITY_CONTEXT = "https://w3id.org/security/v1";
|
|
18
|
+
/** Defines `DataIntegrityProof` (FEP-8b32), written only when a document carries a proof. */
|
|
19
|
+
export const DATA_INTEGRITY_CONTEXT = "https://w3id.org/security/data-integrity/v1";
|
|
20
|
+
/** Defines `Multikey` (FEP-521a), written only when an actor lists `assertionMethod` keys. */
|
|
21
|
+
export const MULTIKEY_CONTEXT = "https://w3id.org/security/multikey/v1";
|
|
22
|
+
/** The media type ActivityPub servers serve and accept, as ActivityPub §3.2 recommends. */
|
|
23
|
+
export const ACTIVITY_JSON = "application/activity+json";
|
|
24
|
+
/** The JSON-LD media type ActivityPub §3.2 requires a server to treat as `ACTIVITY_JSON`. */
|
|
25
|
+
export const LD_JSON = "application/ld+json";
|
|
26
|
+
/** The JSON-LD form with the AS2 profile, as a client sends it in `Accept`. */
|
|
27
|
+
export const LD_JSON_ACTIVITY = `${LD_JSON}; profile="${AS2_CONTEXT}"`;
|
|
28
|
+
/** The `Content-Type` every document this package serves is answered with. */
|
|
29
|
+
export const ACTIVITY_CONTENT_TYPE = `${ACTIVITY_JSON}; charset=utf-8`;
|
|
30
|
+
/** The `Accept` a client sends to ask a server for the AS2 representation of a URL. */
|
|
31
|
+
export const ACTIVITY_ACCEPT = `${ACTIVITY_JSON}, ${LD_JSON_ACTIVITY}`;
|
|
32
|
+
/** The ActivityStreams 2.0 activity types, plus `EmojiReact`, which Pleroma and Akkoma send. */
|
|
33
|
+
export const ACTIVITY_TYPES = [
|
|
34
|
+
"Accept",
|
|
35
|
+
"Add",
|
|
36
|
+
"Announce",
|
|
37
|
+
"Arrive",
|
|
38
|
+
"Block",
|
|
39
|
+
"Create",
|
|
40
|
+
"Delete",
|
|
41
|
+
"Dislike",
|
|
42
|
+
"EmojiReact",
|
|
43
|
+
"Flag",
|
|
44
|
+
"Follow",
|
|
45
|
+
"Ignore",
|
|
46
|
+
"Invite",
|
|
47
|
+
"Join",
|
|
48
|
+
"Leave",
|
|
49
|
+
"Like",
|
|
50
|
+
"Listen",
|
|
51
|
+
"Move",
|
|
52
|
+
"Offer",
|
|
53
|
+
"Question",
|
|
54
|
+
"Read",
|
|
55
|
+
"Reject",
|
|
56
|
+
"Remove",
|
|
57
|
+
"TentativeAccept",
|
|
58
|
+
"TentativeReject",
|
|
59
|
+
"Travel",
|
|
60
|
+
"Undo",
|
|
61
|
+
"Update",
|
|
62
|
+
"View",
|
|
63
|
+
];
|
|
64
|
+
/** The ActivityStreams 2.0 actor types. */
|
|
65
|
+
export const ACTOR_TYPES = ["Application", "Group", "Organization", "Person", "Service"];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Works out where one activity goes: the local actor's followers, paged from the store,
|
|
3
|
+
* and every other addressed actor, resolved to its inbox. Targets are deduplicated by URL,
|
|
4
|
+
* blocked and unavailable servers skipped, and deliveries enqueued in batches.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Cache } from "@sdxc/cache";
|
|
10
|
+
import type { Result } from "@sdxc/result";
|
|
11
|
+
import type { Resolver } from "../remote.js";
|
|
12
|
+
import type { FollowerStore } from "../store.js";
|
|
13
|
+
import { ActivityPubError } from "../errors.js";
|
|
14
|
+
import type { DeliveryInput } from "./delivery-post.js";
|
|
15
|
+
import type { BlockedCheck } from "./inbox-receive.js";
|
|
16
|
+
import type { ActivityPub } from "./types.js";
|
|
17
|
+
/** Leaves room for the inbox, the actor and the job envelope inside a 128 KB queue message. */
|
|
18
|
+
export declare const MAX_ACTIVITY_BYTES: number;
|
|
19
|
+
/** One fan-out: the local actor sending and the activity as JSON text. */
|
|
20
|
+
export interface FanOutInput {
|
|
21
|
+
/** The local actor whose followers receive the activity and whose keys sign it. */
|
|
22
|
+
actor: string;
|
|
23
|
+
/** The activity as JSON text; `bto` and `bcc` are read for addressing and stripped. */
|
|
24
|
+
activity: string;
|
|
25
|
+
}
|
|
26
|
+
/** Why a fan-out failed: `store` and `enqueue` are retryable, the rest repeat on a retry. */
|
|
27
|
+
export type FanOutErrorCode = "invalid-input" | "invalid-document" | "too-large" | "store" | "enqueue";
|
|
28
|
+
/** What `fanOut` needs. */
|
|
29
|
+
export interface FanOutOptions {
|
|
30
|
+
/** The sending actor's document: its `followers` IRI is what addressing names to reach them. */
|
|
31
|
+
actor: Pick<ActivityPub.Actor, "id" | "followers">;
|
|
32
|
+
followers: FollowerStore;
|
|
33
|
+
/** Resolves every other addressed actor to its inbox. */
|
|
34
|
+
resolver: Pick<Resolver, "actor">;
|
|
35
|
+
/** Called once per host; a blocked host receives nothing and its actors are never fetched. */
|
|
36
|
+
blocked: BlockedCheck;
|
|
37
|
+
/** Where `deliver` keeps each origin's failure window. */
|
|
38
|
+
cache: Cache;
|
|
39
|
+
/**
|
|
40
|
+
* Writes one batch of delivery jobs, of any total size; a failure or rejection fails the
|
|
41
|
+
* fan-out as retryable.
|
|
42
|
+
*/
|
|
43
|
+
enqueue: (deliveries: DeliveryInput[]) => Promise<Result<void, Error> | void>;
|
|
44
|
+
/** Followers' inboxes read per store page, and the most deliveries per batch. @default 100 */
|
|
45
|
+
pageSize?: number;
|
|
46
|
+
/** The clock origin failure windows are measured against. @default () => new Date() */
|
|
47
|
+
now?: () => Date;
|
|
48
|
+
}
|
|
49
|
+
/** What a fan-out planned. */
|
|
50
|
+
export interface FanOutResult {
|
|
51
|
+
/** Deliveries enqueued, one per distinct inbox URL. */
|
|
52
|
+
inboxes: number;
|
|
53
|
+
/** Targets left out: blocked hosts, unavailable origins, and actors that would not resolve. */
|
|
54
|
+
skipped: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Enqueues one delivery per distinct inbox the activity is addressed to. The sender's
|
|
58
|
+
* followers collection in `to`, `cc`, `bto` or `bcc` (of the activity or its embedded
|
|
59
|
+
* object) pages through `FollowerStore.inboxes`; every other IRI except `PUBLIC` resolves
|
|
60
|
+
* to its actor's `sharedInbox ?? inbox`, and one that fails to resolve is skipped and
|
|
61
|
+
* counted, so one unreachable mention never holds back delivery to every follower.
|
|
62
|
+
*
|
|
63
|
+
* A batch closes at `pageSize` deliveries. A retried fan-out may enqueue some deliveries
|
|
64
|
+
* twice, which receivers absorb by activity id.
|
|
65
|
+
*
|
|
66
|
+
* @param input - The sending actor and the activity.
|
|
67
|
+
* @param options - The stores, the moderation callback and the job writer.
|
|
68
|
+
* @returns The counts, or `too-large` for an activity over 120 KB, `invalid-document`
|
|
69
|
+
* for one that does not parse, or a retryable `store` or `enqueue` failure.
|
|
70
|
+
* @example let planned = await fanOut(ctx.input, { actor: ACTOR, followers, resolver, blocked, cache, enqueue });
|
|
71
|
+
*/
|
|
72
|
+
export declare function fanOut(input: FanOutInput, options: FanOutOptions): Promise<Result<FanOutResult, ActivityPubError<FanOutErrorCode>>>;
|