fedipod 1.40.0 → 1.43.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 (70) hide show
  1. package/README.md +5 -0
  2. package/architecture.md +1 -3
  3. package/architecture.svg +93 -85
  4. package/gateway.md +7 -2
  5. package/lib/client/c2s.mjs +16 -5
  6. package/lib/client/masto/oauth.mjs +1 -1
  7. package/lib/connections/vault.mjs +1 -1
  8. package/lib/core/agent.mjs +720 -0
  9. package/lib/core/deliver.mjs +11 -1
  10. package/lib/core/gateway-move.mjs +58 -0
  11. package/lib/core/intake/group.mjs +10 -11
  12. package/lib/core/keys.mjs +1 -1
  13. package/lib/core/lease.mjs +4 -3
  14. package/lib/core/publisher/index.mjs +4 -6
  15. package/lib/core/publisher/notes.mjs +73 -4
  16. package/lib/core/remote.mjs +54 -0
  17. package/lib/core/shapes/shapes-text.mjs +11 -3
  18. package/lib/core/wire.mjs +4 -1
  19. package/lib/device/admin/index.mjs +4 -11
  20. package/lib/device/admin/routes/setup.mjs +60 -5
  21. package/lib/device/admin/server.mjs +18 -5
  22. package/lib/device/cli/commands/account.mjs +1 -1
  23. package/lib/device/cli/commands/run.mjs +2 -2
  24. package/lib/device/cli/commands/service.mjs +1 -1
  25. package/lib/device/cli/commands/setup.mjs +1 -1
  26. package/lib/device/cli/commands/state.mjs +1 -1
  27. package/lib/device/cli/context.mjs +1 -1
  28. package/lib/{gateway → device}/directory.mjs +1 -1
  29. package/lib/device/gateway-move.mjs +3 -58
  30. package/lib/device/home.mjs +3 -50
  31. package/lib/{client → device}/localapi.mjs +2 -2
  32. package/lib/device/remote.mjs +3 -54
  33. package/lib/gateway/copy.mjs +12 -3
  34. package/lib/gateway/front-core.mjs +16 -17
  35. package/lib/gateway/front-pages.mjs +31 -0
  36. package/lib/gateway/gateway-core.mjs +1 -1
  37. package/lib/gateway/held-mail.mjs +3 -0
  38. package/lib/gateway/keeper-session.mjs +10 -1
  39. package/lib/gateway/keeper.mjs +3 -0
  40. package/lib/gateway/masto-gateway.mjs +61 -12
  41. package/lib/gateway/outbox-door.mjs +31 -0
  42. package/lib/gateway/state-api.mjs +39 -7
  43. package/lib/server/embed.mjs +4 -4
  44. package/lib/shared/files.mjs +49 -0
  45. package/lib/shared/ua.mjs +3 -0
  46. package/lib/surface/index.mjs +7 -0
  47. package/lib/{device/admin → surface}/origins.mjs +1 -1
  48. package/lib/{device/admin → surface}/routes/gateway.mjs +7 -11
  49. package/lib/{device/admin → surface}/routes/lifecycle.mjs +3 -3
  50. package/lib/{device/admin → surface}/routes/owner.mjs +4 -60
  51. package/lib/{device/admin → surface}/routes/social.mjs +1 -1
  52. package/lib/{device/admin → surface}/static.mjs +6 -1
  53. package/lib/{device/admin → surface}/surface.mjs +19 -16
  54. package/package.json +23 -5
  55. package/run-agent.mjs +19 -720
  56. package/scripts/build-app.mjs +4 -0
  57. package/scripts/check-netlify-bundles.mjs +88 -0
  58. package/scripts/check-pod-calls.mjs +42 -0
  59. package/scripts/vendor-sanitize.mjs +27 -0
  60. package/vendor/idp-grant.cjs +11 -5
  61. package/vendor/sanitize-html.cjs +11456 -0
  62. package/web/app/agent.mjs +4 -2
  63. package/web/app/copy-mode.mjs +27 -3
  64. package/web/app/dist/boot.js.map +1 -1
  65. package/web/app/dist/sw.js +110 -24
  66. package/web/app/dist/sw.js.map +4 -4
  67. package/web/app-signin/app-signin.mjs +2 -2
  68. package/web/app-signin/index.html +13 -5
  69. package/browser.svg +0 -72
  70. /package/lib/{device/admin → surface}/routes/connections.mjs +0 -0
@@ -46490,6 +46490,10 @@ var StateApiStorage = class {
46490
46490
  }
46491
46491
  };
46492
46492
 
46493
+ // lib/core/publisher/index.mjs
46494
+ init_node_crypto();
46495
+ init_wire();
46496
+
46493
46497
  // web/app/shims/node-fs.mjs
46494
46498
  var PKG = JSON.stringify({ version: "0.18.0", name: "fedipod" });
46495
46499
  var readFileSync = (p) => {
@@ -46508,10 +46512,6 @@ var unlinkSync = nope("unlinkSync");
46508
46512
  var rmSync = nope("rmSync");
46509
46513
  var node_fs_default = { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync, rmSync };
46510
46514
 
46511
- // lib/core/publisher/index.mjs
46512
- init_node_crypto();
46513
- init_wire();
46514
-
46515
46515
  // lib/shared/ua.mjs
46516
46516
  var version = "0";
46517
46517
  try {
@@ -46529,6 +46529,7 @@ try {
46529
46529
  }
46530
46530
  }
46531
46531
  var USER_AGENT = `fedipod/${version} (+https://github.com/jeff-zucker/FediPod)`;
46532
+ var AGENT_VERSION = version;
46532
46533
 
46533
46534
  // lib/core/publisher/index.mjs
46534
46535
  init_safefetch();
@@ -57531,11 +57532,31 @@ async function updateObject(publisher, s, patch, { updated = (/* @__PURE__ */ ne
57531
57532
  return update;
57532
57533
  }
57533
57534
  function rowContent(obj) {
57535
+ const text = objectText(obj);
57536
+ const esc = (v) => String(v).replace(/[&<>"]/gu, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
57537
+ const on = annotationTarget(obj);
57538
+ const link = on.page ? `<a href="${esc(on.page)}">${esc(on.page)}</a>` : "";
57539
+ const context = on.exact ? `<p>${link ? `text from ${link}:` : "text:"}</p><blockquote><p>${esc(on.exact)}</p></blockquote>` : link ? `<p>${link}</p>` : "";
57540
+ if (text) return context ? `${context}<p>comment:</p>${text}` : text;
57541
+ return context || `<p><a href="${esc(obj?.id || "")}">${esc(obj?.type || "object")}</a></p>`;
57542
+ }
57543
+ function objectText(obj) {
57534
57544
  if (typeof obj?.content === "string" && obj.content.trim()) return sanitizeHtml(obj.content);
57535
57545
  const plain = [obj?.bodyValue, obj?.name, obj?.summary].find((v) => typeof v === "string" && v.trim());
57536
57546
  if (plain) return contentHtml(plain);
57537
- const esc = (v) => String(v).replace(/[&<>"]/gu, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
57538
- return `<p><a href="${esc(obj?.id || "")}">${esc(obj?.type || "object")}</a></p>`;
57547
+ const bodies = [].concat(obj?.body || []).map((b) => b && typeof b === "object" && b.purpose !== "tagging" ? b.value && typeof b.value === "object" ? b.value["@value"] : b.value : null).filter((v) => typeof v === "string" && v.trim());
57548
+ return bodies.map((v) => /<[a-z]/iu.test(v) ? sanitizeHtml(v) : contentHtml(v)).join("");
57549
+ }
57550
+ function annotationTarget(obj) {
57551
+ const val = (v) => v && typeof v === "object" ? v["@value"] ?? v.id : v;
57552
+ for (const t of [].concat(obj?.target || [])) {
57553
+ const page2 = val(typeof t === "string" ? t : t?.source ?? t?.id);
57554
+ const quote = [].concat(t?.selector || []).find((sel) => sel?.type === "TextQuoteSelector");
57555
+ const exact = val(quote?.exact);
57556
+ const ok = typeof page2 === "string" && /^https?:\/\//u.test(page2);
57557
+ if (ok || exact) return { page: ok ? page2 : null, exact: typeof exact === "string" && exact.trim() ? exact.trim() : null };
57558
+ }
57559
+ return { page: null, exact: null };
57539
57560
  }
57540
57561
  async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, sensitive = false, slug: wanted = null, also = [], deliverTo = [], quote = null } = {}) {
57541
57562
  const { urls } = publisher;
@@ -57637,7 +57658,8 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
57637
57658
  }
57638
57659
  return note;
57639
57660
  }
57640
- async function publishObject(publisher, object, { visibility = "public", slug: wanted = null, also = [], deliverTo = [] } = {}) {
57661
+ async function publishObject(publisher, object, { visibility = "public", slug: wanted = null, also = [], deliverTo = [], unaddressed = false } = {}) {
57662
+ if (unaddressed) return publishUnaddressed(publisher, object, wanted);
57641
57663
  const { urls } = publisher;
57642
57664
  const priv = visibility === "private" || visibility === "direct";
57643
57665
  if (priv) {
@@ -57695,6 +57717,45 @@ async function publishObject(publisher, object, { visibility = "public", slug: w
57695
57717
  publisher.log(`${row.type || "object"} published: ${id} \u2192 ${inboxes.length} inbox(es)`);
57696
57718
  return { id, createId, copied: !ownId };
57697
57719
  }
57720
+ async function publishUnaddressed(publisher, object, wanted) {
57721
+ const { urls } = publisher;
57722
+ const published = (/* @__PURE__ */ new Date()).toISOString();
57723
+ const pod = publisher.config?.remotePod || urls.base;
57724
+ const ownId = typeof object.id === "string" && /^https?:\/\//u.test(object.id) && object.id.startsWith(pod) ? object.id : null;
57725
+ const name = await slugFor(publisher, urls.notes, wanted, published);
57726
+ const id = ownId || urls.notes + name;
57727
+ let doc = null;
57728
+ if (!ownId) {
57729
+ const { bto, bcc, ...sent } = object;
57730
+ doc = { ...sent, id, attributedTo: urls.actor, published: object.published || published };
57731
+ if (!doc["@context"]) doc["@context"] = AS_CTX;
57732
+ if (typeof doc.content === "string") doc.content = sanitizeHtml(doc.content);
57733
+ await write4(publisher.remote, id, doc);
57734
+ }
57735
+ const row = doc || object;
57736
+ publisher.store.addStatus({
57737
+ noteId: id,
57738
+ actor: urls.actor,
57739
+ content: rowContent(row),
57740
+ published: row.published || published,
57741
+ kind: "post",
57742
+ slug: name,
57743
+ visibility: "direct"
57744
+ });
57745
+ if (await publisher.store.commit?.() === false) publisher.log(`object kept but its timeline row was refused: ${id}`);
57746
+ const create = {
57747
+ "@context": AS_CTX,
57748
+ id: createActivityId(ownId ? urls.notes + name : id),
57749
+ type: "Create",
57750
+ actor: urls.actor,
57751
+ published: row.published || published,
57752
+ object: doc || id
57753
+ };
57754
+ await writeCreate(publisher.remote, create.id, create);
57755
+ publisher.recordOwn?.(create);
57756
+ publisher.log(`${row.type || "object"} kept, addressed to nobody: ${id}`);
57757
+ return { id, createId: create.id, copied: !ownId };
57758
+ }
57698
57759
  function rowTags(note) {
57699
57760
  const tags = [].concat(note.tag || []);
57700
57761
  const mentions = tags.filter((t) => t.type === "Mention").map((t) => ({ href: t.href, name: t.name }));
@@ -58372,10 +58433,6 @@ async function writePages(remote, before = {}, pages) {
58372
58433
  }
58373
58434
 
58374
58435
  // lib/core/publisher/index.mjs
58375
- var AGENT_VERSION = JSON.parse(node_fs_default.readFileSync(
58376
- node_path_default.join(node_path_default.dirname(fileURLToPath(import.meta.url)), "../../../package.json"),
58377
- "utf8"
58378
- )).version;
58379
58436
  var Publisher = class {
58380
58437
  constructor({
58381
58438
  config,
@@ -66457,6 +66514,11 @@ var ACTIVITY_TYPES = /* @__PURE__ */ new Set([
66457
66514
  ]);
66458
66515
  var arr = (v) => v === void 0 || v === null ? [] : Array.isArray(v) ? v : [v];
66459
66516
  var idOf2 = (v) => typeof v === "string" ? v : v?.id || null;
66517
+ var PUBLIC_SPELLINGS = /* @__PURE__ */ new Set(["Public", "as:Public"]);
66518
+ var audienceOf = (v) => {
66519
+ const id = idOf2(v);
66520
+ return PUBLIC_SPELLINGS.has(id) ? PUBLIC : id;
66521
+ };
66460
66522
  function readBody(req) {
66461
66523
  return new Promise((resolve2, reject) => {
66462
66524
  let size = 0;
@@ -66474,6 +66536,7 @@ function readBody(req) {
66474
66536
  req.on("error", reject);
66475
66537
  });
66476
66538
  }
66539
+ var isAnnotation = (o) => arr(o?.type).some((t) => t === "Annotation" || t === "oa:Annotation" || t === "http://www.w3.org/ns/oa#Annotation");
66477
66540
  var C2S = class {
66478
66541
  constructor({ agent: agent2, log: log2 = console.log, auth, scheme = null, mount = "" }) {
66479
66542
  this.agent = agent2;
@@ -66706,8 +66769,8 @@ var C2S = class {
66706
66769
  // every client means by a post with no audience chosen, and what a client
66707
66770
  // that never addresses (dokieli's annotations) needs.
66708
66771
  visibilityOf(activity, object) {
66709
- const to = arr(activity.to ?? object?.to).map(idOf2);
66710
- const cc = arr(activity.cc ?? object?.cc).map(idOf2);
66772
+ const to = arr(activity.to ?? object?.to).map(audienceOf);
66773
+ const cc = arr(activity.cc ?? object?.cc).map(audienceOf);
66711
66774
  const blind = arr(activity.bto ?? object?.bto).length + arr(activity.bcc ?? object?.bcc).length;
66712
66775
  if (!to.length && !cc.length) return blind ? "direct" : "public";
66713
66776
  if (to.includes(PUBLIC)) return "public";
@@ -66738,7 +66801,7 @@ var C2S = class {
66738
66801
  // in to/cc are listed and delivered to; the ones in bto/bcc are delivered
66739
66802
  // to and never listed (§6).
66740
66803
  addressedActors(activity, object) {
66741
- const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(idOf2)))].filter((a) => typeof a === "string" && /^https?:\/\//u.test(a) && a !== PUBLIC && a !== this.urls.followers && a !== this.urls.actor);
66804
+ const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(audienceOf)))].filter((a) => typeof a === "string" && /^https?:\/\//u.test(a) && a !== PUBLIC && a !== this.urls.followers && a !== this.urls.actor);
66742
66805
  return { also: pick("to", "cc", "audience"), deliverTo: pick("bto", "bcc") };
66743
66806
  }
66744
66807
  async _dispatch(activity, { slug, raw, reply, serial, at }) {
@@ -66760,7 +66823,8 @@ var C2S = class {
66760
66823
  const { also, deliverTo } = this.addressedActors(activity, object);
66761
66824
  if (makes !== "Note" && makes !== "Question") {
66762
66825
  const asSent = raw && typeof raw === "object" && !Array.isArray(raw) ? ACTIVITY_TYPES.has(raw.type) ? raw.object && typeof raw.object === "object" ? raw.object : null : raw : null;
66763
- const made2 = await agent2.publisher.publishObject(asSent || object, { visibility, slug, also, deliverTo });
66826
+ const unaddressed = isAnnotation(asSent || object) && ["to", "cc", "bto", "bcc", "audience"].every((f) => !arr(activity[f] ?? object[f]).length);
66827
+ const made2 = await agent2.publisher.publishObject(asSent || object, { visibility, slug, also, deliverTo, unaddressed });
66764
66828
  return reply(201, { id: made2.createId, object: made2.id }, { location: made2.createId });
66765
66829
  }
66766
66830
  const text = String(object.source?.content ?? object.content ?? "");
@@ -66973,6 +67037,7 @@ var Lease = class {
66973
67037
  this.timer = null;
66974
67038
  this.heldUntil = 0;
66975
67039
  this.denied = null;
67040
+ this.unreadable = null;
66976
67041
  }
66977
67042
  // null means the lease document is NOT THERE — nobody holds it. UNREADABLE
66978
67043
  // means we could not ask, which is a different answer and must not be read as
@@ -66984,9 +67049,13 @@ var Lease = class {
66984
67049
  try {
66985
67050
  res = await this.fetchImpl(this.url, { headers: { accept: "application/json" } });
66986
67051
  if (res.status === 404 || res.status === 410) return null;
66987
- if (res.status >= 400) return UNREADABLE;
67052
+ if (res.status >= 400) {
67053
+ this.unreadable = `HTTP ${res.status}`;
67054
+ return UNREADABLE;
67055
+ }
66988
67056
  body = await res.text();
66989
- } catch {
67057
+ } catch (e) {
67058
+ this.unreadable = e.message;
66990
67059
  return UNREADABLE;
66991
67060
  }
66992
67061
  this.etag = res.headers?.get?.("etag") || null;
@@ -67017,7 +67086,7 @@ var Lease = class {
67017
67086
  const cur = await this.readFresh();
67018
67087
  if (cur === UNREADABLE) {
67019
67088
  this.denied = "unreadable";
67020
- this.log("lease unreadable \u2014 staying a viewer");
67089
+ this.log(`lease unreadable (${this.unreadable}) \u2014 staying a viewer`);
67021
67090
  return false;
67022
67091
  }
67023
67092
  if (cur && cur.holder !== this.id && Date.now() < cur.expiresAt) {
@@ -67419,7 +67488,7 @@ function sendPodSigninPage(res, client, webId, mount = "") {
67419
67488
  } catch {
67420
67489
  }
67421
67490
  const who = client.name ? escapeHtml(client.name) : where ? escapeHtml(where) : "A client";
67422
- asking = `<p><strong>${who}</strong> is asking to use your account${where ? `, and will be sent back to <code>${escapeHtml(where)}</code>` : ""}.</p>`;
67491
+ asking = `<p><strong>${who}</strong> is asking to use your account${where ? `, and you will be sent back to <code>${escapeHtml(where)}</code>` : ""}.</p>`;
67423
67492
  }
67424
67493
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" });
67425
67494
  res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
@@ -71850,7 +71919,7 @@ var fail2 = () => {
71850
71919
  };
71851
71920
  var node_os_default = new Proxy({}, { get: () => fail2 });
71852
71921
 
71853
- // lib/device/home.mjs
71922
+ // lib/shared/files.mjs
71854
71923
  function writeFileAtomic(file, body, { mode = 384 } = {}) {
71855
71924
  const tmp = `${file}.${process.pid}.tmp`;
71856
71925
  node_fs_default.mkdirSync(node_path_default.dirname(file), { recursive: true, mode: 448 });
@@ -72951,9 +73020,18 @@ init_urls();
72951
73020
 
72952
73021
  // web/app/copy-mode.mjs
72953
73022
  var RENEW_BEFORE_MS = 2 * 36e5;
72954
- async function openCopy(agent2, frontOrigin, { handle: handle7 = null, kept = null } = {}) {
73023
+ var ASK_AGAIN_MS = 15 * 6e4;
73024
+ var REMEMBERED = [404, 409, 501, 502, 503];
73025
+ var failedKey = (agent2, frontOrigin) => `copy-open-failed:${frontOrigin}:${agent2.webId}`;
73026
+ var forgetFailedOpen = (agent2, frontOrigin) => kvDel(failedKey(agent2, frontOrigin)).catch(() => {
73027
+ });
73028
+ async function openCopy(agent2, frontOrigin, { handle: handle7 = null, kept = null, askAnyway = false, letGo = false } = {}) {
72955
73029
  if (kept && kept.expiresAt - Date.now() > RENEW_BEFORE_MS) return kept;
72956
73030
  if (!agent2.sessionFetch || !frontOrigin) return null;
73031
+ const key = failedKey(agent2, frontOrigin);
73032
+ const failed = await kvGet(key).catch(() => null);
73033
+ const held = letGo && failed?.status === 409;
73034
+ if (!askAnyway && !held && failed && Date.now() - failed.at < ASK_AGAIN_MS) return null;
72957
73035
  let res;
72958
73036
  try {
72959
73037
  res = await agent2.sessionFetch(`${frontOrigin.replace(/\/$/u, "")}/api/state/open`, {
@@ -72966,9 +73044,16 @@ async function openCopy(agent2, frontOrigin, { handle: handle7 = null, kept = nu
72966
73044
  return null;
72967
73045
  }
72968
73046
  if (res.status !== 200) {
72969
- if (![404, 409, 501].includes(res.status)) agent2.log(`the account's copy: the gateway answered ${res.status}`);
73047
+ if (![404, 409, 501].includes(res.status)) {
73048
+ const why = (await res.json().catch(() => null))?.error;
73049
+ agent2.log(`the account's copy: the gateway answered ${res.status}${why ? ` (${why})` : ""}`);
73050
+ }
73051
+ if (REMEMBERED.includes(res.status)) await kvPut(key, { at: Date.now(), status: res.status }).catch(() => {
73052
+ });
72970
73053
  return null;
72971
73054
  }
73055
+ if (failed) await kvDel(key).catch(() => {
73056
+ });
72972
73057
  const copy = await res.json().catch(() => null);
72973
73058
  return copy?.base && copy?.token ? { handle: copy.handle, base: copy.base, token: copy.token, expiresAt: copy.expiresAt, podHome: copy.podHome || null } : null;
72974
73059
  }
@@ -73052,7 +73137,7 @@ async function moveIntoCopy(agent2, frontOrigin) {
73052
73137
  if (agent2.copy) return true;
73053
73138
  await agent2.store.commit();
73054
73139
  await agent2.lease.release();
73055
- const copy = await openCopy(agent2, frontOrigin, { handle: agent2.doorKey });
73140
+ const copy = await openCopy(agent2, frontOrigin, { handle: agent2.doorKey, letGo: true });
73056
73141
  if (!copy) {
73057
73142
  if (await agent2.lease.acquire()) agent2.lease.startRenewal();
73058
73143
  return false;
@@ -73257,6 +73342,7 @@ var BrowserAgent = class _BrowserAgent {
73257
73342
  if (!on) return { status: 200, ok: true, kept: false };
73258
73343
  const said = await this.tellGateway("keeper", { handle: this.doorKey, on: true });
73259
73344
  if (said?.status === 200) {
73345
+ await forgetFailedOpen(this, this.frontOrigin);
73260
73346
  this._keeper.kept = true;
73261
73347
  if (this.masto) this.masto.scheduling = true;
73262
73348
  this.log("the gateway keeps this account running while the app is closed");
@@ -73470,7 +73556,7 @@ var BrowserAgent = class _BrowserAgent {
73470
73556
  const warmFrom = restart ? await warmMeta(webId).catch(() => null) : null;
73471
73557
  this.holderId = await this.deviceId();
73472
73558
  this.frontOrigin = frontOrigin || null;
73473
- this.copy = await openCopy(this, this.frontOrigin, { kept: isWarm(warmFrom) ? warmFrom.copy : null });
73559
+ this.copy = await openCopy(this, this.frontOrigin, { kept: isWarm(warmFrom) ? warmFrom.copy : null, askAnyway: !restart });
73474
73560
  const warmRoot = isWarm(warmFrom) && !!warmFrom.copy === !!this.copy ? warmFrom.root : null;
73475
73561
  const copyRoot = this.copy?.podHome?.startsWith(remotePod) ? this.copy.podHome.slice(remotePod.length) : null;
73476
73562
  this._sweptAt = { ...warmFrom?.sweptAt || {} };