fedipod 0.19.0 → 1.2.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/README.md +104 -134
- package/architecture.md +41 -0
- package/bin/fedipod.mjs +45 -2253
- package/browser.svg +72 -0
- package/cli.md +0 -4
- package/gateway.md +160 -0
- package/groups.md +0 -6
- package/gui.md +7 -11
- package/installed-agent.md +97 -0
- package/lib/{c2s.mjs → client/c2s.mjs} +8 -3
- package/lib/{localapi.mjs → client/localapi.mjs} +2 -2
- package/lib/client/masto/accounts.mjs +264 -0
- package/lib/client/masto/body.mjs +69 -0
- package/lib/client/masto/index.mjs +183 -0
- package/lib/client/masto/instance.mjs +104 -0
- package/lib/client/masto/media.mjs +133 -0
- package/lib/client/masto/oauth.mjs +599 -0
- package/lib/client/masto/render.mjs +459 -0
- package/lib/client/masto/statuses.mjs +331 -0
- package/lib/client/masto/timelines.mjs +316 -0
- package/lib/{streaming.mjs → client/streaming.mjs} +1 -1
- package/lib/{acctfeed.mjs → connections/acctfeed.mjs} +1 -1
- package/lib/{atproto.mjs → connections/atproto.mjs} +15 -16
- package/lib/{bskygroup.mjs → connections/bskygroup.mjs} +1 -1
- package/lib/{fediacct.mjs → connections/fediacct.mjs} +31 -35
- package/lib/{import.mjs → connections/import.mjs} +1 -1
- package/lib/{tagfeed.mjs → connections/tagfeed.mjs} +3 -3
- package/lib/connections/vault.mjs +114 -0
- package/lib/core/as2.mjs +124 -0
- package/lib/core/contexts/activitystreams.json +379 -0
- package/lib/core/contexts/did-v1.json +57 -0
- package/lib/core/contexts/fep-5711.json +36 -0
- package/lib/core/contexts/gotosocial.json +86 -0
- package/lib/core/contexts/identity-v1.json +152 -0
- package/lib/core/contexts/index.mjs +45 -0
- package/lib/core/contexts/join-lemmy.json +33 -0
- package/lib/core/contexts/joinmastodon.json +28 -0
- package/lib/core/contexts/map.json +16 -0
- package/lib/core/contexts/miscellany.json +19 -0
- package/lib/core/contexts/schemaorg.json +8845 -0
- package/lib/core/contexts/security-data-integrity-v1.json +78 -0
- package/lib/core/contexts/security-data-integrity-v2.json +81 -0
- package/lib/core/contexts/security-multikey-v1.json +35 -0
- package/lib/core/contexts/security-v1.json +74 -0
- package/lib/core/contexts/webfinger.json +10 -0
- package/lib/{deliver.mjs → core/deliver.mjs} +2 -2
- package/lib/core/intake/activities.mjs +437 -0
- package/lib/core/intake/activity.mjs +240 -0
- package/lib/core/intake/channel.mjs +144 -0
- package/lib/core/intake/group.mjs +222 -0
- package/lib/core/intake/index.mjs +629 -0
- package/lib/core/intake/notes.mjs +288 -0
- package/lib/core/intake/verify.mjs +141 -0
- package/lib/{keys.mjs → core/keys.mjs} +1 -1
- package/lib/core/publisher/collections.mjs +229 -0
- package/lib/core/publisher/index.mjs +421 -0
- package/lib/core/publisher/notes.mjs +188 -0
- package/lib/core/publisher/questions.mjs +233 -0
- package/lib/core/publisher/restore.mjs +196 -0
- package/lib/core/shapes/activitystreams.ttl +129 -0
- package/lib/core/shapes/index.mjs +107 -0
- package/lib/core/shapes/shapes-text.mjs +13 -0
- package/lib/{social.mjs → core/social.mjs} +2 -2
- package/lib/{store.mjs → core/store.mjs} +4 -0
- package/lib/{wire.mjs → core/wire.mjs} +2 -2
- package/lib/device/admin/index.mjs +13 -0
- package/lib/device/admin/origins.mjs +35 -0
- package/lib/device/admin/routes/connections.mjs +144 -0
- package/lib/device/admin/routes/gateway.mjs +199 -0
- package/lib/device/admin/routes/lifecycle.mjs +191 -0
- package/lib/device/admin/routes/owner.mjs +322 -0
- package/lib/device/admin/routes/setup.mjs +393 -0
- package/lib/device/admin/routes/social.mjs +188 -0
- package/lib/device/admin/server.mjs +95 -0
- package/lib/device/admin/static.mjs +244 -0
- package/lib/device/admin/surface.mjs +274 -0
- package/lib/device/cli/commands/account.mjs +586 -0
- package/lib/device/cli/commands/run.mjs +278 -0
- package/lib/device/cli/commands/service.mjs +221 -0
- package/lib/device/cli/commands/setup.mjs +410 -0
- package/lib/device/cli/commands/state.mjs +559 -0
- package/lib/device/cli/context.mjs +288 -0
- package/lib/{migrate.mjs → device/migrate.mjs} +1 -1
- package/lib/{remote.mjs → device/remote.mjs} +3 -3
- package/lib/{setup.mjs → device/setup.mjs} +3 -3
- package/lib/{update.mjs → device/update.mjs} +1 -1
- package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
- package/lib/{front-core.mjs → gateway/front-core.mjs} +3 -3
- package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +1 -1
- package/lib/{httpsig.mjs → gateway/httpsig.mjs} +1 -1
- package/lib/{embed.mjs → server/embed.mjs} +131 -22
- package/lib/{links.mjs → shared/links.mjs} +1 -1
- package/lib/{ua.mjs → shared/ua.mjs} +1 -1
- package/package.json +16 -2
- package/run-agent.mjs +33 -25
- package/scripts/build-app.mjs +4 -0
- package/scripts/check-pod-calls.mjs +8 -2
- package/scripts/refresh-contexts.mjs +39 -0
- package/web/admin/actors.js +145 -0
- package/web/admin/common.js +23 -0
- package/web/admin/connections.js +112 -0
- package/web/admin/gateway.js +111 -0
- package/web/admin/group.js +258 -0
- package/web/admin/index.html +7 -1
- package/web/admin/record.js +378 -0
- package/web/admin/setup/index.html +1 -0
- package/web/admin/setup/setup.js +2 -13
- package/web/admin/upkeep.js +170 -0
- package/web/app/README.md +6 -6
- package/web/app/admin-facade.mjs +3 -3
- package/web/app/agent.mjs +12 -12
- package/web/app/atproto-browser.mjs +1 -1
- package/web/app/deliver-relay.mjs +1 -1
- package/web/app/dist/sw.js +21684 -5415
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/fediacct-browser.mjs +1 -1
- package/web/app/shims/shapes-text.mjs +8 -0
- package/web/app/site/admin/actors.js +145 -0
- package/web/app/site/admin/common.js +23 -0
- package/web/app/site/admin/connections.js +112 -0
- package/web/app/site/admin/gateway.js +111 -0
- package/web/app/site/admin/group.js +258 -0
- package/web/app/site/admin/index.html +7 -1
- package/web/app/site/admin/record.js +378 -0
- package/web/app/site/admin/setup/index.html +1 -0
- package/web/app/site/admin/setup/setup.js +2 -13
- package/web/app/site/admin/upkeep.js +170 -0
- package/web/app/site/sw.js +21684 -5415
- package/web/app/sw-src.mjs +17 -2
- package/lib/admin.mjs +0 -1913
- package/lib/intake.mjs +0 -1981
- package/lib/mastoapi.mjs +0 -2284
- package/lib/publisher.mjs +0 -1192
- package/web/admin/admin.js +0 -1181
- package/web/app/site/admin/admin.js +0 -1181
- /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
- /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
- /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
- /package/lib/{lease.mjs → core/lease.mjs} +0 -0
- /package/lib/{polls.mjs → core/polls.mjs} +0 -0
- /package/lib/{proof.mjs → core/proof.mjs} +0 -0
- /package/lib/{storage.mjs → core/storage.mjs} +0 -0
- /package/lib/{account.mjs → device/account.mjs} +0 -0
- /package/lib/{certs.mjs → device/certs.mjs} +0 -0
- /package/lib/{export-collections.mjs → device/export-collections.mjs} +0 -0
- /package/lib/{home.mjs → device/home.mjs} +0 -0
- /package/lib/{ports.mjs → device/ports.mjs} +0 -0
- /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
- /package/lib/{safefetch.mjs → shared/safefetch.mjs} +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// media.mjs — an upload on its way to the pod's media container: what an
|
|
2
|
+
// attachment is allowed to be, the multipart reader, and the two media
|
|
3
|
+
// endpoints.
|
|
4
|
+
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import * as podMedia from '../../pod/media.mjs';
|
|
7
|
+
import { readBody } from './body.mjs';
|
|
8
|
+
|
|
9
|
+
// What an attachment is allowed to BE. Anything else is stored as bytes, which
|
|
10
|
+
// a browser downloads rather than runs.
|
|
11
|
+
const ATTACHMENT_KINDS = new Set(['image', 'video', 'audio']);
|
|
12
|
+
// image/* with an exception: an SVG is a document, it carries script, and a
|
|
13
|
+
// browser renders it rather than showing it. Mastodon does not take them as
|
|
14
|
+
// media either.
|
|
15
|
+
const NEVER = new Set(['image/svg+xml', 'image/svg']);
|
|
16
|
+
const OPAQUE = 'application/octet-stream';
|
|
17
|
+
|
|
18
|
+
export function attachmentType(claimed) {
|
|
19
|
+
const t = String(claimed || '').split(';')[0].trim().toLowerCase();
|
|
20
|
+
if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(t) || NEVER.has(t)) return OPAQUE;
|
|
21
|
+
return ATTACHMENT_KINDS.has(t.split('/')[0]) ? t : OPAQUE;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// From the TYPE we accepted, not from the name the client sent — an `.html`
|
|
25
|
+
// suffix on a file stored as octet-stream is what a pod would serve from, and
|
|
26
|
+
// a filename is the client's to choose.
|
|
27
|
+
export function extensionFor(mediaType, filename = '') {
|
|
28
|
+
if (mediaType === OPAQUE) return 'bin';
|
|
29
|
+
const sub = mediaType.split('/')[1].replace(/[^a-z0-9]/g, '');
|
|
30
|
+
const given = String(filename || '').includes('.')
|
|
31
|
+
? filename.split('.').pop().toLowerCase().replace(/[^a-z0-9]/g, '') : '';
|
|
32
|
+
// Keep the client's suffix only when it plainly belongs to the accepted type,
|
|
33
|
+
// so jpg/jpeg and mp4/m4v survive without a table of every media type.
|
|
34
|
+
return given && (given === sub || sub.startsWith(given) || given.startsWith(sub)) ? given : (sub || 'bin');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Minimal multipart/form-data reader for media uploads: string fields plus
|
|
38
|
+
// at most one file part (Mastodon's media endpoints send exactly one).
|
|
39
|
+
export function readMultipart(req, limit = 12e6) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let n = 0;
|
|
43
|
+
req.on('data', c => {
|
|
44
|
+
n += c.length;
|
|
45
|
+
if (n > limit) { reject(new Error('upload too large')); req.destroy(); return; }
|
|
46
|
+
chunks.push(c);
|
|
47
|
+
});
|
|
48
|
+
req.on('error', reject);
|
|
49
|
+
req.on('end', () => {
|
|
50
|
+
try {
|
|
51
|
+
const m = /boundary=(?:"([^"]+)"|([^;]+))/.exec(String(req.headers['content-type'] || ''));
|
|
52
|
+
if (!m) return resolve({ fields: {}, file: null });
|
|
53
|
+
const buf = Buffer.concat(chunks);
|
|
54
|
+
const boundary = Buffer.from('--' + (m[1] || m[2]).trim());
|
|
55
|
+
const fields = {};
|
|
56
|
+
// `file` is the last one seen, which is what the single-file media
|
|
57
|
+
// upload wants. `files` keys them by field name, because the profile
|
|
58
|
+
// editor submits an avatar and a header in one request.
|
|
59
|
+
const files = {};
|
|
60
|
+
let file = null;
|
|
61
|
+
let i = buf.indexOf(boundary);
|
|
62
|
+
while (i >= 0) {
|
|
63
|
+
const start = i + boundary.length;
|
|
64
|
+
if (buf.slice(start, start + 2).toString() === '--') break;
|
|
65
|
+
const next = buf.indexOf(boundary, start);
|
|
66
|
+
if (next < 0) break;
|
|
67
|
+
const part = buf.slice(start + 2, next - 2); // strip the CRLFs framing the part
|
|
68
|
+
const sep = part.indexOf('\r\n\r\n');
|
|
69
|
+
if (sep >= 0) {
|
|
70
|
+
const head = part.slice(0, sep).toString();
|
|
71
|
+
const body = part.slice(sep + 4);
|
|
72
|
+
const name = /name="([^"]*)"/.exec(head)?.[1];
|
|
73
|
+
const filename = /filename="([^"]*)"/.exec(head)?.[1];
|
|
74
|
+
if (filename !== undefined) {
|
|
75
|
+
file = {
|
|
76
|
+
filename,
|
|
77
|
+
contentType: /content-type:\s*([^\r\n]+)/i.exec(head)?.[1]?.trim() || 'application/octet-stream',
|
|
78
|
+
data: body,
|
|
79
|
+
};
|
|
80
|
+
if (name) files[name] = file;
|
|
81
|
+
} else if (name) fields[name] = body.toString();
|
|
82
|
+
}
|
|
83
|
+
i = next;
|
|
84
|
+
}
|
|
85
|
+
resolve({ fields, file, files });
|
|
86
|
+
} catch (e) { reject(e); }
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function handle(api, ctx) {
|
|
92
|
+
const { req, res, pathname, url, send } = ctx; // eslint-disable-line no-unused-vars
|
|
93
|
+
|
|
94
|
+
// Media upload: file → remote pod /ap/media/ (public-Read), entry in the
|
|
95
|
+
// media registry so a later POST /statuses can attach it.
|
|
96
|
+
if ((pathname === '/api/v2/media' || pathname === '/api/v1/media') && req.method === 'POST') {
|
|
97
|
+
const { fields, file } = await readMultipart(req);
|
|
98
|
+
if (!file?.data?.length) return send(422, { error: 'file required' });
|
|
99
|
+
// The client said what this is, and we used to believe it. The media
|
|
100
|
+
// container is world-readable and sits on the pod's own origin — the same
|
|
101
|
+
// origin as the WebID and the ACLs — so a file stored as text/html is a
|
|
102
|
+
// page served from your identity, and script in it runs as you in any
|
|
103
|
+
// browser already logged into that pod. A bearer is not "only you": the
|
|
104
|
+
// facade exists so third-party clients can connect, tokens last 90 days,
|
|
105
|
+
// and no scope is enforced. So any client you authorize could leave that
|
|
106
|
+
// page behind.
|
|
107
|
+
const mediaType = attachmentType(file.contentType);
|
|
108
|
+
const ext = extensionFor(mediaType, file.filename);
|
|
109
|
+
const slug = new Date().toISOString().slice(0, 10) + '-' + crypto.randomBytes(4).toString('hex') + '.' + ext;
|
|
110
|
+
const mediaUrl = api.urls.media + slug;
|
|
111
|
+
await api.agent.publisher.ensureMediaContainer();
|
|
112
|
+
await podMedia.write(api.agent.remote, mediaUrl, file.data, mediaType);
|
|
113
|
+
const entry = { url: mediaUrl, mediaType, description: fields.description || '' };
|
|
114
|
+
const id = api.store.idFor(mediaUrl);
|
|
115
|
+
api.store.setMedia(id, entry);
|
|
116
|
+
return send(200, api.mediaJson({ id, ...entry }));
|
|
117
|
+
}
|
|
118
|
+
const mMedia = /^\/api\/v1\/media\/([a-f0-9]+)$/.exec(pathname);
|
|
119
|
+
if (mMedia) {
|
|
120
|
+
const entry = api.store.getMedia()[mMedia[1]];
|
|
121
|
+
if (!entry) return send(404, { error: 'Record not found' });
|
|
122
|
+
if (req.method === 'PUT') {
|
|
123
|
+
const body = await readBody(req);
|
|
124
|
+
if (typeof body.description === 'string') {
|
|
125
|
+
entry.description = body.description;
|
|
126
|
+
api.store.setMedia(mMedia[1], entry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return send(200, api.mediaJson({ id: mMedia[1], ...entry }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|