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,410 @@
|
|
|
1
|
+
// setup.mjs — making an identity and its secrets: setup, rotate-key,
|
|
2
|
+
// revoke-credential, tokens, passwd, keys.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { tildify, rootOf, recordLastUsed, writeJsonAtomic } from '../../home.mjs';
|
|
8
|
+
import { insecureUrlReason } from '../../../shared/safefetch.mjs';
|
|
9
|
+
import { localFetch } from '../../../client/localapi.mjs';
|
|
10
|
+
import { args, flag, has, PROFILE, AP_ROOT, HOME, PORT, recordAgent, useProfile, requireHandle, requireIdentity, askHidden, ask, endAsking, openBrowser, finish, IDENTITY_FLAGS, refuseExistingIdentity, runBrowserSetup } from '../context.mjs';
|
|
11
|
+
|
|
12
|
+
export async function setup() {
|
|
13
|
+
// --profile names the home before anything is asked, so a collision is
|
|
14
|
+
// knowable now; refuseExistingIdentity exits when it finds one.
|
|
15
|
+
if (PROFILE) { useProfile(PROFILE); refuseExistingIdentity(); }
|
|
16
|
+
// At a terminal with no identity flag, setup asks two things and hands the
|
|
17
|
+
// rest to the page.
|
|
18
|
+
if (process.stdin.isTTY && !has('cli') && !IDENTITY_FLAGS.some(f => args.includes('--' + f))) {
|
|
19
|
+
return runBrowserSetup();
|
|
20
|
+
}
|
|
21
|
+
const root = flag('root');
|
|
22
|
+
const kind = has('group') ? 'group' : 'person';
|
|
23
|
+
const approveJoins = has('group') && has('approve-joins');
|
|
24
|
+
const summary = flag('summary');
|
|
25
|
+
const icon = flag('icon');
|
|
26
|
+
let pod = flag('pod');
|
|
27
|
+
const interactive = process.stdin.isTTY;
|
|
28
|
+
|
|
29
|
+
// Everything that shapes your identity is asked for here, with defaults,
|
|
30
|
+
// because these are decisions — the pod name becomes half of your
|
|
31
|
+
// permanent address, and nobody should discover that after the fact.
|
|
32
|
+
// Flags skip the matching question, so scripted setup is unchanged.
|
|
33
|
+
let newAccount = has('new-account');
|
|
34
|
+
if (!newAccount && !pod) {
|
|
35
|
+
if (!interactive) {
|
|
36
|
+
console.error('need --email and --handle, plus either --pod <url> or --new-account');
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
const have = await ask('do you already have a Solid pod? (y/n)', 'n');
|
|
40
|
+
if (/^y/i.test(have)) {
|
|
41
|
+
// Either tuck the fediverse account into the pod they already have,
|
|
42
|
+
// or make a fresh pod on the same Solid account for it.
|
|
43
|
+
const where = await ask('store your Fediverse account in that pod, or in a new pod? (existing/new)', 'existing');
|
|
44
|
+
if (/^n/i.test(where)) newAccount = true;
|
|
45
|
+
else pod = await ask('your pod address (e.g. https://you.solidcommunity.net/)');
|
|
46
|
+
} else {
|
|
47
|
+
newAccount = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!newAccount && !pod) { console.error('no pod given'); process.exit(2); }
|
|
51
|
+
|
|
52
|
+
const issuer = flag('issuer') || await ask('Solid identity provider', 'https://solidcommunity.net');
|
|
53
|
+
// Before the password is asked for, let alone sent. The issuer is where it
|
|
54
|
+
// goes and the pod is where the credential it buys is used, so an http:
|
|
55
|
+
// address off this machine puts both in clear. Loopback is exempt: it never
|
|
56
|
+
// reaches a network interface, and a pod on this machine is an ordinary way
|
|
57
|
+
// to run this.
|
|
58
|
+
for (const [url, what] of [[issuer, 'identity provider address'], [pod, 'pod address']]) {
|
|
59
|
+
const bad = insecureUrlReason(url, what);
|
|
60
|
+
if (bad) { console.error(bad); process.exit(2); }
|
|
61
|
+
}
|
|
62
|
+
const email = flag('email') || await ask(`account email at ${new URL(issuer).host}`);
|
|
63
|
+
const handle = flag('handle') || await ask('handle (the name in your address; permanent)');
|
|
64
|
+
if (!email || !handle) {
|
|
65
|
+
console.error('an email and a handle are required');
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
// Before anything irreversible: the account creation and the mint are both
|
|
69
|
+
// one-way, and a credential is shown once. A home that cannot be decided has
|
|
70
|
+
// to be refused here, not after there is something to lose.
|
|
71
|
+
requireHandle(handle);
|
|
72
|
+
useProfile(PROFILE || handle);
|
|
73
|
+
refuseExistingIdentity();
|
|
74
|
+
const podName = newAccount
|
|
75
|
+
? (flag('pod-name') || await ask('pod name (this becomes the domain of your address)', handle))
|
|
76
|
+
: null;
|
|
77
|
+
const name = flag('name') || await ask('display name (shown above your address)', handle);
|
|
78
|
+
|
|
79
|
+
// A handle resolves through <host>/.well-known/webfinger, so it only works
|
|
80
|
+
// when the pod owns the root of its host. Whether a NEW pod gets its own
|
|
81
|
+
// subdomain is the server's call, so promise nothing here we cannot keep.
|
|
82
|
+
const { webfingerHost } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
|
|
83
|
+
const issuerHost = new URL(issuer).host;
|
|
84
|
+
const wfHost = newAccount ? null : webfingerHost(pod);
|
|
85
|
+
// A person warned about an unresolvable handle is the one who suffers, so a
|
|
86
|
+
// warning is their call to accept. Nobody could ever find this group, and the
|
|
87
|
+
// people it would fail are not the operator reading the warning.
|
|
88
|
+
if (kind === 'group' && !newAccount && !wfHost) {
|
|
89
|
+
console.error(`${pod} is a path on ${new URL(pod).host}, not the root of its own host.`);
|
|
90
|
+
console.error('WebFinger is answered only at a host root, so nobody could find this group.');
|
|
91
|
+
console.error('Give the group a pod of its own: fedipod setup --group --new-account');
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
console.log(kind === 'group' ? '\nThe group will be:\n' : '\nYou will be:\n');
|
|
95
|
+
console.log(` ${name}`);
|
|
96
|
+
if (wfHost) {
|
|
97
|
+
console.log(` @${handle}@${wfHost}\n`);
|
|
98
|
+
} else if (newAccount) {
|
|
99
|
+
console.log(` @${handle}@${podName}.${issuerHost}\n`);
|
|
100
|
+
console.log(`— provided ${issuerHost} gives each pod its own subdomain. Some servers put`);
|
|
101
|
+
console.log(`pods at ${issuerHost}/${podName}/ instead, and a pod sharing a host cannot`);
|
|
102
|
+
console.log('answer WebFinger for an address. Setup checks which you got and says so');
|
|
103
|
+
console.log('before publishing anything.\n');
|
|
104
|
+
} else {
|
|
105
|
+
console.log(` @${handle}@${new URL(pod).host} — WILL NOT RESOLVE\n`);
|
|
106
|
+
console.log(`This pod is ${pod} — a path on ${new URL(pod).host}, not the root of its own`);
|
|
107
|
+
console.log('host. WebFinger is answered only at a host root, which this pod cannot');
|
|
108
|
+
console.log('write to, so other servers will not find you. Posting and reading still');
|
|
109
|
+
console.log('work; being discovered does not.\n');
|
|
110
|
+
}
|
|
111
|
+
console.log('The display name can be changed later; the handle and pod cannot.');
|
|
112
|
+
const go = await ask(newAccount
|
|
113
|
+
? (kind === 'group' ? 'create pod and group? (y/n)' : 'create pod and Fediverse account? (y/n)')
|
|
114
|
+
: (kind === 'group' ? 'create group on this pod? (y/n)' : 'create Fediverse account on this pod? (y/n)'), 'y');
|
|
115
|
+
if (!/^y/i.test(go)) { console.log('nothing was created'); process.exit(0); }
|
|
116
|
+
endAsking(); // hand the tty to the password prompt
|
|
117
|
+
|
|
118
|
+
const password = process.env.AP_PASSWORD || await askHidden(`password for ${email} at ${issuer}: `);
|
|
119
|
+
|
|
120
|
+
if (newAccount) {
|
|
121
|
+
const { createAccountWithPod } = await import(new URL('../../../../lib/device/account.mjs', import.meta.url));
|
|
122
|
+
const made = await createAccountWithPod({ issuer, email, password, podName });
|
|
123
|
+
pod = made.pod;
|
|
124
|
+
console.log(`account + pod created: ${pod}`);
|
|
125
|
+
if (!webfingerHost(pod)) {
|
|
126
|
+
console.log(`\n${issuerHost} created the pod at a path rather than on its own subdomain,`);
|
|
127
|
+
console.log(`so @${handle}@\u2026 cannot be discovered by other Fediverse servers.`);
|
|
128
|
+
const cont = kind === 'group' ? 'n' : (interactive ? await ask('continue anyway? (y/n)', 'n') : 'y');
|
|
129
|
+
endAsking();
|
|
130
|
+
if (!/^y/i.test(cont)) {
|
|
131
|
+
console.log('stopping \u2014 the pod exists, but no actor was published');
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { mintCredential } = await import(new URL('../../../../lib/device/remote.mjs', import.meta.url));
|
|
138
|
+
// New credentials only: one already registered keeps the name it was minted
|
|
139
|
+
// under, so existing pods stay as they are unless they are set up again.
|
|
140
|
+
const credential = await mintCredential({ origin: issuer, email, password, name: 'fedipod' });
|
|
141
|
+
const rec = {
|
|
142
|
+
...credential,
|
|
143
|
+
remotePod: pod.endsWith('/') ? pod : pod + '/',
|
|
144
|
+
// The private half goes on THIS machine, exactly as the browser setup does
|
|
145
|
+
// (lib/setup.mjs). Omitting it here meant the two paths produced different
|
|
146
|
+
// installs from the same answers: the CLI put the timeline, contacts,
|
|
147
|
+
// blocklist and notifications on the pod — the layout the relay design
|
|
148
|
+
// exists to avoid, and the one that makes receiving a post cost pod writes.
|
|
149
|
+
// It also made provisioning write a tree it did not need, which is where a
|
|
150
|
+
// slow server showed it up.
|
|
151
|
+
privateRoot: flag('private-root')
|
|
152
|
+
|| pathToFileURL(path.join(HOME, 'private')).href + '/',
|
|
153
|
+
...(root ? { root } : {}),
|
|
154
|
+
...(flag('keys') === 'pod' ? { keysMode: 'pod' } : {}),
|
|
155
|
+
...(has('rotate-key') ? { rotateKeyOnce: true } : {}),
|
|
156
|
+
// What shape this install is. Both setup paths stamp it, so `upgrade` can
|
|
157
|
+
// tell an old install from a new one rather than inferring it.
|
|
158
|
+
layout: (await import(new URL('../../../../lib/device/migrate.mjs', import.meta.url))).CURRENT_LAYOUT,
|
|
159
|
+
};
|
|
160
|
+
fs.mkdirSync(HOME, { recursive: true, mode: 0o700 });
|
|
161
|
+
writeJsonAtomic(path.join(HOME, 'credential.json'), rec);
|
|
162
|
+
// The one you just made is the one you are using.
|
|
163
|
+
if (rootOf(HOME) === AP_ROOT) recordLastUsed(AP_ROOT, path.basename(HOME));
|
|
164
|
+
recordAgent({ port: PORT, handle }); // later commands need no --port
|
|
165
|
+
console.log(`credential minted and saved to ${path.join(HOME, 'credential.json')}`);
|
|
166
|
+
|
|
167
|
+
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
168
|
+
const agent = new Agent({ home: HOME, log: (...a) => console.log('[setup]', ...a) });
|
|
169
|
+
await agent.bootstrap({ handle, name, root, kind, approveJoins, summary, icon });
|
|
170
|
+
await agent.connect({ repair: false }); // publishProfile below is the publish
|
|
171
|
+
await agent.publisher.publishProfile();
|
|
172
|
+
await agent.store.flush();
|
|
173
|
+
const finalHost = webfingerHost(rec.remotePod);
|
|
174
|
+
const what = kind === 'group' ? 'group' : 'actor';
|
|
175
|
+
console.log(finalHost
|
|
176
|
+
? `${what} published: @${handle}@${finalHost}`
|
|
177
|
+
: `${what} published, but not reachable as a handle \u2014 ${rec.remotePod} is not a host root`);
|
|
178
|
+
|
|
179
|
+
// Straight into serving — setup ends with a working client in the browser.
|
|
180
|
+
const { startAdmin } = await import(new URL('../../../../lib/device/admin/index.mjs', import.meta.url));
|
|
181
|
+
const { hostLabel } = await import(new URL('../../../../lib/shared/guard.mjs', import.meta.url));
|
|
182
|
+
startAdmin({ port: PORT, handle, gateToken: process.env.AP_GATE_TOKEN || '', agent, log: (...a) => console.log('[ap]', ...a) });
|
|
183
|
+
const shutdown = () => {
|
|
184
|
+
setTimeout(() => process.exit(0), 1500).unref(); // never hang a stop on a slow pod
|
|
185
|
+
try { fs.rmSync(path.join(HOME, 'agent.pid'), { force: true }); } catch {}
|
|
186
|
+
Promise.allSettled([agent.store.flush(), agent.lease?.release()]).finally(() => process.exit(0));
|
|
187
|
+
};
|
|
188
|
+
process.on('SIGINT', shutdown);
|
|
189
|
+
process.on('SIGTERM', shutdown);
|
|
190
|
+
// The named origin, and the instance to log the client into is that same
|
|
191
|
+
// origin. Opening the shared one and then telling you to log in there is how
|
|
192
|
+
// two identities end up sharing a browser storage bucket, and a client on the
|
|
193
|
+
// shared origin shows whichever account it happens to hold.
|
|
194
|
+
const label = hostLabel(handle);
|
|
195
|
+
const authority = `${label ? label + '.' : ''}localhost:${PORT}`;
|
|
196
|
+
const url = `http://${authority}/`;
|
|
197
|
+
if (kind === 'group') {
|
|
198
|
+
console.log(`group running on ${url} — see \`fedipod members\``);
|
|
199
|
+
} else {
|
|
200
|
+
console.log(`agent running — opening ${url} (log in with instance ${authority})`);
|
|
201
|
+
openBrowser(url);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function rotateKey() {
|
|
206
|
+
requireIdentity();
|
|
207
|
+
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
208
|
+
const agent = new Agent({ home: HOME, log: (...a) => console.log('[rotate-key]', ...a) });
|
|
209
|
+
// A home whose keys.json is gone or truncated cannot connect() at all:
|
|
210
|
+
// resolveKeys refuses to mint over a key the actor already publishes, which
|
|
211
|
+
// is the right default — minting silently would invalidate every signature
|
|
212
|
+
// the other device can still make. It did leave the advice that refusal
|
|
213
|
+
// prints ("run fedipod rotate-key") a dead end, though, because this
|
|
214
|
+
// command connects first and meets the same refusal. --force arms the
|
|
215
|
+
// one-shot rotation in the credential so the connect can get past it.
|
|
216
|
+
const forced = has('force');
|
|
217
|
+
const rotateCredPath = path.join(HOME, 'credential.json');
|
|
218
|
+
if (forced) {
|
|
219
|
+
const cred = JSON.parse(fs.readFileSync(rotateCredPath, 'utf8'));
|
|
220
|
+
writeJsonAtomic(rotateCredPath, { ...cred, rotateKeyOnce: true });
|
|
221
|
+
console.log('--force: the replacement key is minted as the agent connects\n');
|
|
222
|
+
}
|
|
223
|
+
// Read-only until you say yes: connecting for real acquires the lease and
|
|
224
|
+
// starts the whole active agent — a destructive inbox drain, a channel
|
|
225
|
+
// subscription, ACL probes and a tag-feed sweep — before the prompt.
|
|
226
|
+
if (!await agent.connect({ act: false })) {
|
|
227
|
+
console.error('nothing to rotate — no configured agent in this AP_HOME');
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
const cfg = agent.store.getConfig();
|
|
231
|
+
console.log(`\nRotating the signing key for @${cfg.handle}@${new URL(cfg.remotePod).host}\n`);
|
|
232
|
+
console.log(' · a new keypair replaces the one in this home');
|
|
233
|
+
console.log(' · the actor document is republished so other servers learn it');
|
|
234
|
+
console.log(' · ANY OTHER DEVICE holding the old key stops being able to sign\n');
|
|
235
|
+
const ans = has('yes') ? 'y' : await ask('rotate now? (y/n)', 'n');
|
|
236
|
+
endAsking();
|
|
237
|
+
if (!/^y/i.test(ans)) {
|
|
238
|
+
// --force arms the one-shot rotation before the prompt, so the preview
|
|
239
|
+
// connect can read past the mint refusal. Declining has to disarm it, or
|
|
240
|
+
// "key unchanged" would be a lie: the next ordinary start would rotate,
|
|
241
|
+
// and nothing on that path republishes the actor.
|
|
242
|
+
if (forced) {
|
|
243
|
+
try {
|
|
244
|
+
const { rotateKeyOnce, ...rest } = JSON.parse(fs.readFileSync(rotateCredPath, 'utf8'));
|
|
245
|
+
writeJsonAtomic(rotateCredPath, rest);
|
|
246
|
+
} catch { /* the credential moved under us; the flag goes with it */ }
|
|
247
|
+
}
|
|
248
|
+
console.log('key unchanged');
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
await agent.connect(); // now it may act
|
|
252
|
+
if (forced) {
|
|
253
|
+
// connect() has already minted it; all that is left is telling the
|
|
254
|
+
// fediverse. Calling rotateKey here would mint a second one for nothing.
|
|
255
|
+
await agent.publisher.publishProfile();
|
|
256
|
+
console.log('rotated and republished');
|
|
257
|
+
} else {
|
|
258
|
+
const r = await agent.rotateKey();
|
|
259
|
+
console.log(r.changed ? 'rotated and republished' : 'no change — the key was already fresh');
|
|
260
|
+
}
|
|
261
|
+
await finish(agent);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function revokeCredential() {
|
|
265
|
+
// The credential file cannot be protected from anything running as you —
|
|
266
|
+
// so the answer to a suspected leak is to kill it server-side, fast.
|
|
267
|
+
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
268
|
+
const { revokeCredentialViaAccount } = await import(new URL('../../../../lib/device/remote.mjs', import.meta.url));
|
|
269
|
+
const agent = new Agent({ home: HOME, log: () => {} });
|
|
270
|
+
const cred = agent.readCredential();
|
|
271
|
+
if (!cred) { console.error('no credential to revoke'); process.exit(2); }
|
|
272
|
+
const email = flag('email');
|
|
273
|
+
if (!email) { console.error('need --email <account email> (the account password is prompted)'); process.exit(2); }
|
|
274
|
+
const password = process.env.AP_PASSWORD || await askHidden(`password for ${email} at ${cred.issuerOrigin}: `);
|
|
275
|
+
const ok = await revokeCredentialViaAccount({
|
|
276
|
+
origin: cred.issuerOrigin, email, password, resource: cred.resource,
|
|
277
|
+
}).catch(e => { console.error(`revoke failed: ${e.message}`); return false; });
|
|
278
|
+
if (ok) {
|
|
279
|
+
fs.rmSync(path.join(HOME, 'credential.json'), { force: true });
|
|
280
|
+
console.log('credential revoked server-side and deleted locally — run setup again to reconnect');
|
|
281
|
+
} else {
|
|
282
|
+
console.error('server did not confirm revocation — revoke it from the account dashboard');
|
|
283
|
+
console.error(`(credential resource: ${cred.resource || 'unknown — dashboard only'})`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function tokens() {
|
|
289
|
+
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
290
|
+
const { RemotePod } = await import(new URL('../../../../lib/device/remote.mjs', import.meta.url));
|
|
291
|
+
const { apUrls } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
|
|
292
|
+
const agent = new Agent({ home: HOME, log: () => {} });
|
|
293
|
+
const cred = agent.readCredential();
|
|
294
|
+
if (!cred) { console.error('no credential — run setup first'); process.exit(2); }
|
|
295
|
+
agent.remote = new RemotePod(cred);
|
|
296
|
+
await agent.remote.warmup();
|
|
297
|
+
agent.urls = apUrls(cred.remotePod, cred.root);
|
|
298
|
+
agent.store.attach(agent.privateStorage(cred, 'state')); // honours privateRoot
|
|
299
|
+
await agent.store.load();
|
|
300
|
+
const recs = agent.store.read('masto-tokens.json', [])
|
|
301
|
+
.map(r => (typeof r === 'string' ? { token: r, createdAt: null } : r));
|
|
302
|
+
if (has('revoke-all')) {
|
|
303
|
+
agent.store.write('masto-tokens.json', []);
|
|
304
|
+
await agent.store.flush();
|
|
305
|
+
console.log(`revoked ${recs.length} token(s) — every logged-in client must log in again`);
|
|
306
|
+
} else if (flag('revoke')) {
|
|
307
|
+
const prefix = flag('revoke');
|
|
308
|
+
const kept = recs.filter(r => !r.token.startsWith(prefix));
|
|
309
|
+
agent.store.write('masto-tokens.json', kept);
|
|
310
|
+
await agent.store.flush();
|
|
311
|
+
console.log(`revoked ${recs.length - kept.length} token(s) matching "${prefix}"`);
|
|
312
|
+
} else {
|
|
313
|
+
if (!recs.length) console.log('no client tokens issued');
|
|
314
|
+
for (const r of recs) {
|
|
315
|
+
const age = r.createdAt ? `${Math.round((Date.now() - r.createdAt) / 86400000)}d old` : 'undated';
|
|
316
|
+
console.log(`${r.token.slice(0, 8)}… ${age}`);
|
|
317
|
+
}
|
|
318
|
+
console.log('\nrevoke with: fedipod tokens --revoke <prefix> (or --revoke-all)');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function passwd() {
|
|
323
|
+
requireIdentity();
|
|
324
|
+
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
325
|
+
const { hashPassword } = await import(new URL('../../../../lib/client/masto/index.mjs', import.meta.url));
|
|
326
|
+
const { RemotePod } = await import(new URL('../../../../lib/device/remote.mjs', import.meta.url));
|
|
327
|
+
const { apUrls } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
|
|
328
|
+
const agent = new Agent({ home: HOME, log: () => {} });
|
|
329
|
+
const cred = agent.readCredential();
|
|
330
|
+
if (!cred) { console.error('no credential — run setup first'); process.exit(2); }
|
|
331
|
+
const pw = await askHidden('new UI password: ');
|
|
332
|
+
if (!pw) { console.error('empty password — aborted'); process.exit(2); }
|
|
333
|
+
agent.remote = new RemotePod(cred);
|
|
334
|
+
await agent.remote.warmup();
|
|
335
|
+
agent.urls = apUrls(cred.remotePod, cred.root);
|
|
336
|
+
agent.store.attach(agent.privateStorage(cred, 'state')); // honours privateRoot
|
|
337
|
+
await agent.store.load();
|
|
338
|
+
const config = agent.store.getConfig();
|
|
339
|
+
if (!config) { console.error('pod state empty — run setup first'); process.exit(2); }
|
|
340
|
+
agent.store.setConfig({ ...config, uiPassword: hashPassword(pw) });
|
|
341
|
+
await agent.store.flush();
|
|
342
|
+
console.log('UI password set — /oauth/authorize now shows a login form (restart a running agent to pick it up)');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function keys() {
|
|
346
|
+
// Where the signing key lives; `--to pod` moves it into pod state — with
|
|
347
|
+
// the state store on a pod every device reaches, that is what lets them
|
|
348
|
+
// all sign as one actor. `--to local` flips back: the next start adopts
|
|
349
|
+
// the pod copy onto this machine and removes it from the pod.
|
|
350
|
+
requireIdentity();
|
|
351
|
+
const localKeyFile = path.join(HOME, 'keys.json');
|
|
352
|
+
const to = flag('to');
|
|
353
|
+
if (!to) {
|
|
354
|
+
const cred0 = JSON.parse(fs.readFileSync(path.join(HOME, 'credential.json'), 'utf8'));
|
|
355
|
+
console.log(fs.existsSync(localKeyFile)
|
|
356
|
+
? `signing key: ${tildify(localKeyFile)} (this machine only)`
|
|
357
|
+
: cred0.keysMode === 'pod'
|
|
358
|
+
? 'signing key: pod state (shared by every device that reaches the state store)'
|
|
359
|
+
: 'signing key: none found here — it mints on the next start');
|
|
360
|
+
process.exit(0);
|
|
361
|
+
}
|
|
362
|
+
if (!['pod', 'local'].includes(to)) {
|
|
363
|
+
console.error('usage: fedipod keys [--to pod|local]');
|
|
364
|
+
process.exit(2);
|
|
365
|
+
}
|
|
366
|
+
// The running agent holds the key and the config in memory — stop it first.
|
|
367
|
+
const answering = await localFetch(HOME, PORT, `/status`).then(r => r.ok).catch(() => false);
|
|
368
|
+
if (answering) {
|
|
369
|
+
console.error(`an agent is answering on :${PORT} — stop it first (fedipod stop), then re-run`);
|
|
370
|
+
process.exit(2);
|
|
371
|
+
}
|
|
372
|
+
const credPath = path.join(HOME, 'credential.json');
|
|
373
|
+
const cred = JSON.parse(fs.readFileSync(credPath, 'utf8'));
|
|
374
|
+
if (to === 'local') {
|
|
375
|
+
if (cred.keysMode !== 'pod') { console.log('keys are already local'); process.exit(0); }
|
|
376
|
+
delete cred.keysMode;
|
|
377
|
+
writeJsonAtomic(credPath, cred);
|
|
378
|
+
console.log('keys set to local — the next start adopts the pod copy onto this machine and removes it from the pod');
|
|
379
|
+
process.exit(0);
|
|
380
|
+
}
|
|
381
|
+
if (cred.keysMode === 'pod' && !fs.existsSync(localKeyFile)) {
|
|
382
|
+
console.log('keys are already pod-held');
|
|
383
|
+
process.exit(0);
|
|
384
|
+
}
|
|
385
|
+
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
386
|
+
const { RemotePod } = await import(new URL('../../../../lib/device/remote.mjs', import.meta.url));
|
|
387
|
+
const { apUrls } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
|
|
388
|
+
const { moveKeysToPod } = await import(new URL('../../../../lib/core/keys.mjs', import.meta.url));
|
|
389
|
+
const agent = new Agent({ home: HOME, log: () => {} });
|
|
390
|
+
agent.remote = new RemotePod(cred);
|
|
391
|
+
await agent.remote.warmup();
|
|
392
|
+
agent.store.attach(agent.privateStorage(cred, 'state'));
|
|
393
|
+
await agent.store.load();
|
|
394
|
+
const gwActor = agent.store.getConfig()?.gateway?.frontActor;
|
|
395
|
+
const urls = apUrls(cred.remotePod, cred.root,
|
|
396
|
+
gwActor ? { publicBase: gwActor.replace(/ap\/actor\/?$/, '') } : undefined);
|
|
397
|
+
try {
|
|
398
|
+
await moveKeysToPod(agent.store, { localDir: HOME, actorId: urls.actor, log: console.log });
|
|
399
|
+
} catch (e) {
|
|
400
|
+
console.error(e.message);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
cred.keysMode = 'pod';
|
|
404
|
+
writeJsonAtomic(credPath, cred);
|
|
405
|
+
if (cred.privateRoot) {
|
|
406
|
+
console.log(`note: this identity's state store is ${cred.privateRoot} — devices share the key`);
|
|
407
|
+
console.log('only when the state store is a pod they can all reach (`fedipod state --to pod`).');
|
|
408
|
+
}
|
|
409
|
+
console.log('done — restart the agent');
|
|
410
|
+
}
|