fedipod 1.27.1 → 1.27.3

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.
@@ -15,7 +15,7 @@ import * as podPrivate from '../../pod/private.mjs';
15
15
  // still gets everything, so a missed call site degrades to the old cost rather
16
16
  // than silently publishing nothing.
17
17
  export const ALL_COLLECTIONS = { followers: true, following: true, outbox: true, acls: true,
18
- pending: true, blocked: true };
18
+ pending: true, blocked: true, featured: true };
19
19
 
20
20
  // Publish the collections a change actually TOUCHED.
21
21
  //
@@ -152,6 +152,11 @@ export async function publishCollections(publisher, which = ALL_COLLECTIONS) {
152
152
  }
153
153
  if (which.pending) await publisher.publishPending();
154
154
  if (which.blocked) await publisher.publishBlocked();
155
+ // The pinned posts, written even when there are none. The actor names this
156
+ // collection, and a server showing the profile reads it whether or not
157
+ // anything was ever pinned; with no document there the pod refused every
158
+ // reader, a refusal nothing caches, and every server asked again.
159
+ if (which.featured) await publisher.publishFeatured();
155
160
  if (which.outbox) {
156
161
  const outbox = publisher.store.read('outbox.json', []);
157
162
  // Reconcile reads every published page, which is the expensive part of a
@@ -321,6 +321,7 @@ export class Publisher {
321
321
  ['notes', urls.notes],
322
322
  ['followers', urls.followers],
323
323
  ['following', urls.following],
324
+ ['featured', urls.featured],
324
325
  ['outbox', urls.outbox],
325
326
  ];
326
327
  const unreachable = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fedipod",
3
- "version": "1.27.1",
3
+ "version": "1.27.3",
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",
package/run-agent.mjs CHANGED
@@ -497,6 +497,18 @@ export class Agent {
497
497
  return true;
498
498
  }
499
499
 
500
+ // The featured collection was once written only on the first pin, so an
501
+ // account from before then has none, and every server that shows the
502
+ // profile is refused and asks again. One authenticated GET; the empty
503
+ // collection goes up when the pod lacks it. True when it had to.
504
+ async ensureFeaturedPublished() {
505
+ const doc = await this.remote.getJson(this.urls.featured);
506
+ if (doc?.id) return false;
507
+ this.log('featured collection missing from the pod — publishing it');
508
+ await this.publisher.publishFeatured();
509
+ return true;
510
+ }
511
+
500
512
  // Read-only mode: refresh the state cache periodically, and take over the
501
513
  // moment the active agent's lease frees.
502
514
  startViewer() {
@@ -588,6 +600,8 @@ export class Agent {
588
600
  if (repair) {
589
601
  this.ensureActorPublished()
590
602
  .catch(e => this.log(`actor check failed: ${e.message}`));
603
+ this.ensureFeaturedPublished()
604
+ .catch(e => this.log(`featured check failed: ${e.message}`));
591
605
  // The human page, rewritten only when what it shows has changed: one
592
606
  // local digest compare, and a write the first time after an upgrade.
593
607
  this.publisher.publishProfilePage()
@@ -45643,7 +45643,8 @@ var ALL_COLLECTIONS = {
45643
45643
  outbox: true,
45644
45644
  acls: true,
45645
45645
  pending: true,
45646
- blocked: true
45646
+ blocked: true,
45647
+ featured: true
45647
45648
  };
45648
45649
  async function publishOutbox(publisher, outbox, { acls = false, force = false } = {}) {
45649
45650
  const { urls } = publisher;
@@ -45742,6 +45743,7 @@ async function publishCollections(publisher, which = ALL_COLLECTIONS) {
45742
45743
  }
45743
45744
  if (which.pending) await publisher.publishPending();
45744
45745
  if (which.blocked) await publisher.publishBlocked();
45746
+ if (which.featured) await publisher.publishFeatured();
45745
45747
  if (which.outbox) {
45746
45748
  const outbox = publisher.store.read("outbox.json", []);
45747
45749
  const known2 = publisher.store.read("published.json", {}).outboxIndex;
@@ -57345,6 +57347,7 @@ var Publisher = class {
57345
57347
  ["notes", urls.notes],
57346
57348
  ["followers", urls.followers],
57347
57349
  ["following", urls.following],
57350
+ ["featured", urls.featured],
57348
57351
  ["outbox", urls.outbox]
57349
57352
  ];
57350
57353
  const unreachable = [];
@@ -65852,6 +65855,39 @@ function checkPassword(rec, password) {
65852
65855
  }
65853
65856
  var escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
65854
65857
  var parseRedirects = (v) => (Array.isArray(v) ? v : String(v || "").split(/\s+/)).map((s) => s.trim()).filter(Boolean);
65858
+ function sendPodSigninPage(res, client, webId, mount = "") {
65859
+ let asking = "";
65860
+ if (client && (client.name || client.redirect)) {
65861
+ let where = "";
65862
+ try {
65863
+ where = client.redirect ? new URL(client.redirect).host : "";
65864
+ } catch {
65865
+ }
65866
+ const who = client.name ? escapeHtml(client.name) : where ? escapeHtml(where) : "A client";
65867
+ asking = `<p><strong>${who}</strong> is asking to use your account${where ? `, and will be sent back to <code>${escapeHtml(where)}</code>` : ""}.</p>`;
65868
+ }
65869
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" });
65870
+ res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
65871
+ <title>Sign in with your pod</title>
65872
+ <style>:root{color-scheme:light dark;--heading:#1a4f8a;--btn:#3d6b35;--line:#9a9a9a}
65873
+ body{font:20px Arial,Helvetica,sans-serif;max-width:28rem;margin:12vh auto;padding:0 1rem}
65874
+ h1{color:var(--heading);font-size:1.5rem}
65875
+ code{word-break:break-all;background:rgba(120,120,120,.12);padding:.05em .35em;border-radius:4px}
65876
+ button{font:inherit;padding:.6rem 1.1rem;background:var(--btn);color:#fff;border:none;border-radius:9px;cursor:pointer}
65877
+ button[hidden]{display:none}
65878
+ #signin-status{min-height:1.5em;padding:.6rem .8rem;border:1px solid var(--line);border-radius:8px;margin-top:1rem}
65879
+ @media (prefers-color-scheme:dark){:root{--heading:#7fb3e8;--btn:#5a8a4f}}</style></head><body>
65880
+ <main>
65881
+ <h1>Sign in with your pod</h1>
65882
+ ${asking}
65883
+ <p>This account belongs to <a id="webid" href="${escapeHtml(webId)}">${escapeHtml(webId)}</a>. Sign in at that pod to allow it.</p>
65884
+ <p><button type="button" id="signin" hidden>Sign in with your pod</button></p>
65885
+ <p id="signin-status" role="status" aria-live="polite"></p>
65886
+ </main>
65887
+ <script type="module" src="${escapeHtml(mount)}/oauth/session/signin.mjs"><\/script>
65888
+ </body></html>`);
65889
+ return true;
65890
+ }
65855
65891
  function sendLoginForm(res, params, error2 = "", client = null, status2 = null, headers = {}) {
65856
65892
  const hidden = [...params.entries()].filter(([k]) => k !== "password").map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`).join("\n");
65857
65893
  let asking = "<p>Enter the agent password to authorize this client.</p>";
@@ -66120,6 +66156,16 @@ async function handle(api, ctx) {
66120
66156
  params = new URLSearchParams(body);
66121
66157
  }
66122
66158
  const redirect = params.get("redirect_uri") || "";
66159
+ let proved = false;
66160
+ if (req.method === "POST" && req.headers.authorization && req.headers.dpop && api.podAuth) {
66161
+ const proof = await api.podAuth(req, pathname);
66162
+ if (!proof.ok) return send(proof.status || 401, { error: proof.error });
66163
+ proved = true;
66164
+ }
66165
+ const ownerWebId = api.ownerWebId?.() || null;
66166
+ if (req.method === "GET" && !params.get("client_id") && params.get("code") && params.get("state") && ownerWebId) {
66167
+ return sendPodSigninPage(res, null, ownerWebId, api.mount || "");
66168
+ }
66123
66169
  const app = api.findApp(params.get("client_id") || "");
66124
66170
  const doc = app ? null : await api.resolveClientDocument(params.get("client_id") || "");
66125
66171
  const external = !!app || !!doc;
@@ -66127,13 +66173,14 @@ async function handle(api, ctx) {
66127
66173
  api.log(`authorize refused: cross-site navigation to the mint from ${req.headers.referer || "nowhere"}`);
66128
66174
  return send(403, { error: "a cross-site navigation may not authorize a client" });
66129
66175
  }
66130
- if (external && !api.store.getConfig()?.uiPassword && !api.redirectAllowed(redirect)) {
66176
+ const client = { name: app?.name || doc?.name || null, redirect, scope: params.get("scope") || "read" };
66177
+ if (external && !proved && !api.store.getConfig()?.uiPassword && !api.redirectAllowed(redirect)) {
66178
+ if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "");
66131
66179
  api.log(`authorize refused: no UI password, and "${redirect}" is not an address of this agent`);
66132
66180
  return send(403, {
66133
66181
  error: "this client asks to be sent somewhere other than this agent, and no password is set to approve that with. Run `fedipod passwd` and try again."
66134
66182
  });
66135
66183
  }
66136
- const client = { name: app?.name || doc?.name || null, redirect, scope: params.get("scope") || "read" };
66137
66184
  if (app) {
66138
66185
  if (!app.redirectUris.includes(redirect)) {
66139
66186
  api.log(`authorize refused: redirect_uri "${redirect}" not registered for ${app.clientId}`);
@@ -66152,7 +66199,7 @@ async function handle(api, ctx) {
66152
66199
  api.log(`authorize refused: redirect_uri "${redirect}" is not this agent`);
66153
66200
  return send(400, { error: "redirect_uri must be an address of this agent" });
66154
66201
  }
66155
- if (req.method === "POST") {
66202
+ if (req.method === "POST" && !proved) {
66156
66203
  if (api.rateLimited()) {
66157
66204
  api.log("authorize rate limited");
66158
66205
  return sendLoginForm(
@@ -66167,12 +66214,13 @@ async function handle(api, ctx) {
66167
66214
  if (!pw || !checkPassword(pw, body.password || "")) {
66168
66215
  return sendLoginForm(res, params, "wrong password \u2014 try again", client);
66169
66216
  }
66170
- } else if (pw) {
66217
+ } else if (pw && !proved) {
66171
66218
  return sendLoginForm(res, params, "", client);
66172
- } else if (api.allowed && !api.allowed.isLocalRequest(req)) {
66219
+ } else if (!proved && api.allowed && !api.allowed.isLocalRequest(req) && !api.throughDoor?.(req)) {
66220
+ if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "");
66173
66221
  api.log(`authorize refused: no UI password, and "${req.headers.host}" is not this machine`);
66174
66222
  return send(403, {
66175
- error: "this agent answers on an address outside this machine and has no password set \u2014 " + (api.embedded ? `POST {"password":"\u2026"} to the owner door's /config with its door secret before logging in` : "run `fedipod passwd` before logging in over that address")
66223
+ error: api.embedded ? "Open your account's management page first, then sign in from there." : "Signing in from another machine needs a password. On the machine that runs your account, run: fedipod passwd"
66176
66224
  });
66177
66225
  }
66178
66226
  const code = external ? api.mintCode({
@@ -66186,6 +66234,7 @@ async function handle(api, ctx) {
66186
66234
  const target = new URL(redirect);
66187
66235
  target.searchParams.set("code", code);
66188
66236
  if (params.get("state")) target.searchParams.set("state", params.get("state"));
66237
+ if (proved) return send(200, { redirect: target.href });
66189
66238
  res.writeHead(302, { location: target.href });
66190
66239
  res.end();
66191
66240
  return true;
@@ -68091,6 +68140,7 @@ var MastoApi = class _MastoApi {
68091
68140
  allowed = null,
68092
68141
  scheme = null,
68093
68142
  embedded = false,
68143
+ throughDoor = null,
68094
68144
  mount = "",
68095
68145
  streaming = true,
68096
68146
  webPush = true,
@@ -68101,6 +68151,7 @@ var MastoApi = class _MastoApi {
68101
68151
  this.embedded = embedded;
68102
68152
  this.log = log2;
68103
68153
  this.allowed = allowed;
68154
+ this.throughDoor = throughDoor;
68104
68155
  this.scheme = scheme;
68105
68156
  this.streaming = streaming;
68106
68157
  this.webPush = webPush;