fedipod-server 0.14.0 → 0.15.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.
@@ -13,6 +13,7 @@
13
13
  --heading: #123f74; --sub: #565656; --line: #b0b0b0; --accent: #12467e;
14
14
  --btn: #3a5f43; --btn-hover: #30503a; --btn-text: #ffffff;
15
15
  --field-bg: #ececec; --field-text: #141414; --field-border: #6f6f6f;
16
+ --select-bg: #dce7f3; --select-border: #12467e;
16
17
  --err: #b00020; --warn: #8a4b00; --focus: #12467e;
17
18
  }
18
19
  @media (prefers-color-scheme: dark) {
@@ -20,6 +21,7 @@
20
21
  --bg: #191919; --fg: #f3f3f3;
21
22
  --heading: #8fc2f2; --sub: #d2d2d2; --line: #6d6d6d; --accent: #8fc2f2;
22
23
  --field-bg: #474747; --field-text: #f5f5f5; --field-border: #9aa0a6;
24
+ --select-bg: #2f4a66; --select-border: #8fc2f2;
23
25
  --btn: #4a7a54; --btn-hover: #3f6a49;
24
26
  --err: #ff9b9b; --warn: #ffb366; --focus: #8fc2f2;
25
27
  }
@@ -42,6 +44,10 @@ input[type=text], input[type=email], input[type=url], input[type=password], sele
42
44
  font: inherit; font-size: 1rem; width: 100%; padding: .55rem; box-sizing: border-box;
43
45
  border: 1px solid var(--field-border); border-radius: .3rem;
44
46
  background: var(--field-bg); color: var(--field-text); }
47
+ /* A list to pick from looks different from a box to type in: its own tint,
48
+ the accent border, and the same on its options. */
49
+ select { background: var(--select-bg); border-color: var(--select-border); }
50
+ select option { background: var(--select-bg); color: var(--field-text); }
45
51
  ::placeholder { color: var(--sub); opacity: 1; }
46
52
  .choice { display: block; margin: .4rem 0; font-weight: normal; }
47
53
  .choice input { margin-right: .5rem; width: auto; }
@@ -88,7 +94,7 @@ a { color: var(--accent); }
88
94
  <section id="landing" hidden>
89
95
  <h2>Already have a FediPod account?</h2>
90
96
  <p class="row">
91
- <input type="text" id="signin-address" autocapitalize="off" spellcheck="false" placeholder="@me@mypod"
97
+ <input type="text" id="signin-address" autocomplete="username" autocapitalize="off" spellcheck="false" placeholder="@me@mypod"
92
98
  aria-label="Your FediPod address, in the form @you@yourpod">
93
99
  <button type="button" class="primary" id="signin">sign in</button>
94
100
  </p>
@@ -136,18 +142,42 @@ a { color: var(--accent); }
136
142
  </div>
137
143
 
138
144
  <label for="provider">Pod provider</label>
139
- <input type="url" id="provider" name="provider" autocomplete="url" placeholder="https://solidcommunity.net">
145
+ <!-- The providers listed at https://solidproject.org/get_a_pod that run Community
146
+ Solid Server, whose account API is what "A new pod" uses. Checked 2026-09-14. -->
147
+ <select id="provider" name="provider">
148
+ <option value="https://solidcommunity.net">solidcommunity.net</option>
149
+ <option value="https://privatedatapod.com">privatedatapod.com</option>
150
+ <option value="https://solid.redpencil.io">redpencil.io</option>
151
+ <option value="https://pods.solidcommunity.au">solidcommunity.au</option>
152
+ <option value="https://solidweb.me">solidweb.me</option>
153
+ <option value="https://teamid.live">teamid.live</option>
154
+ <option value="">Other…</option>
155
+ </select>
156
+ <div id="provider-other-field" hidden>
157
+ <label for="providerOther">Provider address</label>
158
+ <input type="url" id="providerOther" name="providerOther" autocomplete="url" placeholder="https://pods.example.org">
159
+ </div>
160
+
161
+ <div id="podname-field">
162
+ <label for="podName">Pod username</label>
163
+ <input type="text" id="podName" name="podName" autocomplete="off" aria-describedby="podname-hint">
164
+ <p class="hint" id="podname-hint">Depending on your provider's setup, your pod will be at
165
+ either https://USERNAME.podprovider or https://podprovider/USERNAME.</p>
166
+ </div>
140
167
 
141
- <label for="podName">Pod username/subdomain</label>
142
- <input type="text" id="podName" name="podName" autocomplete="off" aria-describedby="podname-hint">
143
- <p class="hint" id="podname-hint">The subdomain of your pod. On solidcommunity.net, alice
144
- becomes the pod https://alice.solidcommunity.net.</p>
168
+ <div id="pod-field" hidden>
169
+ <label for="pod">Pod address</label>
170
+ <input type="url" id="pod" name="pod" autocomplete="url" aria-describedby="pod-hint"
171
+ placeholder="https://alice.solidcommunity.net/ or https://server.example/alice/">
172
+ <p class="hint" id="pod-hint">The address of your pod: its own host, or a path on a
173
+ shared host.</p>
174
+ </div>
145
175
 
146
- <label for="email">Pod email</label>
176
+ <label for="email">Email (used as pod id)</label>
147
177
  <input type="email" id="email" name="email" autocomplete="email" placeholder="you@example.org">
148
178
 
149
179
  <label for="password">Pod password</label>
150
- <input type="password" id="password" name="password" autocomplete="current-password"
180
+ <input type="password" id="password" name="password" autocomplete="new-password"
151
181
  aria-describedby="password-hint">
152
182
  <p class="hint" id="password-hint">Your password is sent once, to make your account and to
153
183
  lock your signing key. It is never stored. The credential used to set things up is
@@ -172,6 +202,14 @@ a { color: var(--accent); }
172
202
  <p class="hint" id="handle-hint">The name in your Fediverse address. This is permanent.
173
203
  Letters, digits, and hyphens only.</p>
174
204
 
205
+ <div role="radiogroup" aria-labelledby="shape-label" id="shape-group">
206
+ <span id="shape-label" class="grouplabel">Where your address lives</span>
207
+ <label class="choice"><input type="radio" name="shape" value="pod" checked> On your pod</label>
208
+ <label class="choice"><input type="radio" name="shape" value="front"> At this site</label>
209
+ </div>
210
+ <p class="hint" id="shape-hint" hidden>Your pod provider uses suffixes, so your only option
211
+ is to use an account on a gateway like this one.</p>
212
+
175
213
  <span id="addr-label" class="grouplabel">Your Fediverse address</span>
176
214
  <p class="address" id="preview" role="status" aria-live="polite" aria-labelledby="addr-label">@…@…</p>
177
215
  </fieldset>
@@ -141,6 +141,13 @@ function apUrls(remotePod, root, { publicBase = null } = {}) {
141
141
  }
142
142
  return urls;
143
143
  }
144
+ function podBaseOfWebId(webId) {
145
+ const u = new URL(webId);
146
+ u.hash = "";
147
+ u.search = "";
148
+ const dir = u.pathname.replace(/profile\/card$/u, "").replace(/[^/]*$/u, "");
149
+ return `${u.origin}${dir.endsWith("/") ? dir : dir + "/"}`;
150
+ }
144
151
  function webfingerHost(podUrl) {
145
152
  const u = new URL(podUrl);
146
153
  return u.pathname === "/" ? u.host : null;
@@ -9651,9 +9658,10 @@ function hostMeta(base) {
9651
9658
  </XRD>
9652
9659
  `;
9653
9660
  }
9654
- function jrd({ handle: handle7, host, actor }) {
9661
+ function jrd({ handle: handle7, host, actor, aliases = [] }) {
9655
9662
  return {
9656
9663
  subject: `acct:${handle7}@${host}`,
9664
+ ...aliases.length ? { aliases } : {},
9657
9665
  links: [{ rel: "self", type: "application/activity+json", href: actor }]
9658
9666
  };
9659
9667
  }
@@ -57393,7 +57401,7 @@ async function onUndo(intake, activity, actor, { trusted = false } = {}) {
57393
57401
  await intake.republish({ followers: true });
57394
57402
  intake.log(`unfollowed by ${actor}`);
57395
57403
  }
57396
- async function onCreate(intake, activity, actor) {
57404
+ async function onCreate(intake, activity, actor, { trusted = false } = {}) {
57397
57405
  const objectId = typeof activity.object === "string" ? activity.object : activity.object?.id;
57398
57406
  if (!objectId) return "Create without object id";
57399
57407
  if (intake.store.isBlocked(objectId)) return `blocked domain (${objectId})`;
@@ -57402,7 +57410,8 @@ async function onCreate(intake, activity, actor) {
57402
57410
  if (!intake.concernsUs(envelope, actor)) return `not addressed to us (${objectId})`;
57403
57411
  const ingested = intake.store.getStatuses().some((x) => x.noteId === objectId && (x.kind === "timeline" || x.kind === "mention"));
57404
57412
  if (!ingested) {
57405
- const rejected = await intake.ingestNote(objectId, actor);
57413
+ const inline = trusted && typeof activity.object === "object" && activity.object?.id === objectId ? activity.object : null;
57414
+ const rejected = await intake.ingestNote(objectId, actor, { inline });
57406
57415
  if (rejected) return rejected;
57407
57416
  }
57408
57417
  if (intake.config.kind === "group") await intake.amplify(objectId, { activity });
@@ -57616,8 +57625,8 @@ function referencesOurObject(intake, activity) {
57616
57625
  if (refs.some((r) => r.startsWith(intake.urls.notes))) return true;
57617
57626
  return intake.config.kind === "group" && refs.some((r) => intake.store.getStatuses().some((s) => s.noteId === r));
57618
57627
  }
57619
- async function ingestNote(intake, objectId, actor, { via } = {}) {
57620
- const note = await intake.fetchAP(objectId);
57628
+ async function ingestNote(intake, objectId, actor, { via, inline = null } = {}) {
57629
+ const note = inline || await intake.fetchAP(objectId);
57621
57630
  if (!note) return `object fetch failed (${objectId})`;
57622
57631
  if (note.id !== objectId || !isContentType(note.type)) return `object not verifiable content (${objectId}, ${note.type})`;
57623
57632
  const { attachmentsOf: attachmentsOf3, titledContent: titledContent2 } = await Promise.resolve().then(() => (init_wire(), wire_exports));
@@ -63143,7 +63152,7 @@ var Intake = class {
63143
63152
  case "Undo":
63144
63153
  return this.onUndo(activity, actor, { trusted });
63145
63154
  case "Create":
63146
- return this.onCreate(activity, actor);
63155
+ return this.onCreate(activity, actor, { trusted });
63147
63156
  case "Accept":
63148
63157
  return this.onAccept(activity, actor, { trusted });
63149
63158
  case "Like":
@@ -64122,7 +64131,7 @@ __export(social_exports, {
64122
64131
  });
64123
64132
  init_node_crypto();
64124
64133
  init_wire();
64125
- async function lookupWebFinger(acct) {
64134
+ async function lookupWebFinger(acct, { fetchImpl = null } = {}) {
64126
64135
  const clean = String(acct || "").replace(/^acct:/, "");
64127
64136
  const at = clean.lastIndexOf("@");
64128
64137
  if (at < 1) return null;
@@ -64130,9 +64139,8 @@ async function lookupWebFinger(acct) {
64130
64139
  if (!host || /[/\\?#]/.test(host)) return null;
64131
64140
  const { safeFetch: safeFetch2, readCapped: readCapped3 } = await Promise.resolve().then(() => (init_safefetch(), safefetch_exports));
64132
64141
  const url = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${clean}`)}`;
64133
- const res = await safeFetch2(url, {
64134
- headers: { accept: "application/jrd+json, application/json" }
64135
- }).catch(() => null);
64142
+ const headers = { accept: "application/jrd+json, application/json" };
64143
+ const res = await (fetchImpl ? fetchImpl(url, { headers }) : safeFetch2(url, { headers })).catch(() => null);
64136
64144
  if (!res || res.status >= 400) return null;
64137
64145
  let jrd2;
64138
64146
  try {
@@ -64168,13 +64176,15 @@ async function resolveHandle(agent2, handle7) {
64168
64176
  const clean = String(handle7 || "").replace(/^@/, "");
64169
64177
  if (!/^[^@]+@[^@]+$/.test(clean)) throw new Error("handle must look like user@host");
64170
64178
  if (agent2.store.isBlocked("https://" + clean.split("@")[1] + "/")) throw new Error("domain is blocked");
64171
- const jrd2 = await lookupWebFinger("acct:" + clean);
64179
+ const remote = agent2.intake?.deliverer?.signedFetch ? (u, i) => agent2.intake.deliverer.signedFetch(u, i) : null;
64180
+ const lookup2 = (a) => lookupWebFinger(a, { fetchImpl: remote });
64181
+ const jrd2 = await lookup2("acct:" + clean);
64172
64182
  const self2 = selfLink(jrd2);
64173
64183
  if (!self2?.href) throw new Error(`webfinger found no actor for ${clean}`);
64174
64184
  const doc = await agent2.intake.fetchAP(self2.href);
64175
64185
  if (!doc?.id) throw new Error(`actor document unusable for ${clean}`);
64176
64186
  if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
64177
- await confirmDelegation(clean.slice(clean.lastIndexOf("@") + 1), doc);
64187
+ await confirmDelegation(clean.slice(clean.lastIndexOf("@") + 1), doc, lookup2);
64178
64188
  await cacheCounts(agent2, doc);
64179
64189
  return doc;
64180
64190
  }
@@ -66229,8 +66239,11 @@ var MastoApi = class _MastoApi {
66229
66239
  });
66230
66240
  return this._push;
66231
66241
  }
66242
+ // The host in the owner's own address: the gateway's when the identity is
66243
+ // fronted (its documents still live on the pod, but its name does not).
66232
66244
  get host() {
66233
- return this.urls ? new URL(this.urls.base).host : "unconfigured.invalid";
66245
+ if (!this.urls) return "unconfigured.invalid";
66246
+ return new URL(this.urls.publicHome || this.urls.base).host;
66234
66247
  }
66235
66248
  // Where the live feed is, as the CLIENT must address it: this agent's own
66236
66249
  // origin, taken from the request, not the pod's host. An instance document
@@ -66844,9 +66857,10 @@ var PodTransport = class {
66844
66857
  return serialize(doc, g, url, "text/turtle");
66845
66858
  }
66846
66859
  async setAcl(targetUrl, publicModes, opts = {}) {
66847
- const url = await this.aclUrlFor(targetUrl);
66860
+ const podTarget = this.toPod ? this.toPod(targetUrl) : targetUrl;
66861
+ const url = await this.aclUrlFor(podTarget);
66848
66862
  if (!await this.aclWritable(url)) return null;
66849
- return this.put(url, this.aclDoc(targetUrl, publicModes, { ...opts, aclUrl: url }), "text/turtle");
66863
+ return this.put(url, this.aclDoc(podTarget, publicModes, { ...opts, aclUrl: url }), "text/turtle");
66850
66864
  }
66851
66865
  // Child documents of an LDP container (URLs under it, excluding aux docs).
66852
66866
  // Revalidated: the inbox is polled every couple of minutes and is usually
@@ -67164,14 +67178,16 @@ async function cacheOpenedKeys(actorUrl, keysRecord) {
67164
67178
  return keys;
67165
67179
  }
67166
67180
  var keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
67181
+ var podActorOf = (urls) => urls.toPod ? urls.toPod(urls.actor) : urls.actor;
67167
67182
  async function loadKeysFromPod(remote, urls) {
67168
- const cached = await kvGet(keyCacheKey(urls.actor)).catch(() => null);
67183
+ const podActor = podActorOf(urls);
67184
+ const cached = await kvGet(keyCacheKey(podActor)).catch(() => null);
67169
67185
  if (isOpenedKey(cached)) return fromCache(cached);
67170
- if (cached?.rsa?.privatePem) return cacheOpenedKeys(urls.actor, cached);
67186
+ if (cached?.rsa?.privatePem) return cacheOpenedKeys(podActor, cached);
67171
67187
  const doc = await readWrappedKeys(remote, urls);
67172
67188
  if (isKeyEnvelope(doc)) throw new KeyPasswordNeeded();
67173
67189
  if (!doc || !doc.rsa) throw new Error("no signing key on the pod \u2014 sign up did not finish");
67174
- return cacheOpenedKeys(urls.actor, doc);
67190
+ return cacheOpenedKeys(podActor, doc);
67175
67191
  }
67176
67192
 
67177
67193
  // lib/core/deliver.mjs
@@ -69600,6 +69616,7 @@ var AcctFeed = class {
69600
69616
  };
69601
69617
 
69602
69618
  // web/app/agent.mjs
69619
+ init_urls();
69603
69620
  var originAuthorities = (host) => ({
69604
69621
  set: /* @__PURE__ */ new Set([String(host || "").toLowerCase()]),
69605
69622
  has(authority) {
@@ -69724,7 +69741,7 @@ var BrowserAgent = class _BrowserAgent {
69724
69741
  if (oidc) {
69725
69742
  session = { fetch: (u, i) => oidc.fetch(u, i) };
69726
69743
  webId = oidc.webId;
69727
- remotePod = new URL(webId).origin + "/";
69744
+ remotePod = podBaseOfWebId(webId);
69728
69745
  } else {
69729
69746
  const dpop = await makeDpopSession(credential);
69730
69747
  session = { fetch: (u, i) => dpop.fetch(u, i) };
@@ -69742,8 +69759,11 @@ var BrowserAgent = class _BrowserAgent {
69742
69759
  const cfg = config || this.store.getConfig();
69743
69760
  if (!cfg) throw new Error("no account config on this pod \u2014 sign up first");
69744
69761
  this.store.setConfig({ ...this.store.getConfig() || {}, ...cfg, root });
69745
- const keys = keysRecord ? await importSigningKey(keysRecord) : await loadKeysFromPod(this.remote, this.urls);
69746
69762
  config = this.store.getConfig();
69763
+ const publicBase = config.gateway?.frontActor ? config.gateway.frontActor.replace(/ap\/actor\/?$/, "") : null;
69764
+ this.urls = apUrls2(remotePod, root, { publicBase });
69765
+ if (this.urls.toPod) this.remote.setUrlMap(this.urls.toPod);
69766
+ const keys = keysRecord ? await importSigningKey(keysRecord) : await loadKeysFromPod(this.remote, this.urls);
69747
69767
  this.deliverer = new RelayDeliverer({
69748
69768
  passive: true,
69749
69769
  store: this.store,
@@ -69764,7 +69784,11 @@ var BrowserAgent = class _BrowserAgent {
69764
69784
  deliverer: this.deliverer,
69765
69785
  publicKeyPem: keys.rsaPublicPem,
69766
69786
  assertionKey: null,
69767
- log: this.log
69787
+ log: this.log,
69788
+ // Who a post names, resolved — the same lookup the installed agent
69789
+ // gives its publisher. Without it no mention from the browser ever
69790
+ // resolved: a direct message went to nobody, a mention notified no one.
69791
+ resolveMention: (h) => resolveHandle(this, h)
69768
69792
  });
69769
69793
  this.atproto = new BrowserAtproto({ store: this.store, actorId: this.urls.actor, log: this.log });
69770
69794
  this.publisher.atproto = this.atproto;
@@ -69864,7 +69888,7 @@ var BrowserAgent = class _BrowserAgent {
69864
69888
  const rec = await generateKeys();
69865
69889
  rec.mintedFor = this.urls.actor;
69866
69890
  await writeWrappedKeys(this.remote, this.urls, await wrapKeys(rec, password));
69867
- const keys = await cacheOpenedKeys(this.urls.actor, rec);
69891
+ const keys = await cacheOpenedKeys(podActorOf(this.urls), rec);
69868
69892
  this.publisher.publicKeyPem = keys.rsaPublicPem;
69869
69893
  this.deliverer.rsaPrivate = keys.rsaPrivate;
69870
69894
  await this.publisher.publishProfile();