fedipod-server 0.15.0 → 0.17.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 (43) hide show
  1. package/README.md +37 -15
  2. package/dist/claims.d.ts +10 -11
  3. package/dist/claims.js +19 -17
  4. package/dist/directory.d.ts +11 -3
  5. package/dist/directory.js +28 -14
  6. package/dist/handler.d.ts +30 -7
  7. package/dist/handler.js +133 -42
  8. package/dist/handler.jsonld +14 -6
  9. package/dist/streaming-handler.js +7 -2
  10. package/lib/client/masto/index.mjs +7 -1
  11. package/lib/client/masto/timelines.mjs +1 -1
  12. package/lib/client/oidc-auth.mjs +5 -3
  13. package/lib/core/publisher/index.mjs +1 -1
  14. package/lib/core/wire.mjs +4 -1
  15. package/lib/device/admin/routes/gateway.mjs +1 -1
  16. package/lib/device/admin/routes/lifecycle.mjs +1 -1
  17. package/lib/device/admin/routes/setup.mjs +20 -1
  18. package/lib/device/admin/surface.mjs +25 -17
  19. package/lib/device/cli/commands/setup.mjs +53 -21
  20. package/lib/device/setup.mjs +105 -25
  21. package/lib/gateway/front-core.mjs +23 -11
  22. package/lib/pod/root.mjs +12 -1
  23. package/lib/server/embed.mjs +23 -6
  24. package/package.json +2 -1
  25. package/run-agent.mjs +1 -1
  26. package/vendor/gate.cjs +5 -2
  27. package/web/admin/setup/index.html +25 -2
  28. package/web/admin/setup/setup.js +30 -1
  29. package/web/app/README.md +2 -2
  30. package/web/app/admin-facade.mjs +1 -1
  31. package/web/app/agent.mjs +3 -3
  32. package/web/app/boot.mjs +2 -2
  33. package/web/app/dist/boot.js +43 -31
  34. package/web/app/dist/boot.js.map +3 -3
  35. package/web/app/dist/sw.js +4 -4
  36. package/web/app/dist/sw.js.map +2 -2
  37. package/web/app/index.html +1 -1
  38. package/web/app/signup.mjs +5 -3
  39. package/web/app/site/admin/setup/index.html +25 -2
  40. package/web/app/site/admin/setup/setup.js +30 -1
  41. package/web/app/site/boot.js +1 -1
  42. package/web/app/site/index.html +1 -1
  43. package/web/app/site/sw.js +3 -3
@@ -93,12 +93,18 @@ function answers() {
93
93
  const f = $('form').elements;
94
94
  const kind = f.kind.value;
95
95
  const mode = state.resumable ? 'existing' : f.mode.value;
96
+ const issuer = mode === 'new'
97
+ ? (f.issuerNew.value || (f.issuerOther ? f.issuerOther.value : '')).trim()
98
+ : f.issuer.value.trim();
96
99
  const a = {
97
100
  kind,
98
101
  mode,
99
102
  handle: f.handle.value.trim(),
100
- issuer: (mode === 'new' ? f.issuerNew.value : f.issuer.value).trim(),
103
+ issuer,
101
104
  email: f.email.value.trim(),
105
+ // Where the address lives; a group cannot front (yet), so it stays 'pod'.
106
+ shape: (kind === 'group') ? 'pod' : (f.shape ? f.shape.value : 'pod'),
107
+ gatewayOrigin: (f.gatewayOrigin ? f.gatewayOrigin.value.trim() : '') || 'https://fedipod.net',
102
108
  };
103
109
  if (mode === 'new') a.podName = f.podName.value.trim() || a.handle;
104
110
  else a.pod = f.pod.value.trim();
@@ -128,6 +134,9 @@ function onEdit() {
128
134
  $('row-pod').hidden = mode === 'new';
129
135
  $('row-issuer-new').hidden = mode !== 'new';
130
136
  $('row-issuer-existing').hidden = mode === 'new';
137
+ const f = $('form').elements;
138
+ if ($('row-issuer-other')) $('row-issuer-other').hidden = !(mode === 'new' && f.issuerNew.value === '');
139
+ if ($('row-gateway')) $('row-gateway').hidden = !(f.shape && f.shape.value === 'front');
131
140
  clearTimeout(editTimer);
132
141
  editTimer = setTimeout(preview, 150);
133
142
  }
@@ -140,6 +149,26 @@ async function preview() {
140
149
  const { json } = await postJson('/setup/check', a);
141
150
  if (!json) return;
142
151
  $('preview').textContent = json.address || '…';
152
+ // The address-shape choice: hidden for a group (a group cannot front yet),
153
+ // locked to the gateway for a pod on a suffix-based host, an open choice
154
+ // for a pod at its own host.
155
+ const f = $('form').elements;
156
+ const shapeFs = $('fs-shape');
157
+ if (shapeFs) {
158
+ const isGroup = a.kind === 'group';
159
+ shapeFs.hidden = isGroup || (a.mode === 'existing' && !a.pod);
160
+ const forced = !!json.forced;
161
+ for (const r of f.shape) r.disabled = forced;
162
+ if (forced) { for (const r of f.shape) r.checked = r.value === 'front'; }
163
+ if ($('row-gateway')) $('row-gateway').hidden = !(f.shape.value === 'front');
164
+ const note = $('shape-note');
165
+ if (note) {
166
+ note.hidden = !forced;
167
+ note.textContent = forced
168
+ ? 'Your pod is on a suffix-based host, so its address lives at the gateway. Your posts, key and data stay on your pod.'
169
+ : '';
170
+ }
171
+ }
143
172
  const notes = $('preview-notes');
144
173
  for (const w of json.warnings || []) {
145
174
  const p = document.createElement('p');
package/web/app/README.md CHANGED
@@ -72,6 +72,6 @@ See the `MastoApi` options in `agent.mjs`.
72
72
  | **Streaming** | a service worker answers fetches, not sockets. No streaming URL is advertised, so clients poll. |
73
73
  | **Web push** | `shims/web-push.mjs` is a no-op. `vapid` is omitted, and a client that subscribes anyway gets a 422 rather than a subscription nothing will push to. |
74
74
  | **Scheduled posts** | nothing runs between now and the scheduled time. A `scheduled_at` is refused with a 422 that says so — accepting one was silent loss. |
75
- | **Groups** | sign-up makes personal identities only (`signup.mjs`), and the moderation surface is not here. Joining a group works; hosting one needs the installed agent. See `groups.md`. |
76
- | **Changing where the address lives** | the shape is chosen at sign-up (`signup.mjs`): on the pod, `@you@yourpod` with the gateway as a mail door, or at the gateway, `@you@front` — and a pod on a path of a shared host is always fronted. `admin-facade.mjs` refuses changing it afterwards, because a rename needs a restart a browser does not have. |
75
+ | **Groups** | sign-up makes personal identities only (`signup.mjs`), and the moderation surface is not here. Joining a group works; hosting one needs the DeviceAgent. See `groups.md`. |
76
+ | **Changing where the address lives** | the shape is chosen at sign-up (`signup.mjs`): on the pod, `@you@yourpod` with the gateway as a mail door, or at the gateway, `@you@front` — and a pod on a suffix-based host is always fronted. `admin-facade.mjs` refuses changing it afterwards, because a rename needs a restart a browser does not have. |
77
77
  | **Moving the private half** | `/state-move` is about filesystem paths and `credential.json`. A browser has neither; its private half is always on the pod. |
@@ -317,7 +317,7 @@ export class AdminFacade {
317
317
  const aliases = [...(cfg.aliases || [])];
318
318
  if (body.add) {
319
319
  if (!webfingerHost(urls.base) && !cfg.gateway?.frontActor) {
320
- return json(400, { error: 'this pod is a path on a shared host, so other servers could never resolve it as a Move target' });
320
+ return json(400, { error: 'this pod is a suffix-based host, so other servers could never resolve it as a Move target' });
321
321
  }
322
322
  const id = await this.resolveActor(body.add);
323
323
  if (!id) return json(400, { error: `could not fetch the old account (${body.add}) — enter its URL or @user@host, and it must answer` });
package/web/app/agent.mjs CHANGED
@@ -160,7 +160,7 @@ export class BrowserAgent {
160
160
  if (oidc) {
161
161
  session = { fetch: (u, i) => oidc.fetch(u, i) };
162
162
  webId = oidc.webId;
163
- remotePod = podBaseOfWebId(webId); // the pod, which on a shared host is a path
163
+ remotePod = podBaseOfWebId(webId); // the pod, which on a suffix-based host is a path
164
164
  } else {
165
165
  const dpop = await makeDpopSession(credential);
166
166
  session = { fetch: (u, i) => dpop.fetch(u, i) };
@@ -189,7 +189,7 @@ export class BrowserAgent {
189
189
  // builds its own urls from `config.root` (publisher.mjs), and a config
190
190
  // without one falls to the Node default — so an account set up elsewhere
191
191
  // and signed into here would keep its state under `fedipod/` while every
192
- // document it published landed under `activitypods-js/`. One root, decided
192
+ // document it published landed under a different root. One root, decided
193
193
  // once, carried by the config everything downstream reads.
194
194
  this.store.setConfig({ ...(this.store.getConfig() || {}), ...cfg, root });
195
195
  config = this.store.getConfig();
@@ -224,7 +224,7 @@ export class BrowserAgent {
224
224
  this.publisher = new Publisher({
225
225
  config: this.store.getConfig(), remote: this.remote, store: this.store,
226
226
  deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem, assertionKey: null, log: this.log,
227
- // Who a post names, resolved — the same lookup the installed agent
227
+ // Who a post names, resolved — the same lookup the DeviceAgent
228
228
  // gives its publisher. Without it no mention from the browser ever
229
229
  // resolved: a direct message went to nobody, a mention notified no one.
230
230
  resolveMention: (h) => resolveHandle(this, h),
package/web/app/boot.mjs CHANGED
@@ -70,7 +70,7 @@ window.fedipodUnlock = async (password) => {
70
70
  if (!session) throw new Error('Sign in first.');
71
71
  // The config on the pod says where this account's state lives; the key sits
72
72
  // beside it. Both are read with the session, as the owner.
73
- const podFromWebId = podBaseOfWebId(session.webId); // a path on a shared host, or its own host
73
+ const podFromWebId = podBaseOfWebId(session.webId); // a suffix-based host, or its own host
74
74
  const state = `${podFromWebId}${AP_ROOT}ap-state/`;
75
75
  // Through the transport rather than the bare session: this is a pod read
76
76
  // like any other, and going round it skipped the retry ladder that exists
@@ -288,7 +288,7 @@ if (typeof document !== 'undefined') (async () => {
288
288
  // brought by its address, which may be its own host or a path on a shared one.
289
289
  const podHostOf = () => { const sub = f().podName.value.trim().toLowerCase(); const ph = providerHost(); return (sub && ph) ? `${sub}.${ph}` : ''; };
290
290
  const podUrl = () => { let v = f().pod.value.trim(); if (!v) return ''; if (!/^https?:\/\//i.test(v)) v = 'https://' + v; if (!v.endsWith('/')) v += '/'; try { return new URL(v).href; } catch { return ''; } };
291
- // A pod on a path of a shared host cannot answer WebFinger, so its address
291
+ // A pod on a suffix-based host cannot answer WebFinger, so its address
292
292
  // lives at this site; a pod at its own host root gets the choice.
293
293
  const isPathPod = (u) => { try { return new URL(u).pathname !== '/'; } catch { return false; } };
294
294
  // Where the chosen provider puts new pods, asked of the provider itself
@@ -33208,6 +33208,47 @@ async function provisionKey(pod, { stateUrl, keysUrl, envelope }) {
33208
33208
  await pod.putJson(keysUrl, envelope, "application/json");
33209
33209
  }
33210
33210
 
33211
+ // lib/pod/root.mjs
33212
+ var OWNER_LOOKUP_MS = 5e3;
33213
+ var PUBLIC_DOC_MAX_BYTES = 1024 * 1024;
33214
+ async function podLayout(fetchImpl, providerOrigin, { timeoutMs = OWNER_LOOKUP_MS } = {}) {
33215
+ let origin;
33216
+ try {
33217
+ origin = new URL(providerOrigin).origin;
33218
+ } catch {
33219
+ return null;
33220
+ }
33221
+ let res;
33222
+ try {
33223
+ res = await fetchImpl(
33224
+ `${origin}/.well-known/solid`,
33225
+ { headers: { accept: "text/turtle" }, signal: AbortSignal.timeout(timeoutMs) }
33226
+ );
33227
+ } catch {
33228
+ return null;
33229
+ }
33230
+ if (res.status === 501) return "host";
33231
+ if (res.status !== 200) return null;
33232
+ let body = "";
33233
+ try {
33234
+ body = await readCapped(res, 64 * 1024);
33235
+ } catch {
33236
+ return null;
33237
+ }
33238
+ return /ns\/pim\/space#Storage|pim:Storage/u.test(body) ? "path" : null;
33239
+ }
33240
+ async function resourceExists(fetchImpl, url, { timeoutMs = OWNER_LOOKUP_MS } = {}) {
33241
+ try {
33242
+ const res = await fetchImpl(
33243
+ url,
33244
+ { headers: { accept: "application/activity+json" }, signal: AbortSignal.timeout(timeoutMs) }
33245
+ );
33246
+ return !!res && res.status === 200;
33247
+ } catch {
33248
+ return false;
33249
+ }
33250
+ }
33251
+
33211
33252
  // web/app/idb-kv.mjs
33212
33253
  var DB = "fedipod-accounts";
33213
33254
  function open() {
@@ -33310,6 +33351,7 @@ async function signUp(answers, { onStep = () => {
33310
33351
  acct.running("checking your pod");
33311
33352
  const head = await fetch(brought, { method: "HEAD" }).catch(() => null);
33312
33353
  if (!head || head.status >= 400) throw new Error(`the pod at ${brought} did not answer (HTTP ${head?.status || "no response"})`);
33354
+ if (await resourceExists(fetch, actorUrlFor(brought))) throw new Error("The pod already hosts a FediPod account. If you want a second account, put it on a different pod.");
33313
33355
  prog.pod = brought;
33314
33356
  acct.skip("using the pod you brought");
33315
33357
  }
@@ -33321,7 +33363,7 @@ async function signUp(answers, { onStep = () => {
33321
33363
  const pathPod = new URL(pod).pathname !== "/";
33322
33364
  const fronted = pathPod || wantsFront;
33323
33365
  if (fronted && !frontOrigin) {
33324
- throw new Error(`${pod} is a path on a shared host, so its address must live at a gateway, and this page has none.`);
33366
+ throw new Error(`${pod} is a suffix-based host, so its address must live at a gateway, and this page has none.`);
33325
33367
  }
33326
33368
  if (pathPod && !wantsFront) await assertFrontNameFree(frontOrigin, handle);
33327
33369
  const actorUrl = actorUrlFor(pod);
@@ -33450,36 +33492,6 @@ function podBaseOfWebId(webId) {
33450
33492
  return `${u.origin}${dir.endsWith("/") ? dir : dir + "/"}`;
33451
33493
  }
33452
33494
 
33453
- // lib/pod/root.mjs
33454
- var OWNER_LOOKUP_MS = 5e3;
33455
- var PUBLIC_DOC_MAX_BYTES = 1024 * 1024;
33456
- async function podLayout(fetchImpl, providerOrigin, { timeoutMs = OWNER_LOOKUP_MS } = {}) {
33457
- let origin;
33458
- try {
33459
- origin = new URL(providerOrigin).origin;
33460
- } catch {
33461
- return null;
33462
- }
33463
- let res;
33464
- try {
33465
- res = await fetchImpl(
33466
- `${origin}/.well-known/solid`,
33467
- { headers: { accept: "text/turtle" }, signal: AbortSignal.timeout(timeoutMs) }
33468
- );
33469
- } catch {
33470
- return null;
33471
- }
33472
- if (res.status === 501) return "host";
33473
- if (res.status !== 200) return null;
33474
- let body = "";
33475
- try {
33476
- body = await readCapped(res, 64 * 1024);
33477
- } catch {
33478
- return null;
33479
- }
33480
- return /ns\/pim\/space#Storage|pim:Storage/u.test(body) ? "path" : null;
33481
- }
33482
-
33483
33495
  // web/app/oidc-session.mjs
33484
33496
  var DB2 = "fedipod-oidc";
33485
33497
  var STORE = "session";