fedipod-server 0.28.1 → 0.29.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 (61) hide show
  1. package/README.md +27 -28
  2. package/config/server.json +22 -0
  3. package/dist/claims.js +3 -2
  4. package/dist/directory.d.ts +2 -0
  5. package/dist/handler.d.ts +4 -1
  6. package/dist/handler.js +32 -3
  7. package/dist/handler.jsonld +4 -0
  8. package/dist/store-pod.js +37 -0
  9. package/lib/client/c2s.mjs +97 -43
  10. package/lib/client/masto/accounts.mjs +1 -0
  11. package/lib/core/deliver.mjs +17 -1
  12. package/lib/core/intake/activities.mjs +18 -2
  13. package/lib/core/intake/group.mjs +3 -1
  14. package/lib/core/intake/index.mjs +33 -2
  15. package/lib/core/intake/notes.mjs +12 -5
  16. package/lib/core/place.mjs +82 -0
  17. package/lib/core/publisher/collections.mjs +37 -2
  18. package/lib/core/publisher/index.mjs +23 -1
  19. package/lib/core/publisher/notes.mjs +73 -7
  20. package/lib/core/publisher/own.mjs +143 -0
  21. package/lib/core/publisher/questions.mjs +5 -3
  22. package/lib/core/social.mjs +62 -32
  23. package/lib/core/store.mjs +11 -1
  24. package/lib/core/wire.mjs +51 -15
  25. package/lib/device/admin/routes/lifecycle.mjs +1 -1
  26. package/lib/device/admin/routes/setup.mjs +17 -1
  27. package/lib/device/cli/commands/setup.mjs +50 -8
  28. package/lib/device/cli/context.mjs +1 -1
  29. package/lib/device/migrate.mjs +1 -1
  30. package/lib/device/setup.mjs +45 -8
  31. package/lib/gateway/front-core.mjs +54 -58
  32. package/lib/gateway/gateway-core.mjs +87 -8
  33. package/lib/gateway/relay-extras.mjs +89 -0
  34. package/lib/gateway/token-claims.mjs +16 -0
  35. package/lib/pod/location.mjs +52 -0
  36. package/lib/pod/notes.mjs +2 -4
  37. package/lib/pod/transport.mjs +171 -22
  38. package/lib/pod/type-index.mjs +101 -0
  39. package/lib/pod/urls.mjs +6 -0
  40. package/lib/server/embed.mjs +7 -6
  41. package/lib/session/fedi-account.mjs +19 -10
  42. package/lib/session/package.json +1 -1
  43. package/package.json +1 -1
  44. package/run-agent.mjs +2 -0
  45. package/web/admin/actors.js +2 -0
  46. package/web/admin/index.html +9 -0
  47. package/web/admin/record.js +4 -1
  48. package/web/admin/setup/index.html +17 -1
  49. package/web/admin/setup/setup.js +18 -5
  50. package/web/app/README.md +1 -1
  51. package/web/app/admin-facade.mjs +1 -1
  52. package/web/app/agent.mjs +11 -2
  53. package/web/app/boot.mjs +72 -32
  54. package/web/app/dist/boot.js +430 -86
  55. package/web/app/dist/boot.js.map +4 -4
  56. package/web/app/dist/sw.js +3294 -2648
  57. package/web/app/dist/sw.js.map +4 -4
  58. package/web/app/index.html +16 -0
  59. package/web/app/signup.mjs +65 -26
  60. package/web/front/run.html +7 -1
  61. package/web/front/run.js +30 -4
package/web/app/agent.mjs CHANGED
@@ -6,7 +6,8 @@
6
6
  // the relay (deliver-relay). Everything between — wire, the store, the
7
7
  // publisher, intake, the Mastodon facade — is lib/, unchanged.
8
8
  import { kvGet, kvPut } from './idb-kv.mjs';
9
- import { apUrls } from '../../lib/core/wire.mjs';
9
+ import { apUrls, DEFAULT_ROOT } from '../../lib/core/wire.mjs';
10
+ import { findAccount } from '../../lib/core/place.mjs';
10
11
  import * as containers from '../../lib/pod/containers.mjs';
11
12
  import * as podState from '../../lib/pod/state.mjs';
12
13
  import { PodStore } from '../../lib/core/store.mjs';
@@ -180,6 +181,8 @@ export class BrowserAgent {
180
181
  // Own posts the outbox names and the timeline index lacks come back
181
182
  // here, before anything acts on the index.
182
183
  await this.publisher.healStatuses().catch((e) => this.log(`healing the timeline index: ${e.message}`));
184
+ // Likes made before the liked list existed become its first entries.
185
+ try { this.publisher.backfillLiked(); } catch (e) { this.log(`liked list: ${e.message}`); }
183
186
  await this.publisher.publishProfilePage().catch((e) => this.log(`profile page: ${e.message}`));
184
187
  // And start delivering again, since demote() stopped it. startQueue() is
185
188
  // idempotent, so a goActive() that was already active costs nothing.
@@ -289,8 +292,14 @@ export class BrowserAgent {
289
292
  // The pod session's own fetch, for the gateway APIs that take it as
290
293
  // proof of the pod (attach, move) — the transport below is for the pod.
291
294
  this.sessionFetch = session.fetch;
292
- const root = (config && config.root) || 'fedipod/';
293
295
  this.remote = new BrowserRemotePod(session, { webId, log: this.log });
296
+ // Where the account lives: handed in on sign-up, else what its owner's type
297
+ // index records, else `fedipod/` for an account older than the choice.
298
+ const root = (config && config.root)
299
+ || (await findAccount(this.remote, remotePod,
300
+ (r) => podState.readConfig(this.remote, { state: `${remotePod}${r}ap-state/` }),
301
+ (c) => (c.kind || 'person') === 'person').catch(() => null))?.root
302
+ || DEFAULT_ROOT;
294
303
  // Pod-native for now: the state store below is read with these, and only
295
304
  // the config it holds says whether this identity is fronted.
296
305
  this.urls = apUrls(remotePod, root);
package/web/app/boot.mjs CHANGED
@@ -14,7 +14,8 @@
14
14
  // fedipodMoveIn(answers) brings the address here.
15
15
  // Returning: fedipodSignin({ address }) — redirect to the pod's login.
16
16
  // On every load: fedipodOnLoad() — finish a redirect, or restore, then boot.
17
- import { signUp, moveIn, readAccount, handleProblem, AP_ROOT } from './signup.mjs';
17
+ import { signUp, moveIn, readAccount, handleProblem } from './signup.mjs';
18
+ import { podRootPath, chosenRoot } from '../../lib/core/place.mjs';
18
19
  import * as podActor from '../../lib/pod/actor.mjs';
19
20
  import * as podState from '../../lib/pod/state.mjs';
20
21
  import { podBaseOfWebId } from '../../lib/pod/urls.mjs';
@@ -76,16 +77,15 @@ async function bootWorker({ reset = false } = {}) {
76
77
  async function readAccountState() {
77
78
  const session = await getSession();
78
79
  if (!session) throw new Error('Sign in first.');
79
- const podFromWebId = podBaseOfWebId(session.webId); // a suffix-based host, or its own host
80
- const state = `${podFromWebId}${AP_ROOT}ap-state/`;
80
+ // Wherever its owner put it: the type index says, or it is an older account
81
+ // at `fedipod/` (signup.mjs readAccount).
82
+ const here = await readAccount(session);
83
+ if (!here) throw new Error(`could not find a FediPod account on ${podBaseOfWebId(session.webId)}`);
81
84
  const remote = new BrowserRemotePod(session, { webId: session.webId, role: 'signup', log: () => {} });
82
- const urls = { state };
83
- const [cfg, doc] = await Promise.all([
84
- podState.readConfig(remote, urls), podState.readKeys(remote, urls),
85
- ]);
86
- if (!cfg) throw new Error(`could not read this account's config under ${state}`);
87
- const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
88
- return { remote, urls, cfg, doc, actorUrl };
85
+ const urls = { state: `${here.pod}${here.root}ap-state/` };
86
+ const doc = await podState.readKeys(remote, urls);
87
+ const actorUrl = `${here.config.remotePod || here.pod}${here.config.root || here.root}ap/actor`;
88
+ return { remote, urls, cfg: here.config, doc, actorUrl };
89
89
  }
90
90
 
91
91
  window.fedipodUnlock = async (password) => {
@@ -161,22 +161,33 @@ export function parseAddress(input) {
161
161
  if (!handle || !host || !host.includes('.')) return null;
162
162
  return { handle, host };
163
163
  }
164
- async function issuerForPod(pod) {
164
+ async function issuerForActor(actorUrl) {
165
165
  // The pod's actor says where a client signs in (oauthAuthorizationEndpoint's
166
- // origin); failing that, the account provider is the pod host's parent
167
- // domain for a subdomain pod, and the host itself for a pod on a path.
166
+ // origin). Failing that: a host that answers OpenID discovery itself is the
167
+ // provider (a suffixed pod), and otherwise the provider is
168
+ // the pod host's parent domain (a subdomain pod).
168
169
  try {
169
- const authz = await podActor.readIssuer(`${pod}${AP_ROOT}ap/actor`);
170
+ const authz = await podActor.readIssuer(actorUrl);
170
171
  if (authz) return new URL(authz).origin;
171
172
  } catch { /* fall through */ }
172
- const u = new URL(pod);
173
- if (u.pathname !== '/') return u.origin;
173
+ const u = new URL(actorUrl);
174
+ const own = await fetch(`${u.origin}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } }).catch(() => null);
175
+ if (own?.ok) return u.origin;
174
176
  const parent = u.host.split('.').slice(1).join('.');
175
177
  return `https://${parent || u.host}`;
176
178
  }
179
+ // An address on a pod names its actor through the pod host's own WebFinger;
180
+ // an older pod that does not answer is taken to keep it at `fedipod/`.
181
+ async function actorForPodAddress(handle, host) {
182
+ const res = await fetch(`https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${host}`)}`,
183
+ { headers: { accept: 'application/jrd+json, application/json' } }).catch(() => null);
184
+ const doc = res?.ok ? await res.json().catch(() => ({})) : {};
185
+ const self = (doc.links || []).find((l) => l.rel === 'self' && /activity\+json|ld\+json/u.test(l.type || ''));
186
+ return self?.href || `https://${host}/fedipod/ap/actor`;
187
+ }
177
188
  // An address at this site names a fronted identity. Its documents live on a
178
189
  // pod this site's WebFinger names as an alias — the pod's own actor id.
179
- async function podForFrontedAddress(handle) {
190
+ async function actorForFrontedAddress(handle) {
180
191
  const res = await fetch(`/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${location.host}`)}`,
181
192
  { headers: { accept: 'application/jrd+json, application/json' } }).catch(() => null);
182
193
  if (res?.status === 410) throw new Error(`@${handle}@${location.host} is closed: nothing on the pod behind it was touched, but the address is gone for good.`);
@@ -184,19 +195,17 @@ async function podForFrontedAddress(handle) {
184
195
  const doc = await res.json().catch(() => ({}));
185
196
  const podActorId = (doc.aliases || []).find((a) => /\/ap\/actor$/u.test(String(a)));
186
197
  if (!podActorId) throw new Error(`@${handle}@${location.host} lives here but names no pod to sign in to`);
187
- const tail = `${AP_ROOT}ap/actor`;
188
- if (!podActorId.endsWith(tail)) throw new Error(`the pod actor ${podActorId} is not where a FediPod pod keeps one`);
189
- return podActorId.slice(0, -tail.length);
198
+ return podActorId;
190
199
  }
191
200
  window.fedipodSignin = async ({ address }) => {
192
201
  const parsed = parseAddress(address);
193
202
  if (!parsed) throw new Error('Enter your address as @you@yourpod (for example @alice@alice.solidcommunity.net).');
194
203
  const bad = handleProblem(parsed.handle);
195
204
  if (bad) throw new Error(bad);
196
- const pod = parsed.host === location.host.toLowerCase()
197
- ? await podForFrontedAddress(parsed.handle)
198
- : `https://${parsed.host}/`;
199
- const issuer = await issuerForPod(pod);
205
+ const actor = parsed.host === location.host.toLowerCase()
206
+ ? await actorForFrontedAddress(parsed.handle)
207
+ : await actorForPodAddress(parsed.handle, parsed.host);
208
+ const issuer = await issuerForActor(actor);
200
209
  const { authorizationUrl } = await beginLogin({ issuer, redirectUri: REDIRECT });
201
210
  location.href = authorizationUrl;
202
211
  };
@@ -445,6 +454,10 @@ if (typeof document !== 'undefined') (async () => {
445
454
  $('loading').hidden = true; $('hero').hidden = true; $('landing').hidden = true; $('running').hidden = true;
446
455
  $('brand').hidden = false; $('pane-form').hidden = false;
447
456
  $('signed-pod').textContent = signedPod;
457
+ // The container that holds `fedipod/`: the pod's own root unless they say
458
+ // otherwise. A move keeps the place the account already has.
459
+ $('container').placeholder = podRootPath(signedPod);
460
+ $('container-field').hidden = !!moveFrom;
448
461
  $('movein-note').hidden = !moveFrom;
449
462
  if (moveFrom) {
450
463
  $('movein-from').textContent = moveFrom.address;
@@ -484,7 +497,13 @@ if (typeof document !== 'undefined') (async () => {
484
497
  const pathPod = () => { try { return new URL(signedPod).pathname !== '/'; } catch { return false; } };
485
498
  // A move between gateways is an address at a gateway: no choice either.
486
499
  const shape = () => ((pathPod() || moveFrom) ? 'front' : f().shape.value);
487
- const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape() });
500
+ const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape(), container: f().container.value.trim() });
501
+ // Where the data will be, in full, as the person types.
502
+ const previewPlace = () => {
503
+ if (!signedPod) return;
504
+ const r = chosenRoot(signedPod, f().container.value);
505
+ $('place').textContent = r.problem ? '…' : signedPod + r.root;
506
+ };
488
507
  const previewAddr = () => {
489
508
  const handle = f().handle.value.trim().toLowerCase();
490
509
  let host = '';
@@ -507,7 +526,7 @@ if (typeof document !== 'undefined') (async () => {
507
526
  $('register-link').href = url || '#';
508
527
  $('register-link').textContent = providerHost() ? `Create your pod at ${providerHost()}` : 'Create your pod at your provider';
509
528
  };
510
- for (const el of $('form').elements) for (const evt of ['input', 'change']) el.addEventListener(evt, () => { applyMode(); applyShape(); previewAddr(); });
529
+ for (const el of $('form').elements) for (const evt of ['input', 'change']) el.addEventListener(evt, () => { applyMode(); applyShape(); previewAddr(); previewPlace(); });
511
530
  applyMode();
512
531
 
513
532
  // Step machine: one screen at a time, each gated by its own validation.
@@ -516,7 +535,7 @@ if (typeof document !== 'undefined') (async () => {
516
535
  const goStep = (n) => {
517
536
  STEP_IDS.forEach((id, i) => { $(id).hidden = i !== n - 1; });
518
537
  $('err-1').textContent = ''; $('form-error').textContent = '';
519
- if (n === 2) { applyShape(); previewAddr(); }
538
+ if (n === 2) { applyShape(); previewAddr(); previewPlace(); $('index-ask').hidden = true; }
520
539
  if (FOCUS[n]) $(FOCUS[n]).focus();
521
540
  };
522
541
  const validateStep1 = () => {
@@ -526,6 +545,10 @@ if (typeof document !== 'undefined') (async () => {
526
545
  const validateStep2 = () => {
527
546
  const hp = window.fedipodHandleProblem(f().handle.value.trim().toLowerCase());
528
547
  if (hp) return `Fediverse handle: ${hp}`;
548
+ if (!moveFrom) {
549
+ const where = chosenRoot(signedPod, f().container.value).problem;
550
+ if (where) return `Where to store it: ${where}`;
551
+ }
529
552
  return null;
530
553
  };
531
554
  $('create').addEventListener('click', showForm);
@@ -549,12 +572,12 @@ if (typeof document !== 'undefined') (async () => {
549
572
  $('run-retry').addEventListener('click', () => backToForm($('run-error').textContent));
550
573
  $('run-back').addEventListener('click', () => { showLanding(); goStep(1); });
551
574
 
552
- const LABELS = { pod: 'Checking your pod', keys: 'Making your signing key', gateway: 'Connecting your mail door' };
575
+ const LABELS = { pod: 'Checking your pod', keys: 'Making your signing key', gateway: 'Connecting your mail door',
576
+ place: 'Recording where your account lives' };
553
577
  const MOVE_LABELS = { pod: 'Reading your account on your pod', gateway: 'Taking your address here', keys: 'Moving your key and account record' };
554
- $('form').addEventListener('submit', async (e) => {
555
- e.preventDefault(); $('form-error').textContent = '';
556
- const e2 = validateStep2(); if (e2) { $('form-error').textContent = e2; goStep(2); return; }
557
- const a = answers();
578
+ // Run the setup. `createIndex` is the person's yes to a new public type index.
579
+ const run = async (createIndex = false) => {
580
+ const a = { ...answers(), ...(createIndex ? { createIndex: true } : {}) };
558
581
  $('pane-form').hidden = true; $('running').hidden = false;
559
582
  $('running-title').textContent = 'Setting up…'; $('run-error').textContent = ''; $('run-actions').hidden = true;
560
583
  const steps = $('steps'); steps.textContent = ''; const mark = {};
@@ -566,10 +589,27 @@ if (typeof document !== 'undefined') (async () => {
566
589
  location.href = '/admin/client/';
567
590
  }
568
591
  catch (err) {
592
+ // No type index: nothing has been written. Ask, on the form.
593
+ if (err?.code === 'needs-index') {
594
+ backToForm();
595
+ $('index-ask').hidden = false;
596
+ $('index-yes').focus();
597
+ return;
598
+ }
569
599
  $('running-title').textContent = 'Setup did not finish';
570
600
  $('run-error').textContent = err.message || String(err);
571
601
  $('run-actions').hidden = false; // the way out of the failure screen
572
602
  }
603
+ };
604
+ $('form').addEventListener('submit', async (e) => {
605
+ e.preventDefault(); $('form-error').textContent = '';
606
+ const e2 = validateStep2(); if (e2) { $('form-error').textContent = e2; goStep(2); return; }
607
+ await run();
608
+ });
609
+ $('index-yes').addEventListener('click', () => { $('index-ask').hidden = true; run(true); });
610
+ $('index-no').addEventListener('click', () => {
611
+ $('index-ask').hidden = true;
612
+ $('form-error').textContent = 'Sign-up stopped. Nothing was written to your pod.';
573
613
  });
574
614
 
575
615
  if (pendingIdentity) await showIdentity();