fedipod 1.0.0 → 1.3.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 (151) 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} +10 -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 +170 -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/graphview.mjs +269 -0
  42. package/lib/core/intake/activities.mjs +437 -0
  43. package/lib/core/intake/activity.mjs +240 -0
  44. package/lib/core/intake/channel.mjs +144 -0
  45. package/lib/core/intake/group.mjs +222 -0
  46. package/lib/core/intake/index.mjs +629 -0
  47. package/lib/core/intake/notes.mjs +288 -0
  48. package/lib/core/intake/verify.mjs +142 -0
  49. package/lib/{keys.mjs → core/keys.mjs} +1 -1
  50. package/lib/core/publisher/collections.mjs +229 -0
  51. package/lib/core/publisher/index.mjs +421 -0
  52. package/lib/core/publisher/notes.mjs +188 -0
  53. package/lib/core/publisher/questions.mjs +233 -0
  54. package/lib/core/publisher/restore.mjs +199 -0
  55. package/lib/core/shapes/activitystreams.ttl +129 -0
  56. package/lib/core/shapes/index.mjs +107 -0
  57. package/lib/core/shapes/shapes-text.mjs +13 -0
  58. package/lib/{social.mjs → core/social.mjs} +2 -2
  59. package/lib/{store.mjs → core/store.mjs} +4 -0
  60. package/lib/{wire.mjs → core/wire.mjs} +2 -2
  61. package/lib/device/admin/index.mjs +13 -0
  62. package/lib/device/admin/origins.mjs +35 -0
  63. package/lib/device/admin/routes/connections.mjs +144 -0
  64. package/lib/device/admin/routes/gateway.mjs +199 -0
  65. package/lib/device/admin/routes/lifecycle.mjs +191 -0
  66. package/lib/device/admin/routes/owner.mjs +322 -0
  67. package/lib/device/admin/routes/setup.mjs +393 -0
  68. package/lib/device/admin/routes/social.mjs +188 -0
  69. package/lib/device/admin/server.mjs +95 -0
  70. package/lib/device/admin/static.mjs +244 -0
  71. package/lib/device/admin/surface.mjs +274 -0
  72. package/lib/device/cli/commands/account.mjs +586 -0
  73. package/lib/device/cli/commands/run.mjs +278 -0
  74. package/lib/device/cli/commands/service.mjs +221 -0
  75. package/lib/device/cli/commands/setup.mjs +410 -0
  76. package/lib/device/cli/commands/state.mjs +559 -0
  77. package/lib/device/cli/context.mjs +288 -0
  78. package/lib/{migrate.mjs → device/migrate.mjs} +1 -1
  79. package/lib/{remote.mjs → device/remote.mjs} +3 -3
  80. package/lib/{setup.mjs → device/setup.mjs} +3 -3
  81. package/lib/{update.mjs → device/update.mjs} +1 -1
  82. package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
  83. package/lib/{front-core.mjs → gateway/front-core.mjs} +3 -3
  84. package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +1 -1
  85. package/lib/{httpsig.mjs → gateway/httpsig.mjs} +1 -1
  86. package/lib/server/embed.mjs +405 -0
  87. package/lib/{links.mjs → shared/links.mjs} +1 -1
  88. package/lib/{ua.mjs → shared/ua.mjs} +1 -1
  89. package/package.json +11 -1
  90. package/run-agent.mjs +33 -25
  91. package/scripts/build-app.mjs +4 -0
  92. package/scripts/check-pod-calls.mjs +8 -2
  93. package/scripts/refresh-contexts.mjs +39 -0
  94. package/web/admin/actors.js +145 -0
  95. package/web/admin/common.js +23 -0
  96. package/web/admin/connections.js +112 -0
  97. package/web/admin/gateway.js +111 -0
  98. package/web/admin/group.js +258 -0
  99. package/web/admin/index.html +7 -1
  100. package/web/admin/record.js +378 -0
  101. package/web/admin/setup/index.html +1 -0
  102. package/web/admin/setup/setup.js +2 -13
  103. package/web/admin/upkeep.js +170 -0
  104. package/web/app/README.md +6 -6
  105. package/web/app/admin-facade.mjs +3 -3
  106. package/web/app/agent.mjs +14 -16
  107. package/web/app/atproto-browser.mjs +1 -1
  108. package/web/app/boot.mjs +2 -3
  109. package/web/app/deliver-relay.mjs +1 -1
  110. package/web/app/dist/boot.js +22 -3
  111. package/web/app/dist/boot.js.map +2 -2
  112. package/web/app/dist/sw.js +21913 -5446
  113. package/web/app/dist/sw.js.map +4 -4
  114. package/web/app/fediacct-browser.mjs +1 -1
  115. package/web/app/keys-browser.mjs +27 -4
  116. package/web/app/shims/shapes-text.mjs +8 -0
  117. package/web/app/signup.mjs +2 -3
  118. package/web/app/site/admin/actors.js +145 -0
  119. package/web/app/site/admin/common.js +23 -0
  120. package/web/app/site/admin/connections.js +112 -0
  121. package/web/app/site/admin/gateway.js +111 -0
  122. package/web/app/site/admin/group.js +258 -0
  123. package/web/app/site/admin/index.html +7 -1
  124. package/web/app/site/admin/record.js +378 -0
  125. package/web/app/site/admin/setup/index.html +1 -0
  126. package/web/app/site/admin/setup/setup.js +2 -13
  127. package/web/app/site/admin/upkeep.js +170 -0
  128. package/web/app/site/boot.js +22 -3
  129. package/web/app/site/sw.js +21913 -5446
  130. package/web/app/sw-src.mjs +17 -2
  131. package/lib/admin.mjs +0 -1913
  132. package/lib/embed.mjs +0 -220
  133. package/lib/intake.mjs +0 -1981
  134. package/lib/mastoapi.mjs +0 -2284
  135. package/lib/publisher.mjs +0 -1192
  136. package/web/admin/admin.js +0 -1181
  137. package/web/app/site/admin/admin.js +0 -1181
  138. /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
  139. /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
  140. /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
  141. /package/lib/{lease.mjs → core/lease.mjs} +0 -0
  142. /package/lib/{polls.mjs → core/polls.mjs} +0 -0
  143. /package/lib/{proof.mjs → core/proof.mjs} +0 -0
  144. /package/lib/{storage.mjs → core/storage.mjs} +0 -0
  145. /package/lib/{account.mjs → device/account.mjs} +0 -0
  146. /package/lib/{certs.mjs → device/certs.mjs} +0 -0
  147. /package/lib/{export-collections.mjs → device/export-collections.mjs} +0 -0
  148. /package/lib/{home.mjs → device/home.mjs} +0 -0
  149. /package/lib/{ports.mjs → device/ports.mjs} +0 -0
  150. /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
  151. /package/lib/{safefetch.mjs → shared/safefetch.mjs} +0 -0
@@ -0,0 +1,288 @@
1
+ // context.mjs — what every command starts from: the arguments, which
2
+ // identity (HOME) and port (PORT) a command means, and the helpers that ask,
3
+ // probe and finish. HOME, PORT and DEFAULT_ISSUE are live bindings: useProfile()
4
+ // reassigns them and every command module sees the new value.
5
+
6
+ import fs from 'node:fs';
7
+ import { localFetch } from '../../../lib/client/localapi.mjs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import net from 'node:net';
11
+ import readline from 'node:readline';
12
+ import { spawn } from 'node:child_process';
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
+ import { apRoot, profilesDir, identityHomes, isLegacyRoot, CURRENT_ROOT, tildify, rootOf,
15
+ readRoot, writeRoot, defaultProfile, profileHome, rootHoldsIdentity, ROOT_FILE,
16
+ recordLastUsed, writeJsonAtomic } from '../../../lib/device/home.mjs';
17
+ import { insecureUrlReason } from '../../../lib/shared/safefetch.mjs';
18
+ import { portFree, freePortFrom } from '../../../lib/device/ports.mjs';
19
+
20
+ export const args = process.argv.slice(2);
21
+ export const cmd = args[0];
22
+ export const flag = (name, dflt) => {
23
+ const i = args.indexOf('--' + name);
24
+ return i >= 0 ? args[i + 1] : dflt;
25
+ };
26
+ export const has = (name) => args.includes('--' + name);
27
+ // The port can also be given bare — `npm start 8081` reaches us as `up 8081`,
28
+ // because npm passes positionals through but eats `--port`. Bare form only for
29
+ // the start-style commands, where a lone number cannot mean anything else.
30
+ export const barePort = () => {
31
+ if (!['up', 'start', 'run'].includes(cmd)) return null;
32
+ for (let i = 1; i < args.length; i++) {
33
+ if (/^\d+$/.test(args[i]) && !args[i - 1].startsWith('--')) return args[i];
34
+ }
35
+ return null;
36
+ };
37
+ export const portFlag = () => flag('port', null) || barePort();
38
+ // One identity per home, and EVERY identity is `<root>/profiles/<name>/`. There
39
+ // is no privileged unnamed one: `root.json` names which you get when you do not
40
+ // say, and that is a pointer you can change rather than a directory you have to
41
+ // move a private key out of. lib/home.mjs decides the root.
42
+ //
43
+ // An explicit --home / AP_HOME still wins and is taken literally: you named a
44
+ // directory, so that directory is the identity, root.json unread. That is what
45
+ // `rootOf` has always documented for a custom home.
46
+ export const PROFILE = flag('profile', process.env.AP_PROFILE || null);
47
+ export const AP_ROOT = apRoot();
48
+ export const PROFILES_DIR = profilesDir(AP_ROOT);
49
+
50
+ // `let`, because setup does not know which identity it is until it has asked for
51
+ // the handle — the home is named after it. Everything below reads HOME at call
52
+ // time, so reassigning it once, early, is enough; PORT is the exception and is
53
+ // recomputed with it.
54
+ export let DEFAULT_ISSUE = null; // set when the pointer is unusable
55
+ export let HOME = flag('home', process.env.AP_HOME || (() => {
56
+ if (PROFILE) return profileHome(AP_ROOT, PROFILE);
57
+ const d = defaultProfile(AP_ROOT);
58
+ if (typeof d === 'string') return profileHome(AP_ROOT, d);
59
+ DEFAULT_ISSUE = d?.missing
60
+ ? `${ROOT_FILE} names "${d.missing}", which is not an identity here`
61
+ : 'there is more than one identity here and none is the default';
62
+ return profileHome(AP_ROOT, d?.missing || '');
63
+ })());
64
+
65
+ // The port chosen at setup is remembered, so `start`/`stop`/`status` need no
66
+ // flags afterwards. Precedence: --port (or a bare port number, `npm start
67
+ // 8081`) > AP_PORT > the recorded choice > 8030.
68
+ // The handle is remembered alongside it, for the named origin: the agent also
69
+ // answers at <handle>.localhost:<port>, and that has to work from the first
70
+ // request, before pod state has been read.
71
+ export function recordedAgent() {
72
+ try { return JSON.parse(fs.readFileSync(path.join(HOME, 'agent.json'), 'utf8')) || {}; }
73
+ catch { return {}; }
74
+ }
75
+ export function recordedPort() { return Number(recordedAgent().port) || null; }
76
+ export function recordAgent(fields) {
77
+ try {
78
+ fs.mkdirSync(HOME, { recursive: true, mode: 0o700 });
79
+ const rec = { ...recordedAgent(), ...fields };
80
+ writeJsonAtomic(path.join(HOME, 'agent.json'), rec, { mode: 0o644 });
81
+ } catch { /* the flag still works, it just isn't remembered */ }
82
+ }
83
+ export let PORT = Number(portFlag() || process.env.AP_PORT || recordedPort() || 8030);
84
+
85
+ // Setup is the one command that cannot know its home in advance: the identity is
86
+ // named after the handle, and the handle is the first thing it asks. Everything
87
+ // that reads HOME does so at call time, so pointing it at the right directory as
88
+ // soon as the name exists is enough — PORT is recomputed because it was read
89
+ // from the old home's agent.json.
90
+ export function useProfile(name) {
91
+ HOME = flag('home', process.env.AP_HOME || profileHome(AP_ROOT, name));
92
+ PORT = Number(portFlag() || process.env.AP_PORT || recordedPort() || 8030);
93
+ DEFAULT_ISSUE = null;
94
+ return HOME;
95
+ }
96
+
97
+ // A handle becomes a directory name, so it is checked before it is one. Same
98
+ // rule the admin API applies before creating an actor (admin.mjs) — without it
99
+ // a handle containing a slash or `..` climbs out of profiles/.
100
+ const HANDLE_RE = /^[a-z0-9][a-z0-9_-]{0,30}$/;
101
+ export function requireHandle(handle) {
102
+ if (HANDLE_RE.test(handle)) return handle;
103
+ console.error(`"${handle}" cannot be a handle: letters, digits, hyphens and underscores,`);
104
+ console.error('starting with a letter or digit, at most 31 characters.');
105
+ process.exit(2);
106
+ }
107
+
108
+ // Commands that act on an identity need one decided. Says which of the three
109
+ // ways it failed, because "no identity" and "which identity" are different
110
+ // problems with different fixes.
111
+ export function requireIdentity() {
112
+ // An explicit AP_HOME / --home is an explicit identity directory. Nothing
113
+ // about the machine's root applies to it — including whether that root has
114
+ // been restructured, which is somebody else's install's problem.
115
+ if (process.env.AP_HOME || flag('home')) return;
116
+ if (rootHoldsIdentity(AP_ROOT)) {
117
+ console.error(`${tildify(AP_ROOT)} still keeps an identity at its top level.`);
118
+ console.error('Every identity lives in profiles/<name>/ now. Move this one down with:\n');
119
+ console.error(` ${process.argv[1]} home --restructure\n`);
120
+ process.exit(2);
121
+ }
122
+ if (!DEFAULT_ISSUE) return;
123
+ const homes = identityHomes(AP_ROOT).filter(h => fs.existsSync(path.join(h.dir, 'credential.json')));
124
+ console.error(DEFAULT_ISSUE + '.');
125
+ if (homes.length) {
126
+ console.error(`\n ${process.argv[1]} --profile <name> start\n`);
127
+ console.error('Whichever you start is remembered, so plain commands mean that one afterwards.');
128
+ console.error(`here: ${homes.map(h => h.name).join(', ')}`);
129
+ } else {
130
+ console.error(`\nThere are no identities yet — ${process.argv[1]} setup`);
131
+ }
132
+ process.exit(2);
133
+ }
134
+
135
+ export function askHidden(prompt) {
136
+ return new Promise((resolve) => {
137
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
138
+ const onData = (c) => { if (String(c) !== '\n' && String(c) !== '\r') readline.moveCursor(process.stdout, -1, 0), process.stdout.write('*'); };
139
+ process.stdin.on('data', onData);
140
+ rl.question(prompt, (answer) => { process.stdin.off('data', onData); rl.close(); process.stdout.write('\n'); resolve(answer); });
141
+ });
142
+ }
143
+
144
+ // Plain prompt with a default: Enter accepts it. Non-interactive runs
145
+ // (scripts, CI) take the default silently, so flags remain sufficient.
146
+ let sharedRl = null; // one interface: a new one per
147
+ export function ask(prompt, dflt = '') { // question would drop buffered input
148
+ if (!process.stdin.isTTY) return Promise.resolve(dflt);
149
+ sharedRl ||= readline.createInterface({ input: process.stdin, output: process.stdout });
150
+ return new Promise((resolve) => {
151
+ sharedRl.question(dflt ? `${prompt} [${dflt}]: ` : `${prompt}: `, (answer) => {
152
+ resolve(String(answer).trim() || dflt);
153
+ });
154
+ });
155
+ }
156
+ export function endAsking() { sharedRl?.close(); sharedRl = null; }
157
+
158
+ // "Occupied" means "cannot be bound", not "does not answer HTTP": something
159
+ // holding a port without speaking HTTP reads as free to a GET, and then the
160
+ // agent dies on EADDRINUSE.
161
+
162
+ // The first port from `first` upward that binds. Walking always ends in one,
163
+ // so this is a step rather than a condition.
164
+
165
+ // Is anything at all on this port? For the operations that MOVE data, "it did
166
+ // not answer as one of ours" is not the same as "nothing is there": an agent
167
+ // started with AP_GATE_TOKEN answers 401 to an un-tokened /status, so agentOn
168
+ // reads a perfectly live agent as stopped — and a sweep that trusted it would
169
+ // copy the state out from under one, which the next write then overwrites.
170
+ // Bind to find out, and fail closed.
171
+ export async function somethingOn(port) {
172
+ const mine = await agentOn(port);
173
+ if (mine) return 'running';
174
+ return (await portFree(port)) ? null : 'something is on the port and did not answer as ours';
175
+ }
176
+
177
+ // Whatever is on the port — is it one of ours?
178
+ export async function agentOn(port) {
179
+ try {
180
+ const res = await localFetch(HOME, port, `/status`, { signal: AbortSignal.timeout(2000) });
181
+ const body = await res.json();
182
+ return typeof body?.configured === 'boolean' ? body : null;
183
+ } catch { return null; }
184
+ }
185
+
186
+ export const isInside = (root, p) => {
187
+ const rel = path.relative(root, p);
188
+ return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
189
+ };
190
+
191
+ export function openBrowser(url) {
192
+ try {
193
+ const win = process.platform === 'win32';
194
+ const cmd = win ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
195
+ const child = spawn(cmd, win ? ['/c', 'start', '', url] : [url], { detached: true, stdio: 'ignore' });
196
+ // A box with no opener at all (a server, a bare container) emits this
197
+ // asynchronously, where the try/catch cannot reach it — and an unhandled
198
+ // 'error' event on a child process ends the agent.
199
+ child.on('error', () => {});
200
+ child.unref();
201
+ } catch { /* best-effort — the URL is printed anyway */ }
202
+ }
203
+
204
+ // A one-shot command holds the lease only for its own duration: exiting without
205
+ // releasing it leaves the next `start` as a read-only viewer until the lease
206
+ // expires, which is 5 minutes of doing nothing.
207
+ export async function finish(agent, code = 0) {
208
+ await agent.lease?.release().catch(() => {});
209
+ await agent.store?.flush?.().catch(() => {});
210
+ process.exit(code);
211
+ }
212
+
213
+ // Anything that decides the identity. Given even one of these, setup stays on
214
+ // the command line exactly as it always did — scripts, CI and the tarball's
215
+ // unpack-and-go line depend on that. At a terminal with none of them, setup
216
+ // asks the two things it needs to open a browser and asks the rest there.
217
+ export const IDENTITY_FLAGS = ['new-account', 'pod', 'issuer', 'email', 'name', 'pod-name',
218
+ 'group', 'approve-joins', 'summary', 'icon', 'root', 'keys', 'rotate-key'];
219
+
220
+ // Refuse before anything is asked, let alone typed: setup used to overwrite
221
+ // credential.json in place, and a minted credential is only shown once — so
222
+ // the identity it belonged to could not be recovered afterwards.
223
+ export function refuseExistingIdentity() {
224
+ const credPath = path.join(HOME, 'credential.json');
225
+ if (!fs.existsSync(credPath) || has('force')) return;
226
+ let held = '(unreadable)';
227
+ try { held = JSON.parse(fs.readFileSync(credPath, 'utf8')).remotePod; } catch {}
228
+ console.error(`${HOME} already holds an identity: ${held}`);
229
+ console.error('For another identity: fedipod setup --profile <name>');
230
+ console.error('To list what exists: fedipod profiles');
231
+ console.error('To replace this one: add --force (the old credential is lost)');
232
+ console.error('');
233
+ console.error('If a setup died half-way, do NOT re-run it — the credential it already');
234
+ console.error('minted cannot be minted twice. Run `fedipod start` and');
235
+ console.error('finish at /admin/setup/ in the browser.');
236
+ process.exit(2);
237
+ }
238
+
239
+ // Ask the handle (permanent, and it names the origin) and the port, start
240
+ // serving, and hand over to the page at /admin/setup/. Nothing is created here: the
241
+ // agent's own POST /setup does all of it, so a closed tab cannot lose a
242
+ // credential that only exists in an HTTP response.
243
+ export async function runBrowserSetup() {
244
+ const handle = flag('handle') || await ask('handle (the name in your address; permanent)');
245
+ if (!handle) { console.error('a handle is required'); process.exit(2); }
246
+ // The handle names the home, so nothing can be decided before it — including
247
+ // which identity would be overwritten, and which port was remembered.
248
+ requireHandle(handle);
249
+ useProfile(PROFILE || handle);
250
+ refuseExistingIdentity();
251
+ const port = portFlag() ? PORT : (Number(await ask('port', String(PORT))) || PORT);
252
+ endAsking();
253
+
254
+ // Recorded before the server starts, so `stop`/`status` work while the
255
+ // browser flow is still open — it used to be written only after the mint.
256
+ recordAgent({ port, handle });
257
+
258
+ const { Agent } = await import(new URL('../../../run-agent.mjs', import.meta.url));
259
+ const { startAdmin } = await import(new URL('../../../lib/device/admin/index.mjs', import.meta.url));
260
+ const { hostLabel } = await import(new URL('../../../lib/shared/guard.mjs', import.meta.url));
261
+ const agent = new Agent({ home: HOME, log: (...a) => console.log('[ap]', ...a) });
262
+ startAdmin({
263
+ port, handle, agent,
264
+ gateToken: process.env.AP_GATE_TOKEN || '',
265
+ log: (...a) => console.log('[ap]', ...a),
266
+ });
267
+ const shutdown = () => {
268
+ setTimeout(() => process.exit(0), 1500).unref();
269
+ try { fs.rmSync(path.join(HOME, 'agent.pid'), { force: true }); } catch {}
270
+ Promise.allSettled([agent.store.flush(), agent.lease?.release()]).finally(() => process.exit(0));
271
+ };
272
+ process.on('SIGINT', shutdown);
273
+ process.on('SIGTERM', shutdown);
274
+
275
+ const label = hostLabel(handle);
276
+ const named = label ? `https://${label}.localhost:${port}/` : null;
277
+ const plain = `https://localhost:${port}/`;
278
+ const pad = Math.max(named?.length || 0, plain.length);
279
+ console.log('');
280
+ if (named) {
281
+ console.log(` ${named.padEnd(pad)} <- opening this`);
282
+ console.log(` ${plain.padEnd(pad)} <- the same agent, if your browser cannot find that name`);
283
+ } else {
284
+ console.log(` ${plain}`);
285
+ }
286
+ console.log('\nsetup continues in the browser — Ctrl-C to stop\n');
287
+ openBrowser(named || plain);
288
+ }
@@ -99,7 +99,7 @@ export function classifyRemoteState(urls, base) {
99
99
  // a failure part-way leaves the source authoritative.
100
100
  // Used by both `state --to` and the record page's Move private data.
101
101
  export async function copyPrivateHalf({ from, to, log = () => {} }) {
102
- const { PodStore } = await import('./store.mjs');
102
+ const { PodStore } = await import('../core/store.mjs');
103
103
  const src = new PodStore({ log: () => {} });
104
104
  src.attach(from.state);
105
105
  await src.load();
@@ -12,10 +12,10 @@
12
12
  import { createRequire } from 'node:module';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
- import { PodTransport } from './pod/transport.mjs';
15
+ import { PodTransport } from '../pod/transport.mjs';
16
16
 
17
17
  const require = createRequire(import.meta.url);
18
- const vendorDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../vendor');
18
+ const vendorDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../vendor');
19
19
  const { mintCredential, discoverTokenEndpoint, createGrantSession, revokeCredentialViaAccount } =
20
20
  require(path.join(vendorDir, 'idp-grant.cjs'));
21
21
 
@@ -23,7 +23,7 @@ export { mintCredential, discoverTokenEndpoint, revokeCredentialViaAccount };
23
23
 
24
24
  // Re-exported so the callers that guard their own deletions — run-agent, the
25
25
  // server package's live test, the smoke suite — keep importing it from here.
26
- export { protectedFromDeletion } from './pod/transport.mjs';
26
+ export { protectedFromDeletion } from '../pod/transport.mjs';
27
27
 
28
28
  export class RemotePod extends PodTransport {
29
29
  constructor(credential, { log = () => {}, home = null, session = null, role = 'agent' } = {}) {
@@ -15,10 +15,10 @@ import * as $rdf from 'rdflib';
15
15
 
16
16
  import { createAccountWithPod as realCreateAccount } from './account.mjs';
17
17
  import { mintCredential as realMint } from './remote.mjs';
18
- import { hashPassword } from './mastoapi.mjs';
19
- import { webfingerHost } from './wire.mjs';
18
+ import { hashPassword } from '../client/masto/index.mjs';
19
+ import { webfingerHost } from '../core/wire.mjs';
20
20
  import { rootOf, recordLastUsed, writeJsonAtomic } from './home.mjs';
21
- import { insecureUrlReason } from './safefetch.mjs';
21
+ import { insecureUrlReason } from '../shared/safefetch.mjs';
22
22
  import { CURRENT_LAYOUT, isCurrent } from './migrate.mjs';
23
23
 
24
24
  const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#');
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url';
13
13
 
14
14
  export const LATEST_URL = 'https://raw.githubusercontent.com/jeff-zucker/FediPod/main/package.json';
15
15
 
16
- export const repoRoot = () => path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
16
+ export const repoRoot = () => path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
17
17
 
18
18
  export const localVersion = (root = repoRoot()) => {
19
19
  try { return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version || null; }
@@ -13,7 +13,7 @@
13
13
  // whole mechanism (the test suite sets it, so throwaway agents do not fight
14
14
  // over a real machine's door).
15
15
  import https from 'node:https';
16
- import { localFetch } from './localapi.mjs';
16
+ import { localFetch } from '../client/localapi.mjs';
17
17
 
18
18
  export const DIRECTORY_PORT = 8030;
19
19
 
@@ -17,9 +17,9 @@
17
17
 
18
18
  import crypto from 'node:crypto';
19
19
  import { handleDelivery } from './gateway-core.mjs';
20
- import { readCapped, safeFetch, isLoopbackHost } from './safefetch.mjs';
21
- import * as podRoot from './pod/root.mjs';
22
- import * as podPolicy from './pod/policy.mjs';
20
+ import { readCapped, safeFetch, isLoopbackHost } from '../shared/safefetch.mjs';
21
+ import * as podRoot from '../pod/root.mjs';
22
+ import * as podPolicy from '../pod/policy.mjs';
23
23
 
24
24
  // The one WebFinger document, spelled out here rather than imported from
25
25
  // wire.mjs: wire drags the agent's whole HTML pipeline (sanitize-html and
@@ -14,7 +14,7 @@
14
14
 
15
15
  import crypto from 'node:crypto';
16
16
  import { verifyHttpSignature, makeSafeLoader, makeReceipt, signReceipt } from './httpsig.mjs';
17
- import * as inbox from './pod/inbox.mjs';
17
+ import * as inbox from '../pod/inbox.mjs';
18
18
 
19
19
  const DEFAULT_MAX_BYTES = 512 * 1024; // mirror intake.mjs MAX_ITEM_BYTES
20
20
 
@@ -11,7 +11,7 @@
11
11
 
12
12
  import crypto from 'node:crypto';
13
13
  import { verifyRequestDetailed } from '@fedify/fedify/sig';
14
- import { assertPublicUrl, safeFetch, readCapped } from './safefetch.mjs';
14
+ import { assertPublicUrl, safeFetch, readCapped } from '../shared/safefetch.mjs';
15
15
 
16
16
  // An SSRF-safe JSON-LD document loader for Fedify's key fetch. The keyId in a
17
17
  // delivery is chosen by the sender, so every deref it triggers is untrusted