fedipod-server 0.11.0 → 0.13.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 +22 -6
- package/dist/claims.d.ts +8 -0
- package/dist/claims.js +10 -0
- package/dist/handler.d.ts +13 -0
- package/dist/handler.js +56 -11
- package/dist/handler.jsonld +8 -0
- package/dist/store-pod.js +18 -4
- package/lib/{c2s.mjs → client/c2s.mjs} +10 -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 +170 -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/graphview.mjs +269 -0
- 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 +142 -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 +199 -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/server/embed.mjs +405 -0
- package/lib/{links.mjs → shared/links.mjs} +1 -1
- package/lib/{ua.mjs → shared/ua.mjs} +1 -1
- package/package.json +1 -1
- package/run-agent.mjs +33 -25
- 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 +14 -16
- package/web/app/atproto-browser.mjs +1 -1
- package/web/app/boot.mjs +2 -3
- package/web/app/deliver-relay.mjs +1 -1
- package/web/app/dist/boot.js +22 -3
- package/web/app/dist/boot.js.map +2 -2
- package/web/app/dist/sw.js +21913 -5446
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/fediacct-browser.mjs +1 -1
- package/web/app/keys-browser.mjs +27 -4
- package/web/app/shims/shapes-text.mjs +8 -0
- package/web/app/signup.mjs +2 -3
- 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/boot.js +22 -3
- package/web/app/site/sw.js +21913 -5446
- package/web/app/sw-src.mjs +17 -2
- package/lib/admin.mjs +0 -1913
- package/lib/embed.mjs +0 -220
- 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,199 @@
|
|
|
1
|
+
// gateway.mjs — the inbox gateway lifecycle: configure, mode, check, attach
|
|
2
|
+
// through a front, forget.
|
|
3
|
+
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
export async function post(p, body, ctx, req, res) { // eslint-disable-line no-unused-vars
|
|
10
|
+
const { agent, log, allowed, embedded, port, publicOrigin, versionOnDisk, isGroup } = ctx; // eslint-disable-line no-unused-vars
|
|
11
|
+
const json = (res, status, obj) => { ctx.json(res, status, obj); return true; };
|
|
12
|
+
switch (p) {
|
|
13
|
+
// The inbox gateway lifecycle (opt-in). `configure` points at a
|
|
14
|
+
// deployed gateway (and reveals the HMAC secret once, to paste into
|
|
15
|
+
// its env); `mode` walks off→shadow→trust→locked and back, each step
|
|
16
|
+
// reversible; `forget` clears it and restores the pod inbox.
|
|
17
|
+
case '/gateway': {
|
|
18
|
+
const cfg = { ...agent.store.getConfig() };
|
|
19
|
+
const g = { ...(cfg.gateway || {}) };
|
|
20
|
+
const inboxUrl = agent.publisher?.urls?.inbox;
|
|
21
|
+
// Availability is a read: answered before the lease takeover below,
|
|
22
|
+
// which every real gateway change does want.
|
|
23
|
+
if (body.action === 'check') {
|
|
24
|
+
const front = String(body.front || '').replace(/\/+$/, '');
|
|
25
|
+
try { new URL(front); } catch { return json(res, 400, { error: 'front must be a gateway origin URL' }); }
|
|
26
|
+
const chk = await fetch(`${front}/api/handle?handle=${encodeURIComponent(String(body.handle || '').toLowerCase())}`,
|
|
27
|
+
{ headers: { accept: 'application/json' } }).then((r) => r.json()).catch(() => null);
|
|
28
|
+
if (!chk) return json(res, 502, { error: `${front} did not answer its handle check` });
|
|
29
|
+
return json(res, 200, { available: !!chk.available, reason: chk.reason || null });
|
|
30
|
+
}
|
|
31
|
+
await agent.requestTakeover?.();
|
|
32
|
+
const persist = async () => {
|
|
33
|
+
cfg.gateway = g; agent.store.setConfig(cfg);
|
|
34
|
+
if (agent.publisher) agent.publisher.config.gateway = g;
|
|
35
|
+
await agent.store.flush();
|
|
36
|
+
};
|
|
37
|
+
// Fronting renames the actor's ids; the signing key is the same
|
|
38
|
+
// identity's and moves with it, wherever the key record lives.
|
|
39
|
+
const restampKeys = async (actorId) => {
|
|
40
|
+
try {
|
|
41
|
+
const kp = path.join(agent.home, 'keys.json');
|
|
42
|
+
const rec = JSON.parse(fs.readFileSync(kp, 'utf8'));
|
|
43
|
+
if (rec.mintedFor) { rec.mintedFor = actorId; fs.writeFileSync(kp, JSON.stringify(rec)); return; }
|
|
44
|
+
} catch { /* not local — try pod state */ }
|
|
45
|
+
const podRec = agent.store.read('keys.json', null);
|
|
46
|
+
if (podRec?.mintedFor) { podRec.mintedFor = actorId; agent.store.write('keys.json', podRec); await agent.store.flush(); }
|
|
47
|
+
};
|
|
48
|
+
const podActorId = () => {
|
|
49
|
+
const base = cfg.remotePod.endsWith('/') ? cfg.remotePod : `${cfg.remotePod}/`;
|
|
50
|
+
const root = cfg.root ? (cfg.root.endsWith('/') ? cfg.root : `${cfg.root}/`) : 'activitypods-js/';
|
|
51
|
+
return `${base}${root}ap/actor`;
|
|
52
|
+
};
|
|
53
|
+
// The reply first, the restart a beat later — same shape as /update.
|
|
54
|
+
const selfRestart = (why) => setTimeout(() => {
|
|
55
|
+
log(why);
|
|
56
|
+
if (process.env.INVOCATION_ID) { process.exit(1); return; } // systemd: Restart=on-failure respawns
|
|
57
|
+
const child = spawn(process.execPath, process.argv.slice(1),
|
|
58
|
+
{ detached: true, stdio: 'ignore', env: process.env });
|
|
59
|
+
child.unref();
|
|
60
|
+
setTimeout(() => process.exit(0), 300);
|
|
61
|
+
}, 200);
|
|
62
|
+
if (body.action === 'configure') {
|
|
63
|
+
if (!/^https:\/\/\S+$/.test(String(body.url || ''))) return json(res, 400, { error: 'gateway url must be https' });
|
|
64
|
+
if (!/^https?:\/\/\S+$/.test(String(body.webId || ''))) return json(res, 400, { error: 'gateway webId must be a URL' });
|
|
65
|
+
g.url = String(body.url); g.webId = String(body.webId);
|
|
66
|
+
g.hmacSecret = g.hmacSecret || crypto.randomBytes(32).toString('base64');
|
|
67
|
+
g.mode = g.mode || 'off';
|
|
68
|
+
// The fronted actor URL, for a multi-user front that gives this
|
|
69
|
+
// identity a @name@front handle: the agent then publishes and signs
|
|
70
|
+
// under it. Optional; setting it on a published identity renames
|
|
71
|
+
// every id (a Move), so it belongs at setup or a fresh identity.
|
|
72
|
+
if ('frontActor' in body) {
|
|
73
|
+
const fa = String(body.frontActor || '');
|
|
74
|
+
if (fa && !/^https:\/\/\S+\/ap\/actor$/.test(fa)) {
|
|
75
|
+
return json(res, 400, { error: 'frontActor must be an https …/ap/actor URL' });
|
|
76
|
+
}
|
|
77
|
+
g.frontActor = fa || undefined;
|
|
78
|
+
}
|
|
79
|
+
await persist();
|
|
80
|
+
// The one time the secret is returned: the operator pastes it into
|
|
81
|
+
// the gateway's deploy env. /gateway GET never reveals it again.
|
|
82
|
+
return json(res, 200, { ok: true, mode: g.mode, url: g.url, webId: g.webId, hmacSecret: g.hmacSecret });
|
|
83
|
+
}
|
|
84
|
+
if (body.action === 'mode') {
|
|
85
|
+
const target = body.mode;
|
|
86
|
+
if (!['off', 'shadow', 'trust', 'locked'].includes(target)) {
|
|
87
|
+
return json(res, 400, { error: 'mode must be off, shadow, trust or locked' });
|
|
88
|
+
}
|
|
89
|
+
if (target !== 'off' && !g.url) return json(res, 400, { error: 'point at a gateway first (action: configure)' });
|
|
90
|
+
// Locking needs the door's own WebID to name in the ACL. Without it
|
|
91
|
+
// the mode used to persist and the ACL be written for an undefined
|
|
92
|
+
// agent, leaving config and pod disagreeing about who may write.
|
|
93
|
+
if (target === 'locked' && !g.webId) {
|
|
94
|
+
return json(res, 400, {
|
|
95
|
+
error: 'locked needs the gateway\'s WebID — set it with action: configure, webId: <door-webid>',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const prev = g.mode || 'off';
|
|
99
|
+
// The ACL first: a lock that fails must not leave the mode recorded
|
|
100
|
+
// as locked while the pod still accepts anyone's writes.
|
|
101
|
+
if (target === 'locked') await agent.publisher?.lockInboxToGateway(g.webId);
|
|
102
|
+
else if (prev === 'locked' && inboxUrl) await agent.remote.setAcl(inboxUrl, ['Append']);
|
|
103
|
+
g.mode = target;
|
|
104
|
+
await persist();
|
|
105
|
+
// Advertisement flips only when crossing the off boundary (the actor
|
|
106
|
+
// doc's inbox changes → publishProfile republishes, digest-gated).
|
|
107
|
+
if ((prev === 'off') !== (target === 'off')) await agent.publisher?.publishProfile();
|
|
108
|
+
else if (target !== 'off') await agent.publisher?.publishGatewayPolicy().catch(() => {});
|
|
109
|
+
return json(res, 200, { ok: true, mode: g.mode });
|
|
110
|
+
}
|
|
111
|
+
// Attach through a multi-user front (fedipod.net and kin): the agent
|
|
112
|
+
// proves the pod with its own credential — no browser, no password —
|
|
113
|
+
// and the front answers with the door and the receipt secret.
|
|
114
|
+
if (body.action === 'attach') {
|
|
115
|
+
const front = String(body.front || '').replace(/\/+$/, '');
|
|
116
|
+
let fu;
|
|
117
|
+
try { fu = new URL(front); } catch { return json(res, 400, { error: 'front must be the gateway origin, like https://fedipod.net' }); }
|
|
118
|
+
if (fu.protocol !== 'https:' && !/^(localhost|127\.0\.0\.1)$|\.localhost$/.test(fu.hostname)) {
|
|
119
|
+
return json(res, 400, { error: 'front must be https' });
|
|
120
|
+
}
|
|
121
|
+
if (agent.embedded) {
|
|
122
|
+
return json(res, 400, { error: 'this identity runs inside its pod server and has no portable credential — attach from a standalone agent' });
|
|
123
|
+
}
|
|
124
|
+
const named = !!String(body.handle || '').trim();
|
|
125
|
+
let handle = String(body.handle || cfg.handle || '').toLowerCase().trim();
|
|
126
|
+
if (!handle) return json(res, 400, { error: 'a name at the gateway is required' });
|
|
127
|
+
const fronted = body.fronted === true;
|
|
128
|
+
// Availability first, for a clean answer before anything is created.
|
|
129
|
+
const avail = async (h) => fetch(`${front}/api/handle?handle=${encodeURIComponent(h)}`,
|
|
130
|
+
{ headers: { accept: 'application/json' } }).then((r) => r.json()).catch(() => null);
|
|
131
|
+
let chk = await avail(handle);
|
|
132
|
+
if (!chk) return json(res, 502, { error: `${front} did not answer its handle check` });
|
|
133
|
+
// An unnamed door name is plumbing nobody reads: walk to a free
|
|
134
|
+
// variant instead of failing over a label the user never chose.
|
|
135
|
+
if (!chk.available && !named) {
|
|
136
|
+
for (let i = 2; i <= 9 && !chk.available; i++) {
|
|
137
|
+
const cand = `${handle}${i}`;
|
|
138
|
+
const c = await avail(cand);
|
|
139
|
+
if (c?.available) { handle = cand; chk = c; }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!chk.available) return json(res, 409, { error: chk.reason || `the name ${handle} is taken at ${front}` });
|
|
143
|
+
const frontActor = `${front}/u/${handle}/ap/actor`;
|
|
144
|
+
if (g.frontActor && (!fronted || g.frontActor !== frontActor)) {
|
|
145
|
+
return json(res, 400, { error: `this identity already fronts through ${g.frontActor} — changing a published front renames every id; detach first if you mean it` });
|
|
146
|
+
}
|
|
147
|
+
const attach = await agent.remote.session.fetch(`${front}/api/attach`, {
|
|
148
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
149
|
+
body: JSON.stringify({ handle, podHome: agent.urls.home, kind: cfg.kind || 'person', fronted }),
|
|
150
|
+
}).catch(() => null);
|
|
151
|
+
if (!attach) return json(res, 502, { error: `${front} did not answer the attach` });
|
|
152
|
+
const d = await attach.json().catch(() => ({}));
|
|
153
|
+
if (attach.status !== 201) {
|
|
154
|
+
return json(res, attach.status >= 400 && attach.status < 500 ? attach.status : 502,
|
|
155
|
+
{ error: d.error || `attach failed (HTTP ${attach.status})` });
|
|
156
|
+
}
|
|
157
|
+
g.url = String(d.doorInbox || `${front}/u/${handle}/ap/inbox/`);
|
|
158
|
+
if (d.hmacSecret) g.hmacSecret = String(d.hmacSecret);
|
|
159
|
+
if (fronted) g.frontActor = String(d.frontActor || frontActor);
|
|
160
|
+
if (!g.mode || g.mode === 'off') g.mode = 'shadow';
|
|
161
|
+
await persist();
|
|
162
|
+
// Inbox-only applies live: the actor republishes advertising the
|
|
163
|
+
// door. A front carries new ids, which are wired at startup — so a
|
|
164
|
+
// fronted attach restarts this agent itself, the reply going out
|
|
165
|
+
// first so it is not taken down with the process.
|
|
166
|
+
if (!fronted) {
|
|
167
|
+
await agent.publisher?.publishProfile();
|
|
168
|
+
await agent.publisher?.publishGatewayPolicy?.().catch(() => {});
|
|
169
|
+
return json(res, 200, { ok: true, mode: g.mode, url: g.url });
|
|
170
|
+
}
|
|
171
|
+
await restampKeys(g.frontActor);
|
|
172
|
+
json(res, 200, { ok: true, mode: g.mode, url: g.url,
|
|
173
|
+
frontActor: g.frontActor, address: d.address || null, restarting: true });
|
|
174
|
+
selfRestart('restarting to publish under the front');
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (body.action === 'forget') {
|
|
178
|
+
const wasLocked = g.mode === 'locked';
|
|
179
|
+
const wasFronted = !!g.frontActor;
|
|
180
|
+
delete cfg.gateway; agent.store.setConfig(cfg);
|
|
181
|
+
if (agent.publisher) agent.publisher.config.gateway = undefined;
|
|
182
|
+
await agent.store.flush();
|
|
183
|
+
if (wasLocked && inboxUrl) await agent.remote.setAcl(inboxUrl, ['Append']).catch(() => {});
|
|
184
|
+
if (wasFronted) {
|
|
185
|
+
// Going home renames every id back to the pod: key and process
|
|
186
|
+
// follow, the same way attach came.
|
|
187
|
+
await restampKeys(podActorId());
|
|
188
|
+
json(res, 200, { ok: true, mode: 'off', forgotten: true, restarting: true });
|
|
189
|
+
selfRestart('restarting under the pod\'s own ids');
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
await agent.publisher?.publishProfile(); // re-advertise the pod inbox
|
|
193
|
+
return json(res, 200, { ok: true, mode: 'off', forgotten: true });
|
|
194
|
+
}
|
|
195
|
+
return json(res, 400, { error: 'action must be configure, mode, check, attach or forget' });
|
|
196
|
+
}
|
|
197
|
+
default: return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// lifecycle.mjs — what happens to the identity over time: draining and
|
|
2
|
+
// pruning the inbox, rebuilding posts, republishing, parking and reviving,
|
|
3
|
+
// rotating the key, moving, aliases, importing, retiring.
|
|
4
|
+
|
|
5
|
+
import { resolveHandle } from '../../../core/social.mjs';
|
|
6
|
+
import { webfingerHost } from '../../../core/wire.mjs';
|
|
7
|
+
import { normalizeImport, IMPORT_KINDS } from '../../../connections/import.mjs';
|
|
8
|
+
|
|
9
|
+
export async function post(p, body, ctx, req, res) { // eslint-disable-line no-unused-vars
|
|
10
|
+
const { agent, log, allowed, embedded, port, publicOrigin, versionOnDisk, isGroup } = ctx; // eslint-disable-line no-unused-vars
|
|
11
|
+
const json = (res, status, obj) => { ctx.json(res, status, obj); return true; };
|
|
12
|
+
switch (p) {
|
|
13
|
+
// Both of these DELETE from the pod's inbox, which is the one thing the
|
|
14
|
+
// lease exists to keep to a single agent. A viewer must claim it first
|
|
15
|
+
// and give up if it cannot — the facade has refused viewer writes since
|
|
16
|
+
// multi-device landed, and these two were simply missed.
|
|
17
|
+
case '/drain': {
|
|
18
|
+
if (!await agent.requestTakeover?.()) {
|
|
19
|
+
return json(res, 503, { error: 'another agent is active for this pod — it is doing the draining' });
|
|
20
|
+
}
|
|
21
|
+
await agent.intake.drain();
|
|
22
|
+
return json(res, 200, agent.status());
|
|
23
|
+
}
|
|
24
|
+
// The owner's answer to "your inbox is very full". Never automatic:
|
|
25
|
+
// discarding someone's mail is their call, not the agent's.
|
|
26
|
+
case '/inbox/prune': {
|
|
27
|
+
if (!body.before) return json(res, 400, { error: 'before (a date) required' });
|
|
28
|
+
if (!await agent.requestTakeover?.()) {
|
|
29
|
+
return json(res, 503, { error: 'another agent is active for this pod — discard from that one' });
|
|
30
|
+
}
|
|
31
|
+
return json(res, 200, await agent.intake.prune({
|
|
32
|
+
before: body.before,
|
|
33
|
+
...(body.keepConcerning ? { keepConcerning: true } : {}),
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
// Put back the posts a lost or restored machine no longer knows about,
|
|
37
|
+
// from what the pod still serves. It writes the statuses store, which is
|
|
38
|
+
// the lease's business, so it refuses the same way the drain does.
|
|
39
|
+
case '/rebuild': {
|
|
40
|
+
if (!await agent.requestTakeover?.()) {
|
|
41
|
+
return json(res, 503, { error: 'another agent is active for this pod — rebuild from that one' });
|
|
42
|
+
}
|
|
43
|
+
return json(res, 200, await agent.publisher.rebuildStatuses({ fromNotes: !!body.fromNotes }));
|
|
44
|
+
}
|
|
45
|
+
case '/publish-profile': {
|
|
46
|
+
// The explicit "republish now" control. Asking for it IS the reason.
|
|
47
|
+
const r = await agent.publisher.publishProfile({ force: true });
|
|
48
|
+
return json(res, 200, { ok: true, ...(r?.unreachable?.length ? { unreachable: r.unreachable } : {}) });
|
|
49
|
+
}
|
|
50
|
+
// ---- lifecycle: what the CLI calls park | revive | rotate-key | retire ----
|
|
51
|
+
// Each claims the lease first, for the same reason /config does: someone
|
|
52
|
+
// acting on this page outranks an idle active agent on another device.
|
|
53
|
+
// Asking first is the page's job — see the warnings in its markup.
|
|
54
|
+
case '/park': {
|
|
55
|
+
await agent.requestTakeover?.();
|
|
56
|
+
return json(res, 200, { ok: true, ...await agent.park() });
|
|
57
|
+
}
|
|
58
|
+
case '/revive': {
|
|
59
|
+
await agent.requestTakeover?.();
|
|
60
|
+
return json(res, 200, { ok: true, ...await agent.revive() });
|
|
61
|
+
}
|
|
62
|
+
case '/rotate-key': {
|
|
63
|
+
await agent.requestTakeover?.();
|
|
64
|
+
const r = await agent.rotateKey();
|
|
65
|
+
return json(res, 200, { ok: true, changed: !!r?.changed });
|
|
66
|
+
}
|
|
67
|
+
// Hand the identity on. Federated and effectively one-way: the Move
|
|
68
|
+
// tells every follower's server to migrate them, and the actor is left
|
|
69
|
+
// advertising movedTo so the old handle redirects. Same typed-handle
|
|
70
|
+
// interlock as retire — a stray click cannot produce it.
|
|
71
|
+
case '/move': {
|
|
72
|
+
if (!body.target) return json(res, 400, { error: 'target required' });
|
|
73
|
+
if (!body.confirm || body.confirm !== agent.store.getConfig()?.handle) {
|
|
74
|
+
return json(res, 400, { error: 'type the handle to confirm' });
|
|
75
|
+
}
|
|
76
|
+
await agent.requestTakeover?.();
|
|
77
|
+
// A handle is not an actor URI, and the Move's target has to be one or
|
|
78
|
+
// the far side has nothing to migrate anyone to. The CLI resolves it
|
|
79
|
+
// the same way; doing it here rather than in the page keeps the one
|
|
80
|
+
// WebFinger lookup on the side that already knows how.
|
|
81
|
+
let target = String(body.target).trim();
|
|
82
|
+
if (!/^https?:\/\//.test(target)) {
|
|
83
|
+
const doc = await resolveHandle(agent, target);
|
|
84
|
+
if (!doc?.id) return json(res, 400, { error: `could not resolve ${target}` });
|
|
85
|
+
target = doc.id;
|
|
86
|
+
}
|
|
87
|
+
return json(res, 200, { ok: true, ...await agent.moveTo(target) });
|
|
88
|
+
}
|
|
89
|
+
// The inbound half of a migration: list an old account elsewhere in
|
|
90
|
+
// this actor's alsoKnownAs, which is what the old server checks for
|
|
91
|
+
// before it will send a Move here. Whatever is entered is resolved to
|
|
92
|
+
// the account's canonical id — the check on the far side is an exact
|
|
93
|
+
// string match, so a pasted profile URL stored as typed would fail it.
|
|
94
|
+
case '/alias': {
|
|
95
|
+
const urls = agent.publisher?.urls;
|
|
96
|
+
if (!urls) return json(res, 409, { error: 'agent not connected yet' });
|
|
97
|
+
if (!body.add && !body.remove) return json(res, 400, { error: 'add or remove required' });
|
|
98
|
+
await agent.requestTakeover?.();
|
|
99
|
+
const cfg = { ...agent.store.getConfig() };
|
|
100
|
+
const aliases = [...(cfg.aliases || [])];
|
|
101
|
+
if (body.add) {
|
|
102
|
+
// A Move target nobody can resolve is a landing pad nobody lands on.
|
|
103
|
+
if (!webfingerHost(urls.base) && !cfg.gateway?.frontActor) {
|
|
104
|
+
return json(res, 400, {
|
|
105
|
+
error: 'this pod is a path on a shared host, so WebFinger cannot answer for it '
|
|
106
|
+
+ '— other servers could never resolve this account as a Move target',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const input = String(body.add).trim().replace(/^@/, '');
|
|
110
|
+
let doc = /^https?:\/\//.test(input)
|
|
111
|
+
? await agent.intake.fetchAP(input).catch(() => null)
|
|
112
|
+
: await resolveHandle(agent, input).catch(() => null);
|
|
113
|
+
// A URL may answer with any id it likes; before that id is stored
|
|
114
|
+
// it must vouch for itself — the intake's verify-by-deref.
|
|
115
|
+
if (doc?.id && /^https?:\/\//.test(input) && doc.id !== input) {
|
|
116
|
+
const own = await agent.intake.fetchAP(doc.id).catch(() => null);
|
|
117
|
+
doc = own?.id === doc.id ? own : null;
|
|
118
|
+
}
|
|
119
|
+
if (!doc?.id) {
|
|
120
|
+
return json(res, 400, { error: `could not fetch the old account (${input}) — it must exist and answer` });
|
|
121
|
+
}
|
|
122
|
+
if (doc.id === urls.actor) return json(res, 400, { error: 'that is this account' });
|
|
123
|
+
if (!aliases.includes(doc.id)) aliases.push(doc.id);
|
|
124
|
+
} else {
|
|
125
|
+
const target = String(body.remove).trim();
|
|
126
|
+
if (!aliases.includes(target)) return json(res, 404, { error: 'no such alias' });
|
|
127
|
+
// Follower servers process a Move on their own retry schedules,
|
|
128
|
+
// over days — an alias removed early strands the late ones.
|
|
129
|
+
if (!body.confirm) {
|
|
130
|
+
return json(res, 409, {
|
|
131
|
+
error: 'servers still retrying the Move check this alias and would strand '
|
|
132
|
+
+ 'their followers — send confirm: true to remove it anyway',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
aliases.splice(aliases.indexOf(target), 1);
|
|
136
|
+
}
|
|
137
|
+
cfg.aliases = aliases;
|
|
138
|
+
agent.store.setConfig(cfg);
|
|
139
|
+
agent.publisher.config.aliases = aliases;
|
|
140
|
+
await agent.store.flush();
|
|
141
|
+
const pub = await agent.publisher.publishProfile();
|
|
142
|
+
return json(res, 200, {
|
|
143
|
+
ok: true, aliases,
|
|
144
|
+
...(pub?.unreachable?.length ? { unreachable: pub.unreachable } : {}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Stage a CSV export from the old account. Rows are applied by the
|
|
148
|
+
// agent's paced worker, not in this request — big lists take a while,
|
|
149
|
+
// and GET /import is the window onto how far it has gotten.
|
|
150
|
+
case '/import': {
|
|
151
|
+
if (!agent.importer) return json(res, 409, { error: 'agent not connected yet' });
|
|
152
|
+
// Unlike the one-shot routes, staging arms a worker that keeps
|
|
153
|
+
// writing for minutes — a device that could not take the lease must
|
|
154
|
+
// not run one alongside the device that holds it.
|
|
155
|
+
const took = await agent.requestTakeover?.();
|
|
156
|
+
if (took === false) {
|
|
157
|
+
return json(res, 503, { error: 'another device is active for this pod — import from there' });
|
|
158
|
+
}
|
|
159
|
+
if (body.clear === true) {
|
|
160
|
+
agent.importer.clear();
|
|
161
|
+
return json(res, 200, { ok: true, cleared: true });
|
|
162
|
+
}
|
|
163
|
+
if (!IMPORT_KINDS.includes(body.kind)) {
|
|
164
|
+
return json(res, 400, { error: `kind must be one of: ${IMPORT_KINDS.join(', ')}` });
|
|
165
|
+
}
|
|
166
|
+
if (typeof body.text !== 'string' || !body.text.trim()) {
|
|
167
|
+
return json(res, 400, { error: 'text required — the CSV file contents' });
|
|
168
|
+
}
|
|
169
|
+
const { values, invalid } = normalizeImport(body.kind, body.text);
|
|
170
|
+
const r = agent.importer.stage(body.kind, values);
|
|
171
|
+
await agent.store.flush();
|
|
172
|
+
return json(res, 200, {
|
|
173
|
+
ok: true, kind: body.kind, ...r,
|
|
174
|
+
invalid: invalid.length,
|
|
175
|
+
...(invalid.length ? { invalidSample: invalid.slice(0, 5) } : {}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
case '/retire': {
|
|
179
|
+
// Irreversible and federated: a Delete leaves for every follower and
|
|
180
|
+
// the actor becomes a Tombstone. The typed handle is the interlock —
|
|
181
|
+
// a stray click cannot produce it, and nor can anything that never
|
|
182
|
+
// read the record.
|
|
183
|
+
if (!body.confirm || body.confirm !== agent.store.getConfig()?.handle) {
|
|
184
|
+
return json(res, 400, { error: 'type the handle to confirm' });
|
|
185
|
+
}
|
|
186
|
+
await agent.requestTakeover?.();
|
|
187
|
+
return json(res, 200, { ok: true, ...await agent.publisher.retireActor() });
|
|
188
|
+
}
|
|
189
|
+
default: return false;
|
|
190
|
+
}
|
|
191
|
+
}
|