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.
Files changed (152) hide show
  1. package/README.md +22 -6
  2. package/dist/claims.d.ts +8 -0
  3. package/dist/claims.js +10 -0
  4. package/dist/handler.d.ts +13 -0
  5. package/dist/handler.js +56 -11
  6. package/dist/handler.jsonld +8 -0
  7. package/dist/store-pod.js +18 -4
  8. package/lib/{c2s.mjs → client/c2s.mjs} +10 -3
  9. package/lib/{localapi.mjs → client/localapi.mjs} +2 -2
  10. package/lib/client/masto/accounts.mjs +264 -0
  11. package/lib/client/masto/body.mjs +69 -0
  12. package/lib/client/masto/index.mjs +183 -0
  13. package/lib/client/masto/instance.mjs +104 -0
  14. package/lib/client/masto/media.mjs +133 -0
  15. package/lib/client/masto/oauth.mjs +599 -0
  16. package/lib/client/masto/render.mjs +459 -0
  17. package/lib/client/masto/statuses.mjs +331 -0
  18. package/lib/client/masto/timelines.mjs +316 -0
  19. package/lib/{streaming.mjs → client/streaming.mjs} +1 -1
  20. package/lib/{acctfeed.mjs → connections/acctfeed.mjs} +1 -1
  21. package/lib/{atproto.mjs → connections/atproto.mjs} +15 -16
  22. package/lib/{bskygroup.mjs → connections/bskygroup.mjs} +1 -1
  23. package/lib/{fediacct.mjs → connections/fediacct.mjs} +31 -35
  24. package/lib/{import.mjs → connections/import.mjs} +1 -1
  25. package/lib/{tagfeed.mjs → connections/tagfeed.mjs} +3 -3
  26. package/lib/connections/vault.mjs +114 -0
  27. package/lib/core/as2.mjs +170 -0
  28. package/lib/core/contexts/activitystreams.json +379 -0
  29. package/lib/core/contexts/did-v1.json +57 -0
  30. package/lib/core/contexts/fep-5711.json +36 -0
  31. package/lib/core/contexts/gotosocial.json +86 -0
  32. package/lib/core/contexts/identity-v1.json +152 -0
  33. package/lib/core/contexts/index.mjs +45 -0
  34. package/lib/core/contexts/join-lemmy.json +33 -0
  35. package/lib/core/contexts/joinmastodon.json +28 -0
  36. package/lib/core/contexts/map.json +16 -0
  37. package/lib/core/contexts/miscellany.json +19 -0
  38. package/lib/core/contexts/schemaorg.json +8845 -0
  39. package/lib/core/contexts/security-data-integrity-v1.json +78 -0
  40. package/lib/core/contexts/security-data-integrity-v2.json +81 -0
  41. package/lib/core/contexts/security-multikey-v1.json +35 -0
  42. package/lib/core/contexts/security-v1.json +74 -0
  43. package/lib/core/contexts/webfinger.json +10 -0
  44. package/lib/{deliver.mjs → core/deliver.mjs} +2 -2
  45. package/lib/core/graphview.mjs +269 -0
  46. package/lib/core/intake/activities.mjs +437 -0
  47. package/lib/core/intake/activity.mjs +240 -0
  48. package/lib/core/intake/channel.mjs +144 -0
  49. package/lib/core/intake/group.mjs +222 -0
  50. package/lib/core/intake/index.mjs +629 -0
  51. package/lib/core/intake/notes.mjs +288 -0
  52. package/lib/core/intake/verify.mjs +142 -0
  53. package/lib/{keys.mjs → core/keys.mjs} +1 -1
  54. package/lib/core/publisher/collections.mjs +229 -0
  55. package/lib/core/publisher/index.mjs +421 -0
  56. package/lib/core/publisher/notes.mjs +188 -0
  57. package/lib/core/publisher/questions.mjs +233 -0
  58. package/lib/core/publisher/restore.mjs +199 -0
  59. package/lib/core/shapes/activitystreams.ttl +129 -0
  60. package/lib/core/shapes/index.mjs +107 -0
  61. package/lib/core/shapes/shapes-text.mjs +13 -0
  62. package/lib/{social.mjs → core/social.mjs} +2 -2
  63. package/lib/{store.mjs → core/store.mjs} +4 -0
  64. package/lib/{wire.mjs → core/wire.mjs} +2 -2
  65. package/lib/device/admin/index.mjs +13 -0
  66. package/lib/device/admin/origins.mjs +35 -0
  67. package/lib/device/admin/routes/connections.mjs +144 -0
  68. package/lib/device/admin/routes/gateway.mjs +199 -0
  69. package/lib/device/admin/routes/lifecycle.mjs +191 -0
  70. package/lib/device/admin/routes/owner.mjs +322 -0
  71. package/lib/device/admin/routes/setup.mjs +393 -0
  72. package/lib/device/admin/routes/social.mjs +188 -0
  73. package/lib/device/admin/server.mjs +95 -0
  74. package/lib/device/admin/static.mjs +244 -0
  75. package/lib/device/admin/surface.mjs +274 -0
  76. package/lib/device/cli/commands/account.mjs +586 -0
  77. package/lib/device/cli/commands/run.mjs +278 -0
  78. package/lib/device/cli/commands/service.mjs +221 -0
  79. package/lib/device/cli/commands/setup.mjs +410 -0
  80. package/lib/device/cli/commands/state.mjs +559 -0
  81. package/lib/device/cli/context.mjs +288 -0
  82. package/lib/{migrate.mjs → device/migrate.mjs} +1 -1
  83. package/lib/{remote.mjs → device/remote.mjs} +3 -3
  84. package/lib/{setup.mjs → device/setup.mjs} +3 -3
  85. package/lib/{update.mjs → device/update.mjs} +1 -1
  86. package/lib/{directory.mjs → gateway/directory.mjs} +1 -1
  87. package/lib/{front-core.mjs → gateway/front-core.mjs} +3 -3
  88. package/lib/{gateway-core.mjs → gateway/gateway-core.mjs} +1 -1
  89. package/lib/{httpsig.mjs → gateway/httpsig.mjs} +1 -1
  90. package/lib/server/embed.mjs +405 -0
  91. package/lib/{links.mjs → shared/links.mjs} +1 -1
  92. package/lib/{ua.mjs → shared/ua.mjs} +1 -1
  93. package/package.json +1 -1
  94. package/run-agent.mjs +33 -25
  95. package/web/admin/actors.js +145 -0
  96. package/web/admin/common.js +23 -0
  97. package/web/admin/connections.js +112 -0
  98. package/web/admin/gateway.js +111 -0
  99. package/web/admin/group.js +258 -0
  100. package/web/admin/index.html +7 -1
  101. package/web/admin/record.js +378 -0
  102. package/web/admin/setup/index.html +1 -0
  103. package/web/admin/setup/setup.js +2 -13
  104. package/web/admin/upkeep.js +170 -0
  105. package/web/app/README.md +6 -6
  106. package/web/app/admin-facade.mjs +3 -3
  107. package/web/app/agent.mjs +14 -16
  108. package/web/app/atproto-browser.mjs +1 -1
  109. package/web/app/boot.mjs +2 -3
  110. package/web/app/deliver-relay.mjs +1 -1
  111. package/web/app/dist/boot.js +22 -3
  112. package/web/app/dist/boot.js.map +2 -2
  113. package/web/app/dist/sw.js +21913 -5446
  114. package/web/app/dist/sw.js.map +4 -4
  115. package/web/app/fediacct-browser.mjs +1 -1
  116. package/web/app/keys-browser.mjs +27 -4
  117. package/web/app/shims/shapes-text.mjs +8 -0
  118. package/web/app/signup.mjs +2 -3
  119. package/web/app/site/admin/actors.js +145 -0
  120. package/web/app/site/admin/common.js +23 -0
  121. package/web/app/site/admin/connections.js +112 -0
  122. package/web/app/site/admin/gateway.js +111 -0
  123. package/web/app/site/admin/group.js +258 -0
  124. package/web/app/site/admin/index.html +7 -1
  125. package/web/app/site/admin/record.js +378 -0
  126. package/web/app/site/admin/setup/index.html +1 -0
  127. package/web/app/site/admin/setup/setup.js +2 -13
  128. package/web/app/site/admin/upkeep.js +170 -0
  129. package/web/app/site/boot.js +22 -3
  130. package/web/app/site/sw.js +21913 -5446
  131. package/web/app/sw-src.mjs +17 -2
  132. package/lib/admin.mjs +0 -1913
  133. package/lib/embed.mjs +0 -220
  134. package/lib/intake.mjs +0 -1981
  135. package/lib/mastoapi.mjs +0 -2284
  136. package/lib/publisher.mjs +0 -1192
  137. package/web/admin/admin.js +0 -1181
  138. package/web/app/site/admin/admin.js +0 -1181
  139. /package/lib/{oidc-auth.mjs → client/oidc-auth.mjs} +0 -0
  140. /package/lib/{webpush.mjs → client/webpush.mjs} +0 -0
  141. /package/lib/{bskyfeed.mjs → connections/bskyfeed.mjs} +0 -0
  142. /package/lib/{lease.mjs → core/lease.mjs} +0 -0
  143. /package/lib/{polls.mjs → core/polls.mjs} +0 -0
  144. /package/lib/{proof.mjs → core/proof.mjs} +0 -0
  145. /package/lib/{storage.mjs → core/storage.mjs} +0 -0
  146. /package/lib/{account.mjs → device/account.mjs} +0 -0
  147. /package/lib/{certs.mjs → device/certs.mjs} +0 -0
  148. /package/lib/{export-collections.mjs → device/export-collections.mjs} +0 -0
  149. /package/lib/{home.mjs → device/home.mjs} +0 -0
  150. /package/lib/{ports.mjs → device/ports.mjs} +0 -0
  151. /package/lib/{guard.mjs → shared/guard.mjs} +0 -0
  152. /package/lib/{safefetch.mjs → shared/safefetch.mjs} +0 -0
@@ -3,26 +3,15 @@
3
3
  // at localhost:<port> or at <handle>.localhost:<port>.
4
4
  //
5
5
  // External file, not an inline script: the CSP allows 'self' plus the hashes
6
- // of Phanpy's own inline bootstrap, and nothing else.
6
+ // of Phanpy's own inline bootstrap, and nothing else. `$`, `BASE`, `api` and
7
+ // `postJson` come from ../common.js, loaded first.
7
8
 
8
- const $ = (id) => document.getElementById(id);
9
9
  // Empty means gone, not a blank line where a sentence used to be.
10
10
  const strap = (t) => { const el = $('strap'); el.textContent = t || ''; el.hidden = !t; };
11
11
  // By id, not by position: these were `body > section` until a wrapper went
12
12
  // round them for layout, and the selector then matched nothing — every pane
13
13
  // kept whatever it started as and the page sat on "Reading this agent's state".
14
14
  const show = (id) => { for (const s of document.querySelectorAll('section[id^="pane-"]')) s.hidden = s.id !== id; };
15
- // The door this page is served behind: nothing when the agent runs on this
16
- // machine, `/app` when the identity is hosted by its own pod server. Read from
17
- // the page's own address, so one build serves both.
18
- const BASE = location.pathname.replace(/\/admin\/.*$/u, '');
19
- const api = async (path, init) => {
20
- const res = await fetch(BASE + path, { ...init, headers: { ...(init?.headers || {}), 'x-fedipod-page': '1' } });
21
- return { status: res.status, json: await res.json().catch(() => null) };
22
- };
23
- const postJson = (path, body) => api(path, {
24
- method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
25
- });
26
15
 
27
16
  let state = null;
28
17
 
@@ -0,0 +1,170 @@
1
+ // upkeep.js — upkeep and the lifecycle: the output pane, drain, recover,
2
+ // log and dead letters; the confirmations for rotate, retire and move; and
3
+ // the inbox backlog panel.
4
+
5
+ // Which button filled the output pane, so clicking that one again closes it
6
+ // rather than re-fetching the same thing under an already-open panel.
7
+ let outputSource = null;
8
+
9
+ const OUTPUT_TITLES = { log: 'Log', deadletter: 'Dead letters', drain: 'Inbox drain', rebuild: 'Recovered posts' };
10
+ const output = (obj, source = null) => {
11
+ closePanels('output');
12
+ outputSource = source;
13
+ solWindow.show('output', OUTPUT_TITLES[source] || 'Output');
14
+ $('output').textContent = typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2);
15
+ };
16
+
17
+ const showingFrom = (source) => outputSource === source && solWindow.openId() === 'output';
18
+
19
+ // Drain is the one that does work rather than reveals it, so a second click
20
+ // re-runs instead of closing. It can take a while — the agent joins a sweep
21
+ // already in flight and queues another after it — and a button that looks dead
22
+ // for a minute reads as broken.
23
+ $('do-drain').addEventListener('click', async (ev) => {
24
+ const b = ev.currentTarget;
25
+ b.disabled = true;
26
+ b.textContent = 'draining…';
27
+ say('draining the inbox — if a sweep is already running this waits for it to finish');
28
+ const r = await write('/drain', {}, 'inbox drained');
29
+ b.disabled = false;
30
+ b.textContent = 'Drain the inbox';
31
+ if (r) {
32
+ const box = r.inbox || {};
33
+ say(`inbox drained — ${box.count ?? 0} still waiting`);
34
+ output(r, 'drain');
35
+ }
36
+ });
37
+
38
+ // Like the drain, this does work rather than reveals it, so a second click
39
+ // re-runs. Nothing here can lose anything: it only adds posts back.
40
+ $('do-rebuild').addEventListener('click', async (ev) => {
41
+ const b = ev.currentTarget;
42
+ b.disabled = true;
43
+ b.textContent = 'recovering…';
44
+ say('reading what the pod still holds — one request per post, so this takes a moment');
45
+ const r = await write('/rebuild', {}, 'checked');
46
+ b.disabled = false;
47
+ b.textContent = 'Recover posts';
48
+ if (!r) return;
49
+ if (r.why) { say(r.why, 'err'); return; }
50
+ if (!r.landed) { say('recovered posts could NOT be saved — see the log', 'err'); return; }
51
+ say(r.recovered
52
+ ? `recovered ${r.recovered} post(s) the pod had and this machine did not`
53
+ : `nothing was missing — the pod indexed ${r.indexed} post(s), all of them already here`);
54
+ output(r, 'rebuild');
55
+ });
56
+
57
+ $('do-log').addEventListener('click', async () => {
58
+ if (showingFrom('log')) { closePanels(); return; }
59
+ const { json } = await api('/log');
60
+ output((json?.lines || []).slice(-60).join('\n') || 'nothing logged yet', 'log');
61
+ });
62
+
63
+ $('do-deadletter').addEventListener('click', async () => {
64
+ if (showingFrom('deadletter')) { closePanels(); return; }
65
+ const { json } = await api('/deadletter');
66
+ output(json?.items?.length ? json.items : 'no dead letters', 'deadletter');
67
+ });
68
+
69
+ // ---- lifecycle ----
70
+ // None of these can be taken back by clicking again, so the button only opens
71
+ // the matching warning in the markup; the second click is the one that acts.
72
+ // Retire also wants the handle typed, because a misclick cannot produce it.
73
+
74
+ const LIFECYCLE = {
75
+ 'rotate-key': { path: '/rotate-key', title: 'Rotate the signing key', done: (r) => (r.changed ? 'rotated and republished' : 'no change — the key was already fresh') },
76
+ retire: { path: '/retire', title: 'Retire this identity', go: 'Retire it', danger: true, done: (r) => `retired ${r.deletedAt}: Delete delivered to ${r.inboxes} inbox(es)` },
77
+ move: { path: '/move', title: 'Transfer this account away', go: 'Transfer it', focus: 'move-target',
78
+ done: (r) => `transferred to ${r.target}: Move delivered to ${r.inboxes} inbox(es), unfollowed ${r.unfollowed}/${r.following}` },
79
+ };
80
+ let pending = null;
81
+
82
+ const closeConfirm = () => closePanels();
83
+
84
+ for (const btn of document.querySelectorAll('[data-confirm]')) {
85
+ btn.addEventListener('click', () => {
86
+ const what = btn.dataset.confirm;
87
+ // The same button again closes its question rather than re-asking it.
88
+ if (pending === what && solWindow.openId() === 'confirm-form') { closePanels(); return; }
89
+ closePanels(); // including whatever else was open
90
+ pending = what;
91
+ const spec = LIFECYCLE[what];
92
+ $(`warn-${what}`).hidden = false;
93
+ solWindow.show('confirm-form', spec.title);
94
+ $('confirm-go').className = spec.danger ? 'danger' : 'primary';
95
+ $('confirm-go').textContent = spec.go || 'Confirm';
96
+ if (spec.focus) $(spec.focus).focus();
97
+ else if (what === 'retire') $('confirm-handle').focus();
98
+ });
99
+ }
100
+
101
+ // Offered from inside the retire warning: someone reading it has already said
102
+ // what they want ("not this account, here, any more") and these are the two
103
+ // answers that are not destruction. Switching panels rather than closing means
104
+ // they do not have to go and find the button themselves.
105
+ const openConfirm = (what) => {
106
+ closePanels(); // or `show` toggles: same panel, different warning
107
+ document.querySelector(`[data-confirm="${what}"]`).click();
108
+ };
109
+ // Park lives on the status control now; from inside the retire warning it is
110
+ // still one click — close the question and park.
111
+ $('go-park').addEventListener('click', () => { closePanels(); setStatus(true); });
112
+ $('go-move').addEventListener('click', () => openConfirm('move'));
113
+
114
+ $('confirm-cancel').addEventListener('click', () => { closeConfirm(); say('nothing changed'); });
115
+
116
+ $('confirm-form').addEventListener('submit', async (ev) => {
117
+ ev.preventDefault();
118
+ if (!pending) return;
119
+ const what = pending;
120
+ const body = what === 'retire' ? { confirm: $('confirm-handle').value.trim() }
121
+ : what === 'move' ? { target: $('move-target').value.trim(), confirm: $('confirm-handle-move').value.trim() }
122
+ // Only where the field is shown (the browser build — see index.html).
123
+ // On the Node agent the row stays hidden and nothing is sent, which is
124
+ // what that agent expects.
125
+ : what === 'rotate-key' && !$('rotate-pw-row').hidden
126
+ ? { password: $('rotate-password').value }
127
+ : {};
128
+ if (what === 'move' && !body.target) { say('name the account to transfer to', 'err'); return; }
129
+ $('confirm-go').disabled = true;
130
+ say(`${what} — this talks to the pod and to other servers, so it takes a moment`);
131
+ const r = await write(LIFECYCLE[what].path, body, what);
132
+ $('confirm-go').disabled = false;
133
+ if (!r) return; // write() already said why
134
+ closeConfirm();
135
+ say(LIFECYCLE[what].done(r));
136
+ load(); // mode, and whether it is retired, both changed
137
+ });
138
+
139
+ // ---- inbox ----
140
+ // Only appears when there is enough waiting to be worth a decision. The agent
141
+ // drains oldest-first regardless; this exists to let the owner say "do not
142
+ // bother with that fortnight", which is not a call an agent should make on
143
+ // someone's mail by itself.
144
+ const INBOX_PROMPT_AT = 500;
145
+ let dismissed = false;
146
+
147
+ async function renderInbox() {
148
+ if (dismissed) return;
149
+ const { json: st } = await api('/status');
150
+ const box = st?.inbox;
151
+ const panel = $('pane-inbox');
152
+ if (!box || box.count < INBOX_PROMPT_AT) { panel.hidden = true; return; }
153
+ const mb = box.bytes >= 1048576
154
+ ? `${(box.bytes / 1048576).toFixed(1)} MB` : `${Math.round(box.bytes / 1024)} kB`;
155
+ const since = box.oldest ? new Date(box.oldest).toLocaleDateString() : 'unknown';
156
+ $('inbox-summary').textContent =
157
+ `${box.count.toLocaleString()} deliveries waiting (${mb}), the oldest from ${since}.`;
158
+ // One request each to read and delete, and the agent holds itself to 60 a
159
+ // minute, so the honest number is minutes not seconds.
160
+ $('inbox-warn').hidden = box.count < 2000;
161
+ $('inbox-warn').textContent = box.count >= 2000
162
+ ? `At 60 requests a minute this is roughly ${Math.ceil(box.count * 2 / 60)} minutes of `
163
+ + 'draining if you keep everything. Discarding the old content is much quicker.'
164
+ : '';
165
+ panel.hidden = false;
166
+ }
167
+
168
+ // The gateway, as a facts row under software: attach through a multi-user
169
+ // front with this agent's own credential, detach back to the pod inbox. The
170
+ // forms live in the floating window, like every other disclosure.
@@ -33231,6 +33231,26 @@ async function kvPut(key, val) {
33231
33231
  }
33232
33232
 
33233
33233
  // web/app/keys-browser.mjs
33234
+ var pemToDer = (pem) => Uint8Array.from(
33235
+ atob(pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "")),
33236
+ (c) => c.charCodeAt(0)
33237
+ );
33238
+ async function importSigningKey(keysRecord) {
33239
+ const rsaPrivate = await crypto.subtle.importKey(
33240
+ "pkcs8",
33241
+ pemToDer(keysRecord.rsa.privatePem),
33242
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
33243
+ false,
33244
+ ["sign"]
33245
+ );
33246
+ return { rsaPrivate, rsaPublicPem: keysRecord.rsa.publicPem, edPrivate: null, edPublicMultibase: null };
33247
+ }
33248
+ async function cacheOpenedKeys(actorUrl, keysRecord) {
33249
+ const keys = await importSigningKey(keysRecord);
33250
+ await kvPut(keyCacheKey(actorUrl), { rsaPrivate: keys.rsaPrivate, rsaPublicPem: keys.rsaPublicPem }).catch(() => {
33251
+ });
33252
+ return keys;
33253
+ }
33234
33254
  var keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
33235
33255
 
33236
33256
  // web/app/signup.mjs
@@ -33321,8 +33341,7 @@ async function signUp(answers, { onStep = () => {
33321
33341
  } catch (e) {
33322
33342
  throw new Error(`could not store the signing key on the pod (${e.message}). The credential is for ${credential.webId} \u2014 that WebID must own ${pod} and its ${AP_ROOT} must be writable by it.`);
33323
33343
  }
33324
- await kvPut(keyCacheKey(actorUrl), keys).catch(() => {
33325
- });
33344
+ await cacheOpenedKeys(actorUrl, keys);
33326
33345
  prog.keys = keys;
33327
33346
  prog.keysStored = true;
33328
33347
  keysStep.ok();
@@ -33668,7 +33687,7 @@ window.fedipodUnlock = async (password) => {
33668
33687
  if (!isKeyEnvelope(doc)) throw new Error("this account's key is not locked \u2014 nothing to unlock");
33669
33688
  const rec = await unwrapKeys(doc, password);
33670
33689
  const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
33671
- await kvPut(keyCacheKey(actorUrl), rec);
33690
+ await cacheOpenedKeys(actorUrl, rec);
33672
33691
  await bootWorker();
33673
33692
  };
33674
33693
  window.fedipodSignup = async ({ onStep, ...answers }) => {