fedipod 1.43.0 → 1.45.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 +16 -0
- package/gateway.md +7 -3
- package/lib/client/c2s.mjs +21 -73
- package/lib/core/intake/index.mjs +8 -1
- package/lib/core/intake/received.mjs +78 -0
- package/lib/gateway/front-core.mjs +33 -14
- package/lib/gateway/front-pages.mjs +2 -4
- package/lib/gateway/gateway-core.mjs +46 -14
- package/lib/gateway/held-mail.mjs +7 -0
- package/lib/gateway/httpsig.mjs +6 -1
- package/lib/gateway/private-read.mjs +91 -0
- package/lib/pod/urls.mjs +3 -0
- package/lib/server/embed.mjs +25 -0
- package/package.json +3 -3
- package/scripts/check-pod-calls.mjs +1 -1
- package/scripts/start.mjs +13 -0
- package/web/app/boot.mjs +65 -7
- package/web/app/dist/boot.js +764 -10
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/forum-form.mjs +38 -0
- package/web/app/forum-support.mjs +11 -0
- package/web/app/index.html +29 -4
- package/web/app/signup-forum.mjs +52 -0
- package/web/app/signup.mjs +2 -2
- package/web/front/new-account.html +8 -16
- package/web/front/new-account.js +3 -7
- package/web/front/install.sh +0 -69
package/lib/server/embed.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { Agent } from '../core/agent.mjs';
|
|
|
20
20
|
import { RemotePod } from '../core/remote.mjs';
|
|
21
21
|
import { apUrls, DEFAULT_ROOT } from '../core/wire.mjs';
|
|
22
22
|
import { handleDelivery } from '../gateway/gateway-core.mjs';
|
|
23
|
+
import { signedReader, mayRead } from '../gateway/private-read.mjs';
|
|
23
24
|
import { writeJsonAtomic } from '../shared/files.mjs';
|
|
24
25
|
import { buildAdminSurface } from '../surface/index.mjs';
|
|
25
26
|
import { FixedAuthorities } from '../shared/guard.mjs';
|
|
@@ -260,6 +261,30 @@ export async function deliverToInbox(agent, request, { podPut, gatewayWebId = nu
|
|
|
260
261
|
return handleDelivery(request, ident, { podPut, fetchImpl });
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
/**
|
|
265
|
+
* A signed read of a followers-only or direct post at its address on this
|
|
266
|
+
* pod (ActivityPub §3.2), answered to a server the post was sent to. The
|
|
267
|
+
* rule is lib/gateway/private-read.mjs's; here the post is read through the
|
|
268
|
+
* server's own store and the followers come from the identity's live state.
|
|
269
|
+
* Returns { status, reason, body }: 200 with the document, else 404 with why.
|
|
270
|
+
*/
|
|
271
|
+
export async function readPrivateForSigner(agent, request, { read, fetchImpl = fetch } = {}) {
|
|
272
|
+
const u = agent.urls;
|
|
273
|
+
const toPod = (x) => (u.toPod ? u.toPod(x) : x);
|
|
274
|
+
const url = request.url.split('?')[0];
|
|
275
|
+
const { actor, reason } = await signedReader(request, { fetchImpl, origin: new URL(request.url).origin });
|
|
276
|
+
if (!actor) return { status: 404, reason: `refused — ${reason}` };
|
|
277
|
+
const text = await Promise.resolve().then(() => read(url)).catch(() => null);
|
|
278
|
+
let doc = null;
|
|
279
|
+
try { doc = text ? JSON.parse(text) : null; } catch { doc = null; }
|
|
280
|
+
if (!doc) return { status: 404, reason: `refused — nothing at ${url} (${actor})` };
|
|
281
|
+
const followers = (agent.store.getContacts().followers || []).map((f) => f?.actor).filter(Boolean);
|
|
282
|
+
if (!mayRead(doc, actor, { followersUrls: [u.followers, toPod(u.followers)], followers })) {
|
|
283
|
+
return { status: 404, reason: `refused — ${actor} is not in its audience` };
|
|
284
|
+
}
|
|
285
|
+
return { status: 200, reason: `served to ${actor}`, body: text };
|
|
286
|
+
}
|
|
287
|
+
|
|
263
288
|
/**
|
|
264
289
|
* Bring one identity up inside the server.
|
|
265
290
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fedipod",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"description": "Standalone single-actor ActivityPub agent whose wire face, RDF truth and state all live on a Solid pod (CSS). Bundles a Phanpy UI and a Mastodon client-API facade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
"gateway.md"
|
|
52
52
|
],
|
|
53
53
|
"scripts": {
|
|
54
|
-
"start": "node
|
|
54
|
+
"start": "node scripts/start.mjs",
|
|
55
55
|
"install-service": "node bin/fedipod.mjs install-service",
|
|
56
56
|
"uninstall-service": "node bin/fedipod.mjs uninstall-service",
|
|
57
57
|
"prepublishOnly": "npm test",
|
|
58
|
-
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/copy-mode-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node packages/fedipod-bb/scripts/link-fedipod.mjs && node --test packages/fedipod-bb/test/*.test.mjs && node scripts/check-sizes.mjs",
|
|
58
|
+
"test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/copy-mode-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node packages/fedipod-bb/scripts/link-fedipod.mjs && node --test packages/fedipod-bb/test/*.test.mjs claude/smoke-tests/forum-signup-form.test.mjs claude/smoke-tests/door-verify.test.mjs && node scripts/check-sizes.mjs && node claude/validation/browser-agent/all.mjs",
|
|
59
59
|
"verify-ui": "node scripts/verify-ui.mjs",
|
|
60
60
|
"build:sw": "node scripts/build-app.mjs --entry web/app/sw-src.mjs --out web/app/dist/sw.js",
|
|
61
61
|
"build:boot": "node scripts/build-app.mjs --entry web/app/boot.mjs --out web/app/dist/boot.js",
|
|
@@ -326,7 +326,7 @@ if (!failures) pass('every in-scope caller reaches the pod through a named opera
|
|
|
326
326
|
try {
|
|
327
327
|
const { frontPages } = await import('fedipod/front-pages');
|
|
328
328
|
const pages = frontPages();
|
|
329
|
-
const missing = ['signupPage', 'runPage', 'adminPage', 'noticesPage', 'authBundle'
|
|
329
|
+
const missing = ['signupPage', 'runPage', 'adminPage', 'noticesPage', 'authBundle'].filter((k) => !pages[k])
|
|
330
330
|
.concat(Object.entries(pages.pageScripts).filter(([, v]) => !v).map(([k]) => k));
|
|
331
331
|
if (missing.length) { fail(`frontPages() is missing ${missing.join(', ')}`); bad++; }
|
|
332
332
|
} catch (e) { fail(`frontPages(): ${e.message}`); bad++; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// start.mjs — what `npm start` says in this repository. Nothing starts here:
|
|
2
|
+
// FediPod runs in a browser at a Gateway, and a Gateway is deployed, not
|
|
3
|
+
// started. Until 2026-09-28 `npm start` started the DeviceAgent, which is
|
|
4
|
+
// deprecated and opened a setup page asking for a pod password.
|
|
5
|
+
console.log(`FediPod is not started from this directory.
|
|
6
|
+
|
|
7
|
+
In a browser: open a Gateway such as https://fedipod.net and create an
|
|
8
|
+
account. Nothing is installed.
|
|
9
|
+
A Gateway of your own: deploy this repository to Netlify — netlify/README.md
|
|
10
|
+
A pod server with FediPod built in: packages/fedipod-server/README.md
|
|
11
|
+
A forum: packages/fedipod-bb/README.md, or "A forum" on a
|
|
12
|
+
Gateway's sign-up page
|
|
13
|
+
`);
|
package/web/app/boot.mjs
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// Returning: fedipodSignin({ address }) — redirect to the pod's login.
|
|
16
16
|
// On every load: fedipodOnLoad() — finish a redirect, or restore, then boot.
|
|
17
17
|
import { signUp, moveIn, readAccount, handleProblem } from './signup.mjs';
|
|
18
|
+
import { signUpForum, forumsOffered } from './signup-forum.mjs';
|
|
19
|
+
import { categoriesFrom, moderatorFrom } from './forum-form.mjs';
|
|
18
20
|
import { podRootPath, chosenRoot } from '../../lib/core/place.mjs';
|
|
19
21
|
import * as podActor from '../../lib/pod/actor.mjs';
|
|
20
22
|
import * as podState from '../../lib/pod/state.mjs';
|
|
@@ -134,6 +136,14 @@ window.fedipodSignup = async ({ onStep, ...answers }) => {
|
|
|
134
136
|
await signUp(answers, { session, onStep, frontOrigin: location.origin });
|
|
135
137
|
await bootWorker({ reset: true });
|
|
136
138
|
};
|
|
139
|
+
// A forum, on the pod the person just made for it (signup-forum.mjs): written
|
|
140
|
+
// on the pod, its addresses taken here, this site named its keeper. No agent
|
|
141
|
+
// boots here; the forum runs at this site from its first run on.
|
|
142
|
+
window.fedipodForumSignup = async ({ onStep, ...answers }) => {
|
|
143
|
+
const session = await getSession();
|
|
144
|
+
if (!session) throw new Error("Sign in at the forum's pod first.");
|
|
145
|
+
return signUpForum(answers, { session, onStep, frontOrigin: location.origin });
|
|
146
|
+
};
|
|
137
147
|
// An account whose address lives at another gateway, moving here: the pod's
|
|
138
148
|
// part (signup.mjs moveIn), then a boot under the new ids, which finishes
|
|
139
149
|
// the move by telling the old gateway and the followers (gateway-move.mjs).
|
|
@@ -462,6 +472,9 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
462
472
|
$('container').placeholder = podRootPath(signedPod);
|
|
463
473
|
$('container-field').hidden = !!moveFrom;
|
|
464
474
|
$('movein-note').hidden = !moveFrom;
|
|
475
|
+
// What was being made was chosen before the pod's login; the page came
|
|
476
|
+
// back fresh from it.
|
|
477
|
+
try { const m = sessionStorage.getItem('fedipod:making'); if (m) for (const r of f().making) r.checked = r.value === m; } catch { /* storage blocked */ }
|
|
465
478
|
if (moveFrom) {
|
|
466
479
|
$('movein-from').textContent = moveFrom.address;
|
|
467
480
|
if (!f().handle.value) f().handle.value = moveFrom.config.handle;
|
|
@@ -499,7 +512,10 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
499
512
|
// from the signed-in pod, not guessed from the provider.
|
|
500
513
|
const pathPod = () => { try { return new URL(signedPod).pathname !== '/'; } catch { return false; } };
|
|
501
514
|
// A move between gateways is an address at a gateway: no choice either.
|
|
502
|
-
|
|
515
|
+
// What is being made: a person's account, or a forum (offered only where
|
|
516
|
+
// this site hosts forums). A forum's address is always at this site.
|
|
517
|
+
const making = () => (forumsOffered() && f().making?.value === 'forum' ? 'forum' : 'account');
|
|
518
|
+
const shape = () => ((pathPod() || moveFrom || making() === 'forum') ? 'front' : f().shape.value);
|
|
503
519
|
const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape(), container: f().container.value.trim() });
|
|
504
520
|
// Where the data will be, in full, as the person types.
|
|
505
521
|
const previewPlace = () => {
|
|
@@ -516,10 +532,30 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
516
532
|
// The shape choice is fixed for a path pod, and open for a host-root pod.
|
|
517
533
|
// A path pod has no choice to make: the radios go away and the note says why.
|
|
518
534
|
const applyShape = () => {
|
|
519
|
-
const fixed = pathPod() || !!moveFrom;
|
|
535
|
+
const fixed = pathPod() || !!moveFrom || making() === 'forum';
|
|
520
536
|
for (const r of f().shape) { if (fixed) r.checked = r.value === 'front'; }
|
|
521
537
|
$('shape-group').hidden = fixed;
|
|
522
|
-
$('shape-hint').hidden = !fixed || !!moveFrom; // a move
|
|
538
|
+
$('shape-hint').hidden = !fixed || !!moveFrom || making() === 'forum'; // a move and a forum explain themselves
|
|
539
|
+
};
|
|
540
|
+
// The forum's fields show for a forum; the person's container and address
|
|
541
|
+
// choices go, and the words say what is being made.
|
|
542
|
+
const applyMaking = () => {
|
|
543
|
+
const forum = making() === 'forum';
|
|
544
|
+
$('making-hint').hidden = !forum;
|
|
545
|
+
$('forum-fields').hidden = !forum;
|
|
546
|
+
$('container-field').hidden = forum || !!moveFrom;
|
|
547
|
+
$('identity-legend').textContent = forum ? 'Your forum' : 'Your Fediverse Identity';
|
|
548
|
+
$('handle-label').textContent = forum ? 'Forum handle' : 'Fediverse handle';
|
|
549
|
+
$('handle-hint').textContent = forum
|
|
550
|
+
? `The name in the forum's address, @handle@${location.host}. This is permanent. Letters, digits, and hyphens only.`
|
|
551
|
+
: 'The name in your Fediverse address. This is permanent. Letters, digits, and hyphens only.';
|
|
552
|
+
$('handle').placeholder = forum ? "the handle part of the forum's address: @HANDLE@server" : 'the handle part of your address: @HANDLE@server';
|
|
553
|
+
$('signed-pod-label').textContent = forum ? "The forum's pod" : 'Your pod';
|
|
554
|
+
$('addr-label').textContent = forum ? "The forum's address" : 'Your Fediverse address';
|
|
555
|
+
$('after-hint').textContent = forum
|
|
556
|
+
? "The handle, the categories' short names and the pod cannot be changed afterwards. Everything else you set on the forum's settings page."
|
|
557
|
+
: 'The handle and the pod cannot be changed afterwards. Everything else — display name, bio, pictures — you set later in the client.';
|
|
558
|
+
$('submit').textContent = forum ? 'Create the forum' : moveFrom ? 'Move your account here' : 'Create your Fediverse account';
|
|
523
559
|
};
|
|
524
560
|
const applyMode = () => {
|
|
525
561
|
const existing = f().mode.value === 'existing';
|
|
@@ -529,8 +565,10 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
529
565
|
$('register-link').href = url || '#';
|
|
530
566
|
$('register-link').textContent = providerHost() ? `Create your pod at ${providerHost()}` : 'Create your pod at your provider';
|
|
531
567
|
};
|
|
532
|
-
for (const el of $('form').elements) for (const evt of ['input', 'change']) el.addEventListener(evt, () => { applyMode(); applyShape(); previewAddr(); previewPlace(); });
|
|
568
|
+
for (const el of $('form').elements) for (const evt of ['input', 'change']) el.addEventListener(evt, () => { applyMode(); applyMaking(); applyShape(); previewAddr(); previewPlace(); });
|
|
533
569
|
applyMode();
|
|
570
|
+
$('making-group').hidden = !forumsOffered();
|
|
571
|
+
applyMaking();
|
|
534
572
|
|
|
535
573
|
// Step machine: one screen at a time, each gated by its own validation.
|
|
536
574
|
const STEP_IDS = ['step-1', 'step-2'];
|
|
@@ -538,7 +576,7 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
538
576
|
const goStep = (n) => {
|
|
539
577
|
STEP_IDS.forEach((id, i) => { $(id).hidden = i !== n - 1; });
|
|
540
578
|
$('err-1').textContent = ''; $('form-error').textContent = '';
|
|
541
|
-
if (n === 2) { applyShape(); previewAddr(); previewPlace(); $('index-ask').hidden = true; }
|
|
579
|
+
if (n === 2) { applyMaking(); applyShape(); previewAddr(); previewPlace(); $('index-ask').hidden = true; }
|
|
542
580
|
if (FOCUS[n]) $(FOCUS[n]).focus();
|
|
543
581
|
};
|
|
544
582
|
const validateStep1 = () => {
|
|
@@ -547,6 +585,12 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
547
585
|
};
|
|
548
586
|
const validateStep2 = () => {
|
|
549
587
|
const hp = window.fedipodHandleProblem(f().handle.value.trim().toLowerCase());
|
|
588
|
+
if (making() === 'forum') {
|
|
589
|
+
if (hp) return `Forum handle: ${hp}`;
|
|
590
|
+
if (!f().forumName.value.trim()) return 'Forum name: a name is required.';
|
|
591
|
+
try { categoriesFrom(f().categories.value); moderatorFrom(f().moderator.value, location.origin); } catch (e) { return e.message; }
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
550
594
|
if (hp) return `Fediverse handle: ${hp}`;
|
|
551
595
|
if (!moveFrom) {
|
|
552
596
|
const where = chosenRoot(signedPod, f().container.value).problem;
|
|
@@ -561,6 +605,7 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
561
605
|
$('to-2').addEventListener('click', async () => {
|
|
562
606
|
const e = validateStep1(); if (e) { $('err-1').textContent = e; return; }
|
|
563
607
|
$('err-1').textContent = ''; $('to-2').disabled = true;
|
|
608
|
+
try { sessionStorage.setItem('fedipod:making', making()); } catch { /* storage blocked */ }
|
|
564
609
|
try { await window.fedipodPodLogin({ issuer: providerUrl() }); }
|
|
565
610
|
catch (err) { $('err-1').textContent = err.message || String(err); $('to-2').disabled = false; }
|
|
566
611
|
});
|
|
@@ -578,15 +623,28 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
578
623
|
const LABELS = { pod: 'Checking your pod', keys: 'Making your signing key', gateway: 'Connecting your mail door',
|
|
579
624
|
place: 'Recording where your account lives' };
|
|
580
625
|
const MOVE_LABELS = { pod: 'Reading your account on your pod', gateway: 'Taking your address here', keys: 'Moving your key and account record' };
|
|
626
|
+
const FORUM_LABELS = { pod: "Checking the forum's pod", write: 'Writing the forum on its pod', attach: 'Taking its addresses here', keep: 'Making this site its keeper' };
|
|
581
627
|
// Run the setup. `createIndex` is the person's yes to a new public type index.
|
|
582
628
|
const run = async (createIndex = false) => {
|
|
583
629
|
const a = { ...answers(), ...(createIndex ? { createIndex: true } : {}) };
|
|
584
630
|
$('pane-form').hidden = true; $('running').hidden = false;
|
|
585
|
-
$('running-title').textContent = 'Setting up…'; $('run-error').textContent = ''; $('run-actions').hidden = true;
|
|
631
|
+
$('running-title').textContent = 'Setting up…'; $('run-error').textContent = ''; $('run-actions').hidden = true; $('run-done').hidden = true;
|
|
586
632
|
const steps = $('steps'); steps.textContent = ''; const mark = {};
|
|
587
|
-
const
|
|
633
|
+
const forum = making() === 'forum';
|
|
634
|
+
const labels = forum ? FORUM_LABELS : moveFrom ? MOVE_LABELS : LABELS;
|
|
588
635
|
const onStep = (k, st) => { if (!mark[k]) { const li = document.createElement('li'); steps.appendChild(li); mark[k] = li; } mark[k].textContent = (st === 'ok' ? '✓ ' : st === 'running' ? '… ' : '') + (labels[k] || k); };
|
|
589
636
|
try {
|
|
637
|
+
if (forum) {
|
|
638
|
+
const made = await window.fedipodForumSignup({ handle: a.handle, name: f().forumName.value, categories: f().categories.value,
|
|
639
|
+
moderator: f().moderator.value, onStep });
|
|
640
|
+
try { sessionStorage.removeItem('fedipod:making'); } catch { /* storage blocked */ }
|
|
641
|
+
// The forum's page, read through this site; its first run publishes
|
|
642
|
+
// the forum within the minute.
|
|
643
|
+
$('running-title').textContent = `Your forum is ready: ${made.address}`;
|
|
644
|
+
$('run-done-link').href = `${location.origin}/bb/?forum=${encodeURIComponent(made.handle)}`;
|
|
645
|
+
$('run-done').hidden = false;
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
590
648
|
if (moveFrom) await window.fedipodMoveIn({ handle: a.handle, onStep });
|
|
591
649
|
else await window.fedipodSignup({ ...a, onStep });
|
|
592
650
|
location.href = '/admin/client/';
|