fedipod 1.0.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.
Files changed (143) hide show
  1. package/README.md +4 -4
  2. package/bin/fedipod.mjs +45 -2253
  3. package/gateway.md +1 -1
  4. package/lib/{c2s.mjs → client/c2s.mjs} +8 -3
  5. package/lib/{localapi.mjs → client/localapi.mjs} +2 -2
  6. package/lib/client/masto/accounts.mjs +264 -0
  7. package/lib/client/masto/body.mjs +69 -0
  8. package/lib/client/masto/index.mjs +183 -0
  9. package/lib/client/masto/instance.mjs +104 -0
  10. package/lib/client/masto/media.mjs +133 -0
  11. package/lib/client/masto/oauth.mjs +599 -0
  12. package/lib/client/masto/render.mjs +459 -0
  13. package/lib/client/masto/statuses.mjs +331 -0
  14. package/lib/client/masto/timelines.mjs +316 -0
  15. package/lib/{streaming.mjs → client/streaming.mjs} +1 -1
  16. package/lib/{acctfeed.mjs → connections/acctfeed.mjs} +1 -1
  17. package/lib/{atproto.mjs → connections/atproto.mjs} +15 -16
  18. package/lib/{bskygroup.mjs → connections/bskygroup.mjs} +1 -1
  19. package/lib/{fediacct.mjs → connections/fediacct.mjs} +31 -35
  20. package/lib/{import.mjs → connections/import.mjs} +1 -1
  21. package/lib/{tagfeed.mjs → connections/tagfeed.mjs} +3 -3
  22. package/lib/connections/vault.mjs +114 -0
  23. package/lib/core/as2.mjs +124 -0
  24. package/lib/core/contexts/activitystreams.json +379 -0
  25. package/lib/core/contexts/did-v1.json +57 -0
  26. package/lib/core/contexts/fep-5711.json +36 -0
  27. package/lib/core/contexts/gotosocial.json +86 -0
  28. package/lib/core/contexts/identity-v1.json +152 -0
  29. package/lib/core/contexts/index.mjs +45 -0
  30. package/lib/core/contexts/join-lemmy.json +33 -0
  31. package/lib/core/contexts/joinmastodon.json +28 -0
  32. package/lib/core/contexts/map.json +16 -0
  33. package/lib/core/contexts/miscellany.json +19 -0
  34. package/lib/core/contexts/schemaorg.json +8845 -0
  35. package/lib/core/contexts/security-data-integrity-v1.json +78 -0
  36. package/lib/core/contexts/security-data-integrity-v2.json +81 -0
  37. package/lib/core/contexts/security-multikey-v1.json +35 -0
  38. package/lib/core/contexts/security-v1.json +74 -0
  39. package/lib/core/contexts/webfinger.json +10 -0
  40. package/lib/{deliver.mjs → core/deliver.mjs} +2 -2
  41. package/lib/core/intake/activities.mjs +437 -0
  42. package/lib/core/intake/activity.mjs +240 -0
  43. package/lib/core/intake/channel.mjs +144 -0
  44. package/lib/core/intake/group.mjs +222 -0
  45. package/lib/core/intake/index.mjs +629 -0
  46. package/lib/core/intake/notes.mjs +288 -0
  47. package/lib/core/intake/verify.mjs +141 -0
  48. package/lib/{keys.mjs → core/keys.mjs} +1 -1
  49. package/lib/core/publisher/collections.mjs +229 -0
  50. package/lib/core/publisher/index.mjs +421 -0
  51. package/lib/core/publisher/notes.mjs +188 -0
  52. package/lib/core/publisher/questions.mjs +233 -0
  53. package/lib/core/publisher/restore.mjs +196 -0
  54. package/lib/core/shapes/activitystreams.ttl +129 -0
  55. package/lib/core/shapes/index.mjs +107 -0
  56. package/lib/core/shapes/shapes-text.mjs +13 -0
  57. package/lib/{social.mjs → core/social.mjs} +2 -2
  58. package/lib/{store.mjs → core/store.mjs} +4 -0
  59. package/lib/{wire.mjs → core/wire.mjs} +2 -2
  60. package/lib/device/admin/index.mjs +13 -0
  61. package/lib/device/admin/origins.mjs +35 -0
  62. package/lib/device/admin/routes/connections.mjs +144 -0
  63. package/lib/device/admin/routes/gateway.mjs +199 -0
  64. package/lib/device/admin/routes/lifecycle.mjs +191 -0
  65. package/lib/device/admin/routes/owner.mjs +322 -0
  66. package/lib/device/admin/routes/setup.mjs +393 -0
  67. package/lib/device/admin/routes/social.mjs +188 -0
  68. package/lib/device/admin/server.mjs +95 -0
  69. package/lib/device/admin/static.mjs +244 -0
  70. package/lib/device/admin/surface.mjs +274 -0
  71. package/lib/device/cli/commands/account.mjs +586 -0
  72. package/lib/device/cli/commands/run.mjs +278 -0
  73. package/lib/device/cli/commands/service.mjs +221 -0
  74. package/lib/device/cli/commands/setup.mjs +410 -0
  75. package/lib/device/cli/commands/state.mjs +559 -0
  76. package/lib/device/cli/context.mjs +288 -0
  77. package/lib/{migrate.mjs → device/migrate.mjs} +1 -1
  78. package/lib/{remote.mjs → device/remote.mjs} +3 -3
  79. package/lib/{setup.mjs → device/setup.mjs} +3 -3
  80. package/lib/{update.mjs → device/update.mjs} +1 -1
  81. package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
  82. package/lib/{front-core.mjs → gateway/front-core.mjs} +3 -3
  83. package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +1 -1
  84. package/lib/{httpsig.mjs → gateway/httpsig.mjs} +1 -1
  85. package/lib/{embed.mjs → server/embed.mjs} +131 -22
  86. package/lib/{links.mjs → shared/links.mjs} +1 -1
  87. package/lib/{ua.mjs → shared/ua.mjs} +1 -1
  88. package/package.json +11 -1
  89. package/run-agent.mjs +33 -25
  90. package/scripts/build-app.mjs +4 -0
  91. package/scripts/check-pod-calls.mjs +8 -2
  92. package/scripts/refresh-contexts.mjs +39 -0
  93. package/web/admin/actors.js +145 -0
  94. package/web/admin/common.js +23 -0
  95. package/web/admin/connections.js +112 -0
  96. package/web/admin/gateway.js +111 -0
  97. package/web/admin/group.js +258 -0
  98. package/web/admin/index.html +7 -1
  99. package/web/admin/record.js +378 -0
  100. package/web/admin/setup/index.html +1 -0
  101. package/web/admin/setup/setup.js +2 -13
  102. package/web/admin/upkeep.js +170 -0
  103. package/web/app/README.md +6 -6
  104. package/web/app/admin-facade.mjs +3 -3
  105. package/web/app/agent.mjs +12 -12
  106. package/web/app/atproto-browser.mjs +1 -1
  107. package/web/app/deliver-relay.mjs +1 -1
  108. package/web/app/dist/sw.js +21684 -5415
  109. package/web/app/dist/sw.js.map +4 -4
  110. package/web/app/fediacct-browser.mjs +1 -1
  111. package/web/app/shims/shapes-text.mjs +8 -0
  112. package/web/app/site/admin/actors.js +145 -0
  113. package/web/app/site/admin/common.js +23 -0
  114. package/web/app/site/admin/connections.js +112 -0
  115. package/web/app/site/admin/gateway.js +111 -0
  116. package/web/app/site/admin/group.js +258 -0
  117. package/web/app/site/admin/index.html +7 -1
  118. package/web/app/site/admin/record.js +378 -0
  119. package/web/app/site/admin/setup/index.html +1 -0
  120. package/web/app/site/admin/setup/setup.js +2 -13
  121. package/web/app/site/admin/upkeep.js +170 -0
  122. package/web/app/site/sw.js +21684 -5415
  123. package/web/app/sw-src.mjs +17 -2
  124. package/lib/admin.mjs +0 -1913
  125. package/lib/intake.mjs +0 -1981
  126. package/lib/mastoapi.mjs +0 -2284
  127. package/lib/publisher.mjs +0 -1192
  128. package/web/admin/admin.js +0 -1181
  129. package/web/app/site/admin/admin.js +0 -1181
  130. /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
  131. /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
  132. /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
  133. /package/lib/{lease.mjs → core/lease.mjs} +0 -0
  134. /package/lib/{polls.mjs → core/polls.mjs} +0 -0
  135. /package/lib/{proof.mjs → core/proof.mjs} +0 -0
  136. /package/lib/{storage.mjs → core/storage.mjs} +0 -0
  137. /package/lib/{account.mjs → device/account.mjs} +0 -0
  138. /package/lib/{certs.mjs → device/certs.mjs} +0 -0
  139. /package/lib/{export-collections.mjs → device/export-collections.mjs} +0 -0
  140. /package/lib/{home.mjs → device/home.mjs} +0 -0
  141. /package/lib/{ports.mjs → device/ports.mjs} +0 -0
  142. /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
  143. /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
+ }