fedipod 1.40.0 → 1.43.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 (70) hide show
  1. package/README.md +5 -0
  2. package/architecture.md +1 -3
  3. package/architecture.svg +93 -85
  4. package/gateway.md +7 -2
  5. package/lib/client/c2s.mjs +16 -5
  6. package/lib/client/masto/oauth.mjs +1 -1
  7. package/lib/connections/vault.mjs +1 -1
  8. package/lib/core/agent.mjs +720 -0
  9. package/lib/core/deliver.mjs +11 -1
  10. package/lib/core/gateway-move.mjs +58 -0
  11. package/lib/core/intake/group.mjs +10 -11
  12. package/lib/core/keys.mjs +1 -1
  13. package/lib/core/lease.mjs +4 -3
  14. package/lib/core/publisher/index.mjs +4 -6
  15. package/lib/core/publisher/notes.mjs +73 -4
  16. package/lib/core/remote.mjs +54 -0
  17. package/lib/core/shapes/shapes-text.mjs +11 -3
  18. package/lib/core/wire.mjs +4 -1
  19. package/lib/device/admin/index.mjs +4 -11
  20. package/lib/device/admin/routes/setup.mjs +60 -5
  21. package/lib/device/admin/server.mjs +18 -5
  22. package/lib/device/cli/commands/account.mjs +1 -1
  23. package/lib/device/cli/commands/run.mjs +2 -2
  24. package/lib/device/cli/commands/service.mjs +1 -1
  25. package/lib/device/cli/commands/setup.mjs +1 -1
  26. package/lib/device/cli/commands/state.mjs +1 -1
  27. package/lib/device/cli/context.mjs +1 -1
  28. package/lib/{gateway → device}/directory.mjs +1 -1
  29. package/lib/device/gateway-move.mjs +3 -58
  30. package/lib/device/home.mjs +3 -50
  31. package/lib/{client → device}/localapi.mjs +2 -2
  32. package/lib/device/remote.mjs +3 -54
  33. package/lib/gateway/copy.mjs +12 -3
  34. package/lib/gateway/front-core.mjs +16 -17
  35. package/lib/gateway/front-pages.mjs +31 -0
  36. package/lib/gateway/gateway-core.mjs +1 -1
  37. package/lib/gateway/held-mail.mjs +3 -0
  38. package/lib/gateway/keeper-session.mjs +10 -1
  39. package/lib/gateway/keeper.mjs +3 -0
  40. package/lib/gateway/masto-gateway.mjs +61 -12
  41. package/lib/gateway/outbox-door.mjs +31 -0
  42. package/lib/gateway/state-api.mjs +39 -7
  43. package/lib/server/embed.mjs +4 -4
  44. package/lib/shared/files.mjs +49 -0
  45. package/lib/shared/ua.mjs +3 -0
  46. package/lib/surface/index.mjs +7 -0
  47. package/lib/{device/admin → surface}/origins.mjs +1 -1
  48. package/lib/{device/admin → surface}/routes/gateway.mjs +7 -11
  49. package/lib/{device/admin → surface}/routes/lifecycle.mjs +3 -3
  50. package/lib/{device/admin → surface}/routes/owner.mjs +4 -60
  51. package/lib/{device/admin → surface}/routes/social.mjs +1 -1
  52. package/lib/{device/admin → surface}/static.mjs +6 -1
  53. package/lib/{device/admin → surface}/surface.mjs +19 -16
  54. package/package.json +23 -5
  55. package/run-agent.mjs +19 -720
  56. package/scripts/build-app.mjs +4 -0
  57. package/scripts/check-netlify-bundles.mjs +88 -0
  58. package/scripts/check-pod-calls.mjs +42 -0
  59. package/scripts/vendor-sanitize.mjs +27 -0
  60. package/vendor/idp-grant.cjs +11 -5
  61. package/vendor/sanitize-html.cjs +11456 -0
  62. package/web/app/agent.mjs +4 -2
  63. package/web/app/copy-mode.mjs +27 -3
  64. package/web/app/dist/boot.js.map +1 -1
  65. package/web/app/dist/sw.js +110 -24
  66. package/web/app/dist/sw.js.map +4 -4
  67. package/web/app-signin/app-signin.mjs +2 -2
  68. package/web/app-signin/index.html +13 -5
  69. package/browser.svg +0 -72
  70. /package/lib/{device/admin → surface}/routes/connections.mjs +0 -0
@@ -16,12 +16,12 @@ import path from 'node:path';
16
16
 
17
17
  import { createRequire } from 'node:module';
18
18
 
19
- import { Agent } from '../../run-agent.mjs';
20
- import { RemotePod } from '../device/remote.mjs';
19
+ import { Agent } from '../core/agent.mjs';
20
+ import { RemotePod } from '../core/remote.mjs';
21
21
  import { apUrls, DEFAULT_ROOT } from '../core/wire.mjs';
22
22
  import { handleDelivery } from '../gateway/gateway-core.mjs';
23
- import { writeJsonAtomic } from '../device/home.mjs';
24
- import { buildAdminSurface } from '../device/admin/index.mjs';
23
+ import { writeJsonAtomic } from '../shared/files.mjs';
24
+ import { buildAdminSurface } from '../surface/index.mjs';
25
25
  import { FixedAuthorities } from '../shared/guard.mjs';
26
26
  import { CONNECTION_PREFIX, fileVault, moveVault, podVault } from '../connections/vault.mjs';
27
27
 
@@ -0,0 +1,49 @@
1
+ // files.mjs — writing a file without a window in which it is half there, and
2
+ // showing a path the way a person types it.
3
+ //
4
+ // The files written this way are the ones nothing can rebuild: keys.json is
5
+ // the private key remote servers have cached, and credential.json holds a
6
+ // secret a Solid server mints once and will not mint again. A plain
7
+ // writeFileSync truncates first, so a crash, a power loss or a full disk
8
+ // between truncate and write leaves an empty or half-written file where the
9
+ // identity used to be.
10
+ //
11
+ // rename(2) is atomic on POSIX, so a reader — or a backup running at the wrong
12
+ // moment — sees either the old file or the new one. fsync before it, so the
13
+ // content is on the platter and not merely in the page cache when the rename
14
+ // lands. Windows has no atomic rename over an existing file; there the copy
15
+ // path is still better than a truncate, and this is a best effort by design.
16
+
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ export function writeFileAtomic(file, body, { mode = 0o600 } = {}) {
23
+ const tmp = `${file}.${process.pid}.tmp`;
24
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
25
+ const fd = fs.openSync(tmp, 'w', mode);
26
+ try {
27
+ fs.writeFileSync(fd, body);
28
+ fs.fsyncSync(fd);
29
+ } finally {
30
+ fs.closeSync(fd);
31
+ }
32
+ fs.renameSync(tmp, file);
33
+ }
34
+
35
+ // The same, for the JSON these files all happen to be.
36
+ export function writeJsonAtomic(file, obj, opts) {
37
+ writeFileAtomic(file, JSON.stringify(obj, null, 2) + '\n', opts);
38
+ }
39
+
40
+ // A path for display: the home directory becomes `~`, a `file:` URL is shown
41
+ // as the path it is. Never used to build a path — `~` is the shell's, not the
42
+ // filesystem's, and anything that resolves one must expand it first.
43
+ export function tildify(p, homedir = os.homedir()) {
44
+ if (!p) return p;
45
+ let s = String(p);
46
+ if (s.startsWith('file://')) { try { s = fileURLToPath(s); } catch { return s; } }
47
+ if (s === homedir) return '~';
48
+ return s.startsWith(homedir + path.sep) ? '~' + s.slice(homedir.length) : s;
49
+ }
package/lib/shared/ua.mjs CHANGED
@@ -27,3 +27,6 @@ try {
27
27
  // Renamed 2026-07-31, once solidcommunity.net was back up and both actors there
28
28
  // were retired: the name in their logs no longer has to match the name in here.
29
29
  export const USER_AGENT = `fedipod/${version} (+https://github.com/jeff-zucker/FediPod)`;
30
+ // The same version, for anything else that reports it (the publisher's
31
+ // nodeinfo and status), read the same best-effort way.
32
+ export const AGENT_VERSION = version;
@@ -0,0 +1,7 @@
1
+ // The identity's HTTP surface: the Mastodon client API and OAuth, the
2
+ // ActivityPub client-to-server door, nodeinfo, the owner's door and routes,
3
+ // and the pages and client served off disk. buildAdminSurface returns one
4
+ // request handler; whatever listens hands it requests.
5
+
6
+ export { buildAdminSurface } from './surface.mjs';
7
+ export { namedOrigin, secureOrigin, wsOrigins } from './origins.mjs';
@@ -1,7 +1,7 @@
1
1
  // origins.mjs — the addresses an identity answers on, as a browser must be
2
2
  // sent to them.
3
3
 
4
- import { allowedAuthorities, hostLabel } from '../../shared/guard.mjs';
4
+ import { allowedAuthorities, hostLabel } from '../shared/guard.mjs';
5
5
 
6
6
  // The origin a browser should be sent to for an identity. A browser keys
7
7
  // storage per ORIGIN, so linking every identity at localhost:<port> files them
@@ -4,10 +4,9 @@
4
4
  import crypto from 'node:crypto';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import { spawn } from 'node:child_process';
8
7
 
9
8
  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
9
+ const { agent, log, allowed, embedded, port, publicOrigin, versionOnDisk, isGroup, restart } = ctx; // eslint-disable-line no-unused-vars
11
10
  const json = (res, status, obj) => { ctx.json(res, status, obj); return true; };
12
11
  switch (p) {
13
12
  // The inbox gateway lifecycle (opt-in). `configure` points at a
@@ -50,15 +49,10 @@ export async function post(p, body, ctx, req, res) { // eslint-disable-line no
50
49
  const root = cfg.root ? (cfg.root.endsWith('/') ? cfg.root : `${cfg.root}/`) : 'fedipod/';
51
50
  return `${base}${root}ap/actor`;
52
51
  };
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);
52
+ // Fronting and un-fronting rename every id, which only a restart
53
+ // applies. A host that cannot restart itself refuses them.
54
+ const selfRestart = restart;
55
+ const noRestart = { error: 'this identity runs inside its pod server, which cannot restart for it — front it, or un-front it, from a standalone agent' };
62
56
  if (body.action === 'configure') {
63
57
  if (!/^https:\/\/\S+$/.test(String(body.url || ''))) return json(res, 400, { error: 'gateway url must be https' });
64
58
  if (!/^https?:\/\/\S+$/.test(String(body.webId || ''))) return json(res, 400, { error: 'gateway webId must be a URL' });
@@ -121,6 +115,7 @@ export async function post(p, body, ctx, req, res) { // eslint-disable-line no
121
115
  if (agent.embedded) {
122
116
  return json(res, 400, { error: 'this identity runs inside its pod server and has no portable credential — attach from a standalone agent' });
123
117
  }
118
+ if (body.fronted === true && !selfRestart) return json(res, 400, noRestart);
124
119
  const named = !!String(body.handle || '').trim();
125
120
  let handle = String(body.handle || cfg.handle || '').toLowerCase().trim();
126
121
  if (!handle) return json(res, 400, { error: 'a name at the gateway is required' });
@@ -177,6 +172,7 @@ export async function post(p, body, ctx, req, res) { // eslint-disable-line no
177
172
  if (body.action === 'forget') {
178
173
  const wasLocked = g.mode === 'locked';
179
174
  const wasFronted = !!g.frontActor;
175
+ if (wasFronted && !selfRestart) return json(res, 400, noRestart);
180
176
  delete cfg.gateway; agent.store.setConfig(cfg);
181
177
  if (agent.publisher) agent.publisher.config.gateway = undefined;
182
178
  await agent.store.flush();
@@ -2,9 +2,9 @@
2
2
  // pruning the inbox, rebuilding posts, republishing, parking and reviving,
3
3
  // rotating the key, moving, aliases, importing, retiring.
4
4
 
5
- import { resolveHandle } from '../../../core/social.mjs';
6
- import { webfingerHost } from '../../../core/wire.mjs';
7
- import { normalizeImport, IMPORT_KINDS } from '../../../connections/import.mjs';
5
+ import { resolveHandle } from '../../core/social.mjs';
6
+ import { webfingerHost } from '../../core/wire.mjs';
7
+ import { normalizeImport, IMPORT_KINDS } from '../../connections/import.mjs';
8
8
 
9
9
  export async function post(p, body, ctx, req, res) { // eslint-disable-line no-unused-vars
10
10
  const { agent, log, allowed, embedded, port, publicOrigin, versionOnDisk, isGroup } = ctx; // eslint-disable-line no-unused-vars
@@ -3,13 +3,11 @@
3
3
 
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
- import { announceModeration } from '../../../core/social.mjs';
7
- import { addRemoveActivity, publicHandle } from '../../../core/wire.mjs';
8
- import { hashPassword } from '../../../client/masto/index.mjs';
9
- import { identityHomes, rootOf, tildify, defaultProfile } from '../../home.mjs';
10
- import { localFetch } from '../../../client/localapi.mjs';
6
+ import { announceModeration } from '../../core/social.mjs';
7
+ import { addRemoveActivity, publicHandle } from '../../core/wire.mjs';
8
+ import { hashPassword } from '../../client/masto/index.mjs';
9
+ import { tildify } from '../../shared/files.mjs';
11
10
  import { AGENT_VERSION } from '../static.mjs';
12
- import { secureOrigin } from '../origins.mjs';
13
11
 
14
12
  // The identity itself. Changing any of these means a different actor at a
15
13
  // different address, which is a new setup, not an edit.
@@ -42,60 +40,6 @@ export async function get(p, ctx, req, res, url) { // eslint-disable-line no-u
42
40
  stats: agent.store.read('gateway-stats.json', { verified: 0, unverified: 0, lastAt: null }),
43
41
  });
44
42
  }
45
- // The other identities on this machine, so the page can link to each one.
46
- // Their `agent.json` is the only file read — a port and a handle. A
47
- // sibling's credential and keys are never opened; anything else shown
48
- // here comes from that agent answering /status for itself.
49
- if (req.method === 'GET' && p === '/profiles') {
50
- const here = path.resolve(agent.home || '');
51
- // Which identity a plain command means: the last one started. A
52
- // different question from `current`, which is the page you are on.
53
- const wasLast = defaultProfile(rootOf(here));
54
- const rows = await Promise.all(identityHomes(rootOf(here)).map(async ({ name, dir }) => {
55
- // agent.json holds a port AND a handle — setup records both, exactly
56
- // so the named origin can be built before that agent has said a word.
57
- let rec = {};
58
- try { rec = JSON.parse(fs.readFileSync(path.join(dir, 'agent.json'), 'utf8')) || {}; } catch {}
59
- const port = rec.port || null;
60
- // A directory under profiles/ is not an identity until it holds a
61
- // credential or has run somewhere. A half-finished setup leaves one
62
- // behind, and listing it offers a page that cannot exist yet.
63
- if (!port && !fs.existsSync(path.join(dir, 'credential.json'))) return null;
64
- const current = path.resolve(dir) === here;
65
- const live = current ? agent.status() : port
66
- ? await localFetch(agent.home, port, '/status', { timeout: 1200 })
67
- .then(r => r.json()).catch(() => null)
68
- : null;
69
- // The handle is right here — spending it on the origin is the whole
70
- // point of having asked that agent who it is. A stopped one never
71
- // answers, so its recorded handle stands in: the link has to be named
72
- // before you get there, or starting it lands you on the shared origin.
73
- const handle = live?.handle || rec.handle || null;
74
- const origin = port
75
- ? secureOrigin(handle, port)
76
- : null;
77
- // The fediverse address, assembled from the two things /status has:
78
- // the handle, and the pod host its actor URL sits on.
79
- let address = null;
80
- if (live?.handle && live?.actor) {
81
- try {
82
- const front = live.actor.match(/\/u\/([^/]+)\/ap\/actor\/?$/)?.[1];
83
- address = `${front || live.handle}@${new URL(live.actor).host}`;
84
- } catch { /* not a URL yet */ }
85
- }
86
- return {
87
- name, port, current,
88
- admin: origin ? `${origin}/admin/` : null,
89
- app: origin ? `${origin}/` : null,
90
- handle,
91
- address,
92
- lastUsed: name === wasLast,
93
- kind: live?.kind || null,
94
- mode: live ? live.mode : null,
95
- };
96
- }));
97
- return json(res, 200, { identities: rows.filter(Boolean) });
98
- }
99
43
  if (req.method === 'GET' && p === '/blocks') return json(res, 200, agent.store.getBlocklist());
100
44
  if (req.method === 'GET' && p === '/log') return json(res, 200, { lines: agent.logLines(200) });
101
45
  if (req.method === 'GET' && p === '/deadletter') return json(res, 200, { items: agent.store.getDeadLetters() });
@@ -2,7 +2,7 @@
2
2
  // moderating: posting, the tag feed, follows, mutes, blocks, ejecting,
3
3
  // retracting, review, joins, the moderation queue.
4
4
 
5
- import { followHandle, followActor, unfollowActor, ejectFollower, retractAnnouncement, admitRequest, refuseRequest, applyModeration } from '../../../core/social.mjs';
5
+ import { followHandle, followActor, unfollowActor, ejectFollower, retractAnnouncement, admitRequest, refuseRequest, applyModeration } from '../../core/social.mjs';
6
6
 
7
7
  export async function get(p, ctx, req, res, url) { // eslint-disable-line no-unused-vars
8
8
  const { agent, log, allowed, embedded, port, publicOrigin, versionOnDisk, isGroup } = ctx; // eslint-disable-line no-unused-vars
@@ -10,7 +10,7 @@ import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { wsOrigins } from './origins.mjs';
12
12
 
13
- export const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
13
+ export const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
14
14
 
15
15
  const PHANPY_DIR = path.join(projectRoot, 'phanpy/dist');
16
16
  // The vendored client is upstream's build, byte for byte, so the integrity
@@ -63,6 +63,11 @@ export const webMount = (pathname) => {
63
63
  export const SETUP_PAGE = '/admin/setup/';
64
64
 
65
65
  export const AGENT_VERSION = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')).version;
66
+ // Read again on each ask: the checkout can move while a process stays up.
67
+ export const readVersionOnDisk = () => {
68
+ try { return JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')).version || null; }
69
+ catch { return null; }
70
+ };
66
71
 
67
72
  const MIME = {
68
73
  '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css',
@@ -8,17 +8,14 @@
8
8
 
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
- import { MastoApi } from '../../client/masto/index.mjs';
12
- import { C2S } from '../../client/c2s.mjs';
13
- import { makeC2sAuth } from '../../client/oidc-auth.mjs';
14
- import { Streaming } from '../../client/streaming.mjs';
15
- import { nodeinfoPointer, nodeinfoDoc } from '../../core/wire.mjs';
16
- import { checkRequest } from '../../shared/guard.mjs';
17
- import { hasCredential } from '../setup.mjs';
18
- import { localVersion } from '../update.mjs';
19
- import { projectRoot, AGENT_VERSION, SETUP_PAGE, securityHeaders, sendJson, serveStatic, serveWeb, webDirRedirect, webMount } from './static.mjs';
11
+ import { MastoApi } from '../client/masto/index.mjs';
12
+ import { C2S } from '../client/c2s.mjs';
13
+ import { makeC2sAuth } from '../client/oidc-auth.mjs';
14
+ import { Streaming } from '../client/streaming.mjs';
15
+ import { nodeinfoPointer, nodeinfoDoc } from '../core/wire.mjs';
16
+ import { checkRequest } from '../shared/guard.mjs';
17
+ import { projectRoot, AGENT_VERSION, SETUP_PAGE, readVersionOnDisk, securityHeaders, sendJson, serveStatic, serveWeb, webDirRedirect, webMount } from './static.mjs';
20
18
  import * as owner from './routes/owner.mjs';
21
- import * as setup from './routes/setup.mjs';
22
19
  import * as lifecycle from './routes/lifecycle.mjs';
23
20
  import * as gateway from './routes/gateway.mjs';
24
21
  import * as social from './routes/social.mjs';
@@ -65,9 +62,8 @@ function readBody(req) {
65
62
  });
66
63
  }
67
64
 
68
- // The routes, in the order the one if-chain and the one switch used to be
69
- // read: the first module that answers a path wins.
70
- const ROUTES = [owner, setup, lifecycle, gateway, social, connections];
65
+ // Whether this home has been set up: the credential FILE, never configured().
66
+ const hasCredential = (home) => fs.existsSync(path.join(home, 'credential.json'));
71
67
 
72
68
  // The whole admin/facade surface as one request handler, with nothing that
73
69
  // belongs to a process of its own. startAdmin wraps it in listeners; the CSS
@@ -83,10 +79,17 @@ const SESSION_FILES = {
83
79
  '/oauth/session/oidc-session.mjs': 'lib/session/oidc-session.mjs',
84
80
  };
85
81
 
82
+ // `extraRoutes` are the routes a host process adds after the owner's — the
83
+ // DeviceAgent's setup and process routes; a pod server adds none. `restart`
84
+ // restarts the host process, for the changes that need one; a host that
85
+ // cannot passes nothing, and those changes are refused.
86
86
  export function buildAdminSurface({ agent, gate, allowed, log = console.log,
87
87
  port = null, handle = null, embedded = false, basePath = '/', mount = '',
88
- publicOrigin = null, scheme = null,
89
- versionOnDisk = () => localVersion(projectRoot) }) {
88
+ publicOrigin = null, scheme = null, extraRoutes = [], restart = null,
89
+ versionOnDisk = readVersionOnDisk }) {
90
+ // The routes, in the order the one if-chain and the one switch used to be
91
+ // read: the first module that answers a path wins.
92
+ const ROUTES = [owner, ...extraRoutes, lifecycle, gateway, social, connections];
90
93
  const json = (res, status, obj) => sendJson(res, status, obj, allowed);
91
94
  const masto = new MastoApi({ agent, log, allowed, scheme, embedded, mount,
92
95
  throughDoor: (req) => Boolean(gate?.upgradeOk?.(req)) });
@@ -123,7 +126,7 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
123
126
  const atPath = (p_) => mount + (basePath === '/' ? p_ : basePath.slice(0, -1) + p_);
124
127
 
125
128
  // What every route may reach: the agent and the deployment's facts.
126
- const ctx = { agent, log, allowed, embedded, port, handle, publicOrigin, versionOnDisk, isGroup, json, setup: setup_ };
129
+ const ctx = { agent, log, allowed, embedded, port, handle, publicOrigin, versionOnDisk, isGroup, json, setup: setup_, restart };
127
130
 
128
131
  const handler = async (req, res) => {
129
132
  const url = new URL(req.url, 'http://localhost');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fedipod",
3
- "version": "1.40.0",
3
+ "version": "1.43.0",
4
4
  "description": "Standalone single-actor ActivityPub agent whose wire face, RDF truth and state all live on a Solid pod (CSS). Bundles a Phanpy UI and a Mastodon client-API facade.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,6 +14,24 @@
14
14
  "bin": {
15
15
  "fedipod": "bin/fedipod.mjs"
16
16
  },
17
+ "exports": {
18
+ "./embed": "./lib/server/embed.mjs",
19
+ "./front": "./lib/gateway/front-core.mjs",
20
+ "./front-pages": "./lib/gateway/front-pages.mjs",
21
+ "./surface": "./lib/surface/index.mjs",
22
+ "./agent": "./lib/core/agent.mjs",
23
+ "./remote": "./lib/core/remote.mjs",
24
+ "./place": "./lib/core/place.mjs",
25
+ "./core/*": "./lib/core/*",
26
+ "./pod/*": "./lib/pod/*",
27
+ "./client/*": "./lib/client/*",
28
+ "./connections/*": "./lib/connections/*",
29
+ "./gateway/*": "./lib/gateway/*",
30
+ "./shared/*": "./lib/shared/*",
31
+ "./surface/*": "./lib/surface/*",
32
+ "./vendor/*": "./vendor/*",
33
+ "./package.json": "./package.json"
34
+ },
17
35
  "files": [
18
36
  "bin",
19
37
  "lib",
@@ -30,22 +48,22 @@
30
48
  "architecture.svg",
31
49
  "device-agent.md",
32
50
  "architecture.md",
33
- "gateway.md",
34
- "browser.svg"
51
+ "gateway.md"
35
52
  ],
36
53
  "scripts": {
37
54
  "start": "node bin/fedipod.mjs up",
38
55
  "install-service": "node bin/fedipod.mjs install-service",
39
56
  "uninstall-service": "node bin/fedipod.mjs uninstall-service",
40
57
  "prepublishOnly": "npm test",
41
- "test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node --test packages/fedipod-bb/test/*.test.mjs && node scripts/check-sizes.mjs",
58
+ "test": "node claude/smoke-tests/agent-smoke.mjs && node claude/smoke-tests/front-hard-test.mjs && node claude/smoke-tests/copy-test.mjs && node claude/smoke-tests/copy-mode-test.mjs && node claude/smoke-tests/masto-gateway-test.mjs && node scripts/check-pod-calls.mjs && node scripts/verify-ui.mjs && node packages/fedipod-bb/scripts/link-fedipod.mjs && node --test packages/fedipod-bb/test/*.test.mjs && node scripts/check-sizes.mjs",
42
59
  "verify-ui": "node scripts/verify-ui.mjs",
43
60
  "build:sw": "node scripts/build-app.mjs --entry web/app/sw-src.mjs --out web/app/dist/sw.js",
44
61
  "build:boot": "node scripts/build-app.mjs --entry web/app/boot.mjs --out web/app/dist/boot.js",
45
62
  "build:app": "npm run build:sw && npm run build:boot",
46
63
  "stage": "node scripts/stage-site.mjs",
47
64
  "audit": "npm audit --omit=dev",
48
- "agent": "node run-agent.mjs"
65
+ "agent": "node run-agent.mjs",
66
+ "vendor:sanitize": "node scripts/vendor-sanitize.mjs"
49
67
  },
50
68
  "dependencies": {
51
69
  "@fedify/fedify": "^2.3.4",