fedipod 1.44.0 → 1.47.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 (42) hide show
  1. package/README.md +8 -13
  2. package/architecture.md +5 -2
  3. package/gateway.md +56 -24
  4. package/gui.md +3 -2
  5. package/lib/client/c2s.mjs +23 -74
  6. package/lib/client/masto/index.mjs +5 -4
  7. package/lib/client/masto/render.mjs +4 -1
  8. package/lib/core/intake/activities.mjs +8 -9
  9. package/lib/core/intake/counts.mjs +143 -0
  10. package/lib/core/intake/index.mjs +12 -1
  11. package/lib/core/intake/received.mjs +78 -0
  12. package/lib/core/publisher/notes.mjs +4 -1
  13. package/lib/core/social.mjs +2 -0
  14. package/lib/core/storage.mjs +54 -5
  15. package/lib/core/wire.mjs +16 -5
  16. package/lib/gateway/account-agent.mjs +20 -4
  17. package/lib/gateway/copy.mjs +95 -5
  18. package/lib/gateway/front-core.mjs +18 -9
  19. package/lib/gateway/gateway-core.mjs +46 -14
  20. package/lib/gateway/held-mail.mjs +11 -4
  21. package/lib/gateway/httpsig.mjs +6 -1
  22. package/lib/gateway/keeper.mjs +9 -6
  23. package/lib/gateway/masto-gateway.mjs +140 -40
  24. package/lib/gateway/private-read.mjs +98 -0
  25. package/lib/gateway/quiet.mjs +4 -3
  26. package/lib/gateway/state-api.mjs +73 -18
  27. package/lib/pod/notes.mjs +25 -0
  28. package/lib/pod/urls.mjs +3 -0
  29. package/lib/server/embed.mjs +25 -0
  30. package/lib/surface/routes/owner.mjs +2 -2
  31. package/package.json +2 -2
  32. package/web/admin/gateway.js +8 -24
  33. package/web/admin/index.html +0 -16
  34. package/web/admin/record.js +1 -2
  35. package/web/admin/upkeep.js +1 -5
  36. package/web/app/admin-facade.mjs +1 -1
  37. package/web/app/agent.mjs +27 -10
  38. package/web/app/copy-mode.mjs +98 -13
  39. package/web/app/dist/boot.js +12 -3
  40. package/web/app/dist/boot.js.map +2 -2
  41. package/web/app/dist/sw.js +582 -282
  42. package/web/app/dist/sw.js.map +4 -4
@@ -133,6 +133,9 @@ function apUrls(remotePod, root, { publicBase = null } = {}) {
133
133
  // reads them there, with a credential the pod checks.
134
134
  ownOutbox: home + "ap/private/outbox",
135
135
  liked: home + "ap/private/liked",
136
+ // The inbox as its owner reads it (§5.2): what the account received,
137
+ // kept by the drain as a paged collection beside the archive.
138
+ ownInbox: home + "ap/private/inbox",
136
139
  profileHtml: face + "ap/profile.html",
137
140
  // Media stays on the pod even when fronted: attachment urls are not
138
141
  // identity-checked by remotes, and proxying blobs would be pure cost.
@@ -9762,6 +9765,7 @@ __export(wire_exports, {
9762
9765
  publicHandle: () => publicHandle,
9763
9766
  questionDoc: () => questionDoc,
9764
9767
  quotePolicy: () => quotePolicy,
9768
+ reactionCounts: () => reactionCounts,
9765
9769
  rejectActivity: () => rejectActivity,
9766
9770
  repliesId: () => repliesId,
9767
9771
  sanitizeHtml: () => sanitizeHtml,
@@ -9799,7 +9803,7 @@ function jrd({ handle: handle7, host, actor, aliases = [], page: page2 = null })
9799
9803
  };
9800
9804
  }
9801
9805
  function followsNeedApproval(config = {}) {
9802
- return !!config.approveJoins || config.kind !== "group" && !config.autoAcceptFollows;
9806
+ return !!config.approveJoins || config.kind !== "group" && config.autoAcceptFollows === false;
9803
9807
  }
9804
9808
  function actorDoc({
9805
9809
  urls,
@@ -10179,6 +10183,12 @@ function quotePolicy(visibility) {
10179
10183
  const open3 = visibility === "public" || visibility === "unlisted";
10180
10184
  return { canQuote: { automaticApproval: open3 ? [PUBLIC] : [], manualApproval: [] } };
10181
10185
  }
10186
+ function reactionCounts(likes = 0, shares = 0) {
10187
+ return {
10188
+ likes: { type: "Collection", totalItems: Math.max(0, Number(likes) || 0) },
10189
+ shares: { type: "Collection", totalItems: Math.max(0, Number(shares) || 0) }
10190
+ };
10191
+ }
10182
10192
  function noteDoc({
10183
10193
  urls,
10184
10194
  slug,
@@ -10193,7 +10203,9 @@ function noteDoc({
10193
10203
  updated = null,
10194
10204
  container = null,
10195
10205
  also = [],
10196
- quote = null
10206
+ quote = null,
10207
+ likes = 0,
10208
+ shares = 0
10197
10209
  }) {
10198
10210
  const id = (container || urls.notes) + slug;
10199
10211
  const who = mentions.map((m) => m.actor);
@@ -10210,6 +10222,7 @@ function noteDoc({
10210
10222
  to: addressed.to,
10211
10223
  cc: addressed.cc,
10212
10224
  replies: repliesId(id),
10225
+ ...reactionCounts(likes, shares),
10213
10226
  interactionPolicy: quotePolicy(visibility)
10214
10227
  };
10215
10228
  if (quote?.id) {
@@ -29972,13 +29985,13 @@ var require_jsonld = __commonJS({
29972
29985
  );
29973
29986
  };
29974
29987
  jsonld2.get = async function(url, options) {
29975
- let load;
29988
+ let load2;
29976
29989
  if (typeof options.documentLoader === "function") {
29977
- load = options.documentLoader;
29990
+ load2 = options.documentLoader;
29978
29991
  } else {
29979
- load = jsonld2.documentLoader;
29992
+ load2 = jsonld2.documentLoader;
29980
29993
  }
29981
- const remoteDoc = await load(url);
29994
+ const remoteDoc = await load2(url);
29982
29995
  try {
29983
29996
  if (!remoteDoc.document) {
29984
29997
  throw new JsonLdError(
@@ -32864,14 +32877,16 @@ function makeSafeLoader({ getActors = null, fetchImpl = fetch } = {}) {
32864
32877
  async function verifyHttpSignature(request, { documentLoader, keyCache, timeWindow } = {}) {
32865
32878
  const hadSig = request.headers.get("signature") != null;
32866
32879
  let result = null;
32880
+ let threw = null;
32867
32881
  try {
32868
32882
  result = await verifyRequestDetailed(request, {
32869
32883
  documentLoader,
32870
32884
  ...keyCache ? { keyCache } : {},
32871
32885
  timeWindow: timeWindow ?? { hours: 1 }
32872
32886
  });
32873
- } catch {
32887
+ } catch (e) {
32874
32888
  result = null;
32889
+ threw = e?.message || String(e);
32875
32890
  }
32876
32891
  const method = request.headers.has("signature-input") ? "rfc9421" : "draft-cavage";
32877
32892
  if (result?.verified) {
@@ -32912,7 +32927,8 @@ async function verifyHttpSignature(request, { documentLoader, keyCache, timeWind
32912
32927
  keyId: null,
32913
32928
  actor: null,
32914
32929
  reason: "bad-signature",
32915
- checks: { signature: false }
32930
+ checks: { signature: false },
32931
+ why: threw ? `library threw: ${threw.split("\n")[0].slice(0, 120)}` : kind || "unknown"
32916
32932
  };
32917
32933
  }
32918
32934
  function sortedKeys(v) {
@@ -33266,8 +33282,8 @@ var require_common = __commonJS({
33266
33282
  var require_browser = __commonJS({
33267
33283
  "node_modules/debug/src/browser.js"(exports, module2) {
33268
33284
  exports.formatArgs = formatArgs;
33269
- exports.save = save;
33270
- exports.load = load;
33285
+ exports.save = save2;
33286
+ exports.load = load2;
33271
33287
  exports.useColors = useColors;
33272
33288
  exports.storage = localstorage();
33273
33289
  exports.destroy = /* @__PURE__ */ (() => {
@@ -33393,7 +33409,7 @@ var require_browser = __commonJS({
33393
33409
  }
33394
33410
  exports.log = console.debug || console.log || (() => {
33395
33411
  });
33396
- function save(namespaces) {
33412
+ function save2(namespaces) {
33397
33413
  try {
33398
33414
  if (namespaces) {
33399
33415
  exports.storage.setItem("debug", namespaces);
@@ -33403,7 +33419,7 @@ var require_browser = __commonJS({
33403
33419
  } catch (error2) {
33404
33420
  }
33405
33421
  }
33406
- function load() {
33422
+ function load2() {
33407
33423
  let r;
33408
33424
  try {
33409
33425
  r = exports.storage.getItem("debug") || exports.storage.getItem("DEBUG");
@@ -46117,13 +46133,13 @@ var PodStore = class {
46117
46133
  _mergeStatus(all, at, s) {
46118
46134
  const row = all[at];
46119
46135
  const known2 = new Set((row.sourceAccts || []).map((v) => v.acct));
46120
- const fresh = (s.sourceAccts || []).filter((v) => v && !known2.has(v.acct));
46136
+ const fresh2 = (s.sourceAccts || []).filter((v) => v && !known2.has(v.acct));
46121
46137
  const first = (k) => k === "post" || k === "timeline";
46122
46138
  const raise = first(s.kind) && !first(row.kind);
46123
- if (!fresh.length && !raise) return { added: false, merged: false, status: row };
46139
+ if (!fresh2.length && !raise) return { added: false, merged: false, status: row };
46124
46140
  all[at] = {
46125
46141
  ...row,
46126
- ...fresh.length ? { sourceAccts: [...row.sourceAccts || [], ...fresh] } : {},
46142
+ ...fresh2.length ? { sourceAccts: [...row.sourceAccts || [], ...fresh2] } : {},
46127
46143
  ...raise ? { kind: s.kind, ...s.slug ? { slug: s.slug } : {} } : {}
46128
46144
  };
46129
46145
  this.write("statuses.json", all);
@@ -46295,11 +46311,27 @@ var PodStore = class {
46295
46311
  }
46296
46312
  };
46297
46313
 
46298
- // stub:node:fs/promises
46299
- var fail = () => {
46300
- throw new Error("node:fs/promises is not available in the browser agent");
46314
+ // lib/core/publisher/index.mjs
46315
+ init_node_crypto();
46316
+ init_wire();
46317
+
46318
+ // web/app/shims/node-fs.mjs
46319
+ var PKG = JSON.stringify({ version: "0.18.0", name: "fedipod" });
46320
+ var readFileSync = (p) => {
46321
+ if (String(p).endsWith("package.json")) return PKG;
46322
+ throw new Error(`node:fs readFileSync(${p}) is not available in the browser agent`);
46323
+ };
46324
+ var existsSync = () => false;
46325
+ var nope = (name) => () => {
46326
+ throw new Error(`node:fs ${name} is not available in the browser agent`);
46301
46327
  };
46302
- var promises_default = new Proxy({}, { get: () => fail });
46328
+ var writeFileSync = nope("writeFileSync");
46329
+ var mkdirSync = nope("mkdirSync");
46330
+ var readdirSync = () => [];
46331
+ var statSync = nope("statSync");
46332
+ var unlinkSync = nope("unlinkSync");
46333
+ var rmSync = nope("rmSync");
46334
+ var node_fs_default = { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync, rmSync };
46303
46335
 
46304
46336
  // web/app/shims/node-path.mjs
46305
46337
  var normalize = (p) => {
@@ -46345,173 +46377,6 @@ var fileURLToPath = (u) => {
46345
46377
  var pathToFileURL = (p) => new URL("file://" + (p.startsWith("/") ? p : "/" + p));
46346
46378
  var node_url_default = { URL: globalThis.URL, URLSearchParams: globalThis.URLSearchParams, fileURLToPath, pathToFileURL };
46347
46379
 
46348
- // lib/core/pod-only.mjs
46349
- var podOnly = (name) => name === "keys.json" || name === "lease.json" || name.startsWith("conn-");
46350
-
46351
- // lib/core/storage.mjs
46352
- var LDP2 = Namespace("http://www.w3.org/ns/ldp#");
46353
- var slash = (u) => u.endsWith("/") ? u : u + "/";
46354
- var HttpStorage = class {
46355
- constructor(base, fetchImpl) {
46356
- this.base = slash(base);
46357
- this.fetchImpl = fetchImpl;
46358
- }
46359
- get kind() {
46360
- return "pod";
46361
- }
46362
- // The same jail FileStorage has, and for the same reason: a path here can
46363
- // carry `..`, encodeURI leaves both `.` and `/` alone, and fetch normalises
46364
- // the segments away — so a name derived from remote input could name a
46365
- // resource outside the container entirely. A prefix check is not enough on
46366
- // its own: a concatenated URL always satisfies one.
46367
- _url(p) {
46368
- const u = new URL(encodeURI(p), this.base);
46369
- if (!u.href.startsWith(this.base)) throw new Error(`path escapes the container: ${p}`);
46370
- return u.href;
46371
- }
46372
- async list(sub = "", { etag } = {}) {
46373
- const url = this._url(sub);
46374
- const res = await this.fetchImpl(url, {
46375
- headers: { accept: "text/turtle", ...etag ? { "if-none-match": etag } : {} }
46376
- });
46377
- if (res.status === 304) return { notModified: true, names: null, etag };
46378
- if (res.status === 404) return { notModified: false, names: [], etag: null, missing: true };
46379
- if (res.status >= 400) throw new Error(`container unreadable (HTTP ${res.status})`);
46380
- const g = graph();
46381
- parse2(await res.text(), g, url, "text/turtle");
46382
- const here = namedNode2(url);
46383
- const names = g.each(here, LDP2("contains"), null, here).map((n) => n.value).filter((u) => u.startsWith(url) && u !== url).map((u) => decodeURIComponent(u.slice(url.length)));
46384
- return { notModified: false, names, etag: res.headers.get("etag") };
46385
- }
46386
- // `accept` is for callers reading something that is RDF but is wanted as it
46387
- // was written: asking turtle-first for a JSON-LD document gets turtle back,
46388
- // because the server is entitled to convert between two RDF syntaxes.
46389
- async read(p, { etag, accept } = {}) {
46390
- const res = await this.fetchImpl(this._url(p), {
46391
- headers: {
46392
- accept: accept || "text/turtle, application/json;q=0.9, */*;q=0.8",
46393
- ...etag ? { "if-none-match": etag } : {}
46394
- }
46395
- });
46396
- if (res.status === 304) return { ok: true, notModified: true, status: 304, body: null, etag };
46397
- if (res.status >= 400) return { ok: false, notModified: false, status: res.status, body: null, etag: null };
46398
- return { ok: true, notModified: false, status: res.status, body: await res.text(), etag: res.headers.get("etag") };
46399
- }
46400
- async write(p, body, contentType) {
46401
- const url = this._url(p);
46402
- try {
46403
- const res = await this.fetchImpl(url, {
46404
- method: "PUT",
46405
- headers: { "content-type": contentType },
46406
- body
46407
- });
46408
- if (res.status < 400) return { ok: true, retry: false, why: "" };
46409
- const retry = res.status >= 500 || res.status === 429;
46410
- const ra = Number(res.headers?.get?.("retry-after"));
46411
- return {
46412
- ok: false,
46413
- retry,
46414
- why: `HTTP ${res.status}`,
46415
- retryAfterMs: Number.isFinite(ra) && ra > 0 ? ra * 1e3 : 0
46416
- };
46417
- } catch (e) {
46418
- return { ok: false, retry: true, why: e.message, retryAfterMs: 0 };
46419
- }
46420
- }
46421
- async remove(p) {
46422
- const url = this._url(p);
46423
- try {
46424
- const res = await this.fetchImpl(url, { method: "DELETE" });
46425
- return res.status < 400 || res.status === 404;
46426
- } catch {
46427
- return false;
46428
- }
46429
- }
46430
- };
46431
- var StateApiStorage = class {
46432
- constructor(base, { fetchImpl = globalThis.fetch, token, holder, pod = null, onRefused = null }) {
46433
- this.base = slash(base);
46434
- this.fetchImpl = fetchImpl;
46435
- this.token = token;
46436
- this.holder = holder;
46437
- this.pod = pod;
46438
- this.onRefused = onRefused;
46439
- }
46440
- get kind() {
46441
- return "copy";
46442
- }
46443
- _ask(p, init = {}) {
46444
- return this.fetchImpl(this.base + encodeURIComponent(p), {
46445
- ...init,
46446
- headers: { authorization: `Bearer ${this.token}`, ...init.headers || {} }
46447
- });
46448
- }
46449
- async list(sub = "", { etag } = {}) {
46450
- if (sub) return { notModified: false, names: [], etag: null };
46451
- const res = await this._ask("", { headers: etag ? { "if-none-match": etag } : {} });
46452
- if (res.status === 304) return { notModified: true, names: null, etag };
46453
- if (res.status >= 400) throw new Error(`the account's copy at the gateway is unreadable (HTTP ${res.status})`);
46454
- const { names } = await res.json();
46455
- return { notModified: false, names, etag: res.headers.get("etag") };
46456
- }
46457
- async read(p, opts = {}) {
46458
- if (podOnly(p)) return this.pod ? this.pod.read(p, opts) : { ok: false, notModified: false, status: 404, body: null, etag: null };
46459
- const res = await this._ask(p, { headers: opts.etag ? { "if-none-match": opts.etag } : {} });
46460
- if (res.status === 304) return { ok: true, notModified: true, status: 304, body: null, etag: opts.etag };
46461
- if (res.status >= 400) return { ok: false, notModified: false, status: res.status, body: null, etag: null };
46462
- return { ok: true, notModified: false, status: res.status, body: await res.text(), etag: res.headers.get("etag") };
46463
- }
46464
- async write(p, body, contentType = "application/json") {
46465
- if (podOnly(p)) return this.pod ? this.pod.write(p, body, contentType) : { ok: false, retry: false, why: "no pod for this document" };
46466
- try {
46467
- const res = await this._ask(p, { method: "PUT", headers: { "content-type": contentType, "x-fedipod-holder": this.holder }, body });
46468
- if (res.status < 400) return { ok: true, retry: false, why: "" };
46469
- if (res.status === 409) {
46470
- this.onRefused?.();
46471
- return { ok: false, retry: false, why: "another agent holds this account now", lost: true };
46472
- }
46473
- return { ok: false, retry: res.status >= 500 || res.status === 429, why: `HTTP ${res.status}`, retryAfterMs: 0 };
46474
- } catch (e) {
46475
- return { ok: false, retry: true, why: e.message, retryAfterMs: 0 };
46476
- }
46477
- }
46478
- async remove(p) {
46479
- if (podOnly(p)) return this.pod ? this.pod.remove(p) : false;
46480
- try {
46481
- const res = await this._ask(p, { method: "DELETE", headers: { "x-fedipod-holder": this.holder } });
46482
- if (res.status === 409) {
46483
- this.onRefused?.();
46484
- return false;
46485
- }
46486
- return res.status < 400 || res.status === 404;
46487
- } catch {
46488
- return false;
46489
- }
46490
- }
46491
- };
46492
-
46493
- // lib/core/publisher/index.mjs
46494
- init_node_crypto();
46495
- init_wire();
46496
-
46497
- // web/app/shims/node-fs.mjs
46498
- var PKG = JSON.stringify({ version: "0.18.0", name: "fedipod" });
46499
- var readFileSync = (p) => {
46500
- if (String(p).endsWith("package.json")) return PKG;
46501
- throw new Error(`node:fs readFileSync(${p}) is not available in the browser agent`);
46502
- };
46503
- var existsSync = () => false;
46504
- var nope = (name) => () => {
46505
- throw new Error(`node:fs ${name} is not available in the browser agent`);
46506
- };
46507
- var writeFileSync = nope("writeFileSync");
46508
- var mkdirSync = nope("mkdirSync");
46509
- var readdirSync = () => [];
46510
- var statSync = nope("statSync");
46511
- var unlinkSync = nope("unlinkSync");
46512
- var rmSync = nope("rmSync");
46513
- var node_fs_default = { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync, rmSync };
46514
-
46515
46380
  // lib/shared/ua.mjs
46516
46381
  var version = "0";
46517
46382
  try {
@@ -46668,6 +46533,27 @@ var writeEmptyReplies = (pod, repliesId2, doc) => pod.putJson(repliesId2, doc);
46668
46533
  var readReplies = (pod, repliesId2) => pod.getJson(repliesId2);
46669
46534
  var writeReplies = (pod, repliesId2, doc) => pod.putJson(repliesId2, doc);
46670
46535
  var read = (pod, noteId) => pod.getJson(noteId);
46536
+ async function readVersioned(pod, noteId) {
46537
+ if (typeof pod.fetch !== "function") return { doc: await pod.getJson(noteId), version: null };
46538
+ const res = await pod.fetch(noteId, { headers: { accept: "application/activity+json" } });
46539
+ if (res.status === 404 || res.status === 410) return { doc: null, version: null };
46540
+ if (res.status >= 400) throw new Error(`GET ${noteId} \u2192 ${res.status}`);
46541
+ return { doc: await res.json().catch(() => null), version: res.headers?.get?.("etag") || null };
46542
+ }
46543
+ async function writeIfUnchanged(pod, noteId, doc, version2) {
46544
+ if (!version2 || typeof pod.fetch !== "function") {
46545
+ await pod.putJson(noteId, doc);
46546
+ return true;
46547
+ }
46548
+ const res = await pod.fetch(noteId, {
46549
+ method: "PUT",
46550
+ headers: { "content-type": "application/activity+json", "if-match": version2 },
46551
+ body: JSON.stringify(doc)
46552
+ });
46553
+ if (res.status === 412) return false;
46554
+ if (res.status >= 400) throw new Error(`PUT ${noteId} \u2192 ${res.status}`);
46555
+ return true;
46556
+ }
46671
46557
  async function writeTombstone2(pod, noteId, doc) {
46672
46558
  await pod.putJson(noteId, doc);
46673
46559
  await pod.setAcl(noteId, PUBLIC_READ5);
@@ -57364,6 +57250,114 @@ async function readLenient(raw) {
57364
57250
  init_node_crypto();
57365
57251
  init_wire();
57366
57252
  init_wire_quotes();
57253
+
57254
+ // lib/core/intake/counts.mjs
57255
+ init_wire();
57256
+ var MAX_NAMED = 1e3;
57257
+ var MAX_POSTS = 2e3;
57258
+ var REWRITES_PER_SWEEP = 20;
57259
+ var COUNTS = "counts.json";
57260
+ var KIND = { Like: "likes", Announce: "shares" };
57261
+ var MORE = { likes: "moreLikes", shares: "moreShares" };
57262
+ var load = (store) => {
57263
+ const st2 = typeof store?.read === "function" ? store.read(COUNTS, null) : null;
57264
+ return st2 && typeof st2 === "object" && st2.posts && typeof st2.posts === "object" ? { posts: st2.posts, dirty: Array.isArray(st2.dirty) ? st2.dirty : [] } : { posts: {}, dirty: [] };
57265
+ };
57266
+ var totals = (p) => ({
57267
+ likes: (p?.likes?.length || 0) + (p?.moreLikes || 0),
57268
+ shares: (p?.shares?.length || 0) + (p?.moreShares || 0)
57269
+ });
57270
+ function totalsFor(store, noteId) {
57271
+ const st2 = store?.cache?.get?.(COUNTS) ?? store?.read?.(COUNTS, null);
57272
+ return totals(st2?.posts?.[noteId]);
57273
+ }
57274
+ function save(store, st2, noteId) {
57275
+ if (typeof store?.write !== "function") return;
57276
+ if (noteId && !st2.dirty.includes(noteId)) st2.dirty.push(noteId);
57277
+ const ids = Object.keys(st2.posts);
57278
+ if (ids.length > MAX_POSTS) {
57279
+ ids.sort((a, b) => String(st2.posts[a].at || "").localeCompare(String(st2.posts[b].at || "")));
57280
+ for (const id of ids.slice(0, ids.length - MAX_POSTS)) delete st2.posts[id];
57281
+ st2.dirty = st2.dirty.filter((id) => st2.posts[id]);
57282
+ }
57283
+ store.write(COUNTS, st2);
57284
+ }
57285
+ function recordReaction(store, type, noteId, actor) {
57286
+ const kind = KIND[type];
57287
+ if (!kind || !noteId || !actor) return false;
57288
+ const st2 = load(store);
57289
+ const p = st2.posts[noteId] || { likes: [], shares: [] };
57290
+ const named = p[kind] || [];
57291
+ if (named.includes(actor)) return false;
57292
+ if (named.length >= MAX_NAMED) p[MORE[kind]] = (p[MORE[kind]] || 0) + 1;
57293
+ else p[kind] = [...named, actor];
57294
+ p.at = (/* @__PURE__ */ new Date()).toISOString();
57295
+ st2.posts[noteId] = p;
57296
+ save(store, st2, noteId);
57297
+ return true;
57298
+ }
57299
+ function withdrawReaction(store, type, noteId, actor) {
57300
+ const kind = KIND[type];
57301
+ if (!kind || !noteId || !actor) return false;
57302
+ const st2 = load(store);
57303
+ const p = st2.posts[noteId];
57304
+ if (!p) return false;
57305
+ const named = p[kind] || [];
57306
+ if (named.includes(actor)) p[kind] = named.filter((a) => a !== actor);
57307
+ else if (p[MORE[kind]] > 0) p[MORE[kind]] -= 1;
57308
+ else return false;
57309
+ p.at = (/* @__PURE__ */ new Date()).toISOString();
57310
+ save(store, st2, noteId);
57311
+ return true;
57312
+ }
57313
+ function forgetCounts(store, noteId) {
57314
+ const st2 = load(store);
57315
+ if (!st2.posts[noteId] && !st2.dirty.includes(noteId)) return;
57316
+ delete st2.posts[noteId];
57317
+ st2.dirty = st2.dirty.filter((id) => id !== noteId);
57318
+ save(store, st2, null);
57319
+ }
57320
+ function seed(intake) {
57321
+ const { store } = intake;
57322
+ if (typeof store?.read !== "function" || store.read(COUNTS, null)) return;
57323
+ const notes = intake.urls?.notes;
57324
+ const all = typeof store.getNotifications === "function" ? store.getNotifications() : [];
57325
+ for (const n of [...all].reverse()) {
57326
+ if (!notes || !String(n?.noteId || "").startsWith(notes)) continue;
57327
+ if (n.type === "favourite") recordReaction(store, "Like", n.noteId, n.actor);
57328
+ else if (n.type === "reblog") recordReaction(store, "Announce", n.noteId, n.actor);
57329
+ }
57330
+ if (!store.read(COUNTS, null)) store.write(COUNTS, { posts: {}, dirty: [] });
57331
+ }
57332
+ async function publishCounts(intake) {
57333
+ seed(intake);
57334
+ const st2 = load(intake.store);
57335
+ if (!st2.dirty.length) return 0;
57336
+ const left = [];
57337
+ let wrote = 0;
57338
+ for (const noteId of st2.dirty.slice(0, REWRITES_PER_SWEEP)) {
57339
+ try {
57340
+ const { doc, version: version2 } = await readVersioned(intake.remote, noteId);
57341
+ if (!doc || typeof doc !== "object" || !doc.id || doc.type === "Tombstone") {
57342
+ delete st2.posts[noteId];
57343
+ continue;
57344
+ }
57345
+ const t = totals(st2.posts[noteId]);
57346
+ const next = { ...doc, ...reactionCounts(t.likes, t.shares) };
57347
+ if (JSON.stringify([next.likes, next.shares]) === JSON.stringify([doc.likes, doc.shares])) continue;
57348
+ if (await writeIfUnchanged(intake.remote, doc.id, next, version2)) wrote++;
57349
+ else left.push(noteId);
57350
+ } catch (e) {
57351
+ intake.log?.(`like and boost totals for ${noteId}: ${e.message}`);
57352
+ left.push(noteId);
57353
+ }
57354
+ }
57355
+ st2.dirty = [...left, ...st2.dirty.slice(REWRITES_PER_SWEEP)].filter((id) => st2.posts[id]);
57356
+ intake.store.write(COUNTS, st2);
57357
+ return wrote;
57358
+ }
57359
+
57360
+ // lib/core/publisher/notes.mjs
57367
57361
  async function ensureMediaContainer(publisher) {
57368
57362
  if (publisher._mediaReady) return;
57369
57363
  if (await exists(publisher.remote, publisher.urls.media)) {
@@ -57505,7 +57499,9 @@ var FIXED = /* @__PURE__ */ new Set([
57505
57499
  "endTime",
57506
57500
  "closed",
57507
57501
  "votersCount",
57508
- "replies"
57502
+ "replies",
57503
+ "likes",
57504
+ "shares"
57509
57505
  ]);
57510
57506
  async function updateObject(publisher, s, patch, { updated = (/* @__PURE__ */ new Date()).toISOString() } = {}) {
57511
57507
  const { urls } = publisher;
@@ -57798,7 +57794,8 @@ async function updateNote(publisher, s, { content, spoilerText = null, sensitive
57798
57794
  updated,
57799
57795
  container,
57800
57796
  also: [.../* @__PURE__ */ new Set([...s.named || [], ...reply ? [reply.actor] : []])],
57801
- quote: rowQuote(s)
57797
+ quote: rowQuote(s),
57798
+ ...totalsFor(publisher.store, s.noteId)
57802
57799
  });
57803
57800
  await write4(publisher.remote, note.id, note);
57804
57801
  await writeCreate(publisher.remote, createActivityId(note.id), createActivity(note, urls));
@@ -57851,7 +57848,8 @@ async function restateNote(publisher, s, { quote }) {
57851
57848
  updated: s.editedAt || null,
57852
57849
  container,
57853
57850
  also: [...reply ? [reply.actor] : [], ...s.quoteRequest?.actor ? [s.quoteRequest.actor] : []],
57854
- quote
57851
+ quote,
57852
+ ...totalsFor(publisher.store, s.noteId)
57855
57853
  });
57856
57854
  await write4(publisher.remote, note.id, note);
57857
57855
  await writeCreate(publisher.remote, createActivityId(note.id), createActivity(note, urls));
@@ -59464,14 +59462,18 @@ async function amplify(intake, noteId, { approved = false, activity = null } = {
59464
59462
  serial: intake.serial++,
59465
59463
  audience: intake.urls.actor
59466
59464
  });
59465
+ if (intake.onCarry) {
59466
+ const placed = await intake.onCarry({ noteId, actor: s.actor, activity: act, sent: activity });
59467
+ if (placed === false) {
59468
+ intake.log(`not amplified \u2014 ${noteId} has no place here`);
59469
+ return;
59470
+ }
59471
+ }
59467
59472
  await intake.deliverer.deliverToAll(inboxes, act);
59468
59473
  intake.store.updateStatus(noteId, { announcedAt: (/* @__PURE__ */ new Date()).toISOString(), announceActivity: act });
59469
59474
  if (!intake.config.private) await intake.publisher.recordOutbox(act);
59470
59475
  intake.store.setPending(intake.store.getPending().filter((p) => p.noteId !== noteId));
59471
59476
  intake.log(`amplified ${noteId} \u2192 ${inboxes.length} inbox(es)`);
59472
- if (intake.onCarried) {
59473
- await intake.onCarried({ noteId, actor: s.actor, activity: act, sent: activity }).catch((e) => intake.log(`after the carry of ${noteId}: ${e.message}`));
59474
- }
59475
59477
  await intake.bskyGroup?.mirrorCarry(s).catch((e) => intake.log(`bluesky mirror of the carry failed: ${e.message}`));
59476
59478
  }
59477
59479
  async function isCoMember(intake, actor) {
@@ -59479,11 +59481,11 @@ async function isCoMember(intake, actor) {
59479
59481
  const groups = intake.store.getContacts().following.filter((f) => f.accepted && intake.store.getActors()[f.actor]?.type === "Group").map((f) => f.actor);
59480
59482
  if (!groups.length) return false;
59481
59483
  const cache = intake.store.read("comembers.json", {});
59482
- const fresh = Date.now() - CO_MEMBER_TTL_MS;
59484
+ const fresh2 = Date.now() - CO_MEMBER_TTL_MS;
59483
59485
  let changed = false;
59484
59486
  for (const g of groups) {
59485
59487
  const held = cache[g];
59486
- if (held && Date.parse(held.at || 0) > fresh) continue;
59488
+ if (held && Date.parse(held.at || 0) > fresh2) continue;
59487
59489
  const doc = await intake.fetchAP(g).catch(() => null);
59488
59490
  const list3 = doc?.followers ? await intake.collectionMembers(doc.followers) : null;
59489
59491
  if (!list3) continue;
@@ -59518,6 +59520,56 @@ function announceTargets(intake, author) {
59518
59520
  return [...byTarget].filter(([, who]) => !(who.size === 1 && who.has(author))).map(([t]) => t);
59519
59521
  }
59520
59522
 
59523
+ // lib/core/intake/received.mjs
59524
+ init_wire();
59525
+ var PAGE_SIZE = FOLLOWERS_PAGE_SIZE;
59526
+ var SEEN_MAX = 500;
59527
+ var ITEM_MAX_CHARS = 32 * 1024;
59528
+ var HELD_MAX = PAGE_SIZE * 5;
59529
+ var RECEIVED = "received.json";
59530
+ var fresh = () => ({ sealed: 0, open: [], total: 0, seen: [] });
59531
+ var keeps = (intake) => !!intake.archive && !!intake.urls?.ownInbox && intake.store.getConfig?.()?.archiveInbox !== false;
59532
+ function recordReceived(intake, activity) {
59533
+ if (!keeps(intake) || !activity || typeof activity !== "object") return false;
59534
+ const st2 = { ...fresh(), ...intake.store.read(RECEIVED, {}) };
59535
+ const id = typeof activity.id === "string" ? activity.id : null;
59536
+ if (id && st2.seen.includes(id)) return false;
59537
+ st2.open.push(id && JSON.stringify(activity).length > ITEM_MAX_CHARS ? id : activity);
59538
+ if (st2.open.length > HELD_MAX) st2.open.splice(0, st2.open.length - HELD_MAX);
59539
+ st2.total += 1;
59540
+ if (id) {
59541
+ st2.seen.push(id);
59542
+ if (st2.seen.length > SEEN_MAX) st2.seen.splice(0, st2.seen.length - SEEN_MAX);
59543
+ }
59544
+ intake.store.write(RECEIVED, st2);
59545
+ intake._receivedDirty = true;
59546
+ return true;
59547
+ }
59548
+ async function publishReceived(intake) {
59549
+ if (!intake._receivedDirty || !keeps(intake)) return;
59550
+ const ready = intake.publisher?.privateReady;
59551
+ if (typeof ready === "function" && await ready.call(intake.publisher) !== true) return;
59552
+ const { remote, urls } = intake;
59553
+ const st2 = { ...fresh(), ...intake.store.read(RECEIVED, {}) };
59554
+ do {
59555
+ const n = st2.sealed + 1;
59556
+ const items = st2.open.slice(0, PAGE_SIZE);
59557
+ await writePage(
59558
+ remote,
59559
+ followersPageId(urls.ownInbox, n),
59560
+ followersPage(urls.ownInbox, n, items, n)
59561
+ );
59562
+ if (items.length < PAGE_SIZE) break;
59563
+ st2.sealed = n;
59564
+ st2.open = st2.open.slice(PAGE_SIZE);
59565
+ intake.store.write(RECEIVED, st2);
59566
+ } while (st2.open.length);
59567
+ const pages = Math.max(1, st2.sealed + (st2.open.length ? 1 : 0));
59568
+ await writeHead(remote, urls.ownInbox, followersHead(urls.ownInbox, st2.total, pages));
59569
+ intake.store.write(RECEIVED, st2);
59570
+ intake._receivedDirty = false;
59571
+ }
59572
+
59521
59573
  // lib/core/intake/activities.mjs
59522
59574
  var REACCEPT_MS = 60 * 60 * 1e3;
59523
59575
  var idOf = (v) => typeof v === "string" ? v : v?.id;
@@ -59534,7 +59586,7 @@ async function onFollow(intake, activity, actor, { trusted = false } = {}) {
59534
59586
  if (!doc.inbox) return `actor has no inbox (${actor})`;
59535
59587
  const contacts = intake.store.getContacts();
59536
59588
  const existing = contacts.followers.find((f) => f.actor === actor);
59537
- const unverifiedNeedsOk = intake.config.kind !== "group" && !intake.config.autoAcceptFollows && !trusted;
59589
+ const unverifiedNeedsOk = intake.config.kind !== "group" && intake.config.autoAcceptFollows === false && !trusted;
59538
59590
  const mustApprove = intake.config.approveJoins || unverifiedNeedsOk;
59539
59591
  if (mustApprove && !existing) {
59540
59592
  const reqs = intake.store.getRequests();
@@ -59702,6 +59754,7 @@ async function onUndoReaction(intake, inner, actor, { trusted = false } = {}) {
59702
59754
  const type = inner.type === "Like" ? "favourite" : "reblog";
59703
59755
  const gone = intake.store.removeNotifications((n) => n.type === type && n.actor === actor && n.noteId === objectId);
59704
59756
  if (gone) intake.log(`${inner.type} withdrawn by ${actor} on ${objectId}`);
59757
+ withdrawReaction(intake.store, inner.type, objectId, actor);
59705
59758
  return;
59706
59759
  }
59707
59760
  if (inner.type !== "Announce") return;
@@ -65369,6 +65422,7 @@ var Intake = class {
65369
65422
  this._draining = this._drainOnce().finally(async () => {
65370
65423
  this._inSweep = false;
65371
65424
  await this._publishPending();
65425
+ await publishCounts(this).catch((e) => this.log(`like and boost totals: ${e.message}`));
65372
65426
  this.store.release?.();
65373
65427
  this._draining = null;
65374
65428
  if (this._drainAgain) {
@@ -65610,6 +65664,7 @@ var Intake = class {
65610
65664
  if (pending.length >= DELETE_BATCH && !await flush()) return;
65611
65665
  }
65612
65666
  if (!await this._finishSweep(flush)) return;
65667
+ await publishReceived(this).catch((e) => this.log(`inbox collection: ${e.message}`));
65613
65668
  for (const stray of orphanReceipts(this.remote, this.urls).slice(0, ORPHAN_RECEIPTS_PER_SWEEP)) {
65614
65669
  if (!await dropStrayReceipt(this.remote, stray)) break;
65615
65670
  await new Promise((r) => setTimeout(r, DELETE_GAP_MS));
@@ -65640,7 +65695,10 @@ var Intake = class {
65640
65695
  if (activity && this.gatewaySecret()) this._bumpGatewayStat(!!receipt?.verified);
65641
65696
  const owned = activity && this.isOwnerPost(receipt);
65642
65697
  const rejection = !activity ? "unparsable JSON" : owned ? await this.ownerPostFrom(activity, raw, receipt) : await this.handle(activity, receipt);
65643
- if (!rejection && raw && !owned) await this._archive(url, raw, activity);
65698
+ if (!rejection && raw && !owned) {
65699
+ await this._archive(url, raw, activity);
65700
+ recordReceived(this, activity);
65701
+ }
65644
65702
  if (!rejection && !owned) await this._maybeForward(activity);
65645
65703
  if (rejection) {
65646
65704
  this.store.addDeadLetter({
@@ -65837,6 +65895,7 @@ var Intake = class {
65837
65895
  noteId: objectId,
65838
65896
  ...this.known(actor) ? {} : { unverified: true }
65839
65897
  });
65898
+ recordReaction(this.store, activity.type, objectId, actor);
65840
65899
  return;
65841
65900
  }
65842
65901
  if (activity.type === "Announce") return this.onAnnounce(activity, actor, objectId);
@@ -66388,6 +66447,7 @@ async function deleteNote(agent2, s) {
66388
66447
  };
66389
66448
  }
66390
66449
  await dropReplies(agent2.remote, repliesId(s.noteId));
66450
+ forgetCounts(agent2.store, s.noteId);
66391
66451
  if (s.atproto?.uri && agent2.atproto?.connected()) {
66392
66452
  await agent2.atproto.deleteCrossPost(s.atproto.uri).then(() => agent2.log?.(`bluesky mirror deleted: ${s.atproto.uri}`)).catch((e) => agent2.log?.(`bluesky mirror not deleted (${e.message}): ${s.atproto.uri}`));
66393
66453
  }
@@ -66495,7 +66555,6 @@ async function pinStatus(agent2, s, pinned) {
66495
66555
  // lib/client/c2s.mjs
66496
66556
  init_wire();
66497
66557
  var MAX_BODY = 512 * 1024;
66498
- var MAX_INBOX_PAGE = 500;
66499
66558
  var MAX_OUTBOX_PAGE = 50;
66500
66559
  var ACTIVITY_TYPES = /* @__PURE__ */ new Set([
66501
66560
  "Create",
@@ -66563,72 +66622,33 @@ var C2S = class {
66563
66622
  byIri(iri) {
66564
66623
  return iri ? this.store.getStatuses().find((s) => s.noteId === iri) : null;
66565
66624
  }
66566
- /** The months the archive holds, newest first. One container listing. */
66567
- async archiveMonths() {
66568
- const archive = this.agent.intake?.archive;
66569
- if (!archive) return [];
66570
- const { names } = await archive.list("");
66571
- return (names || []).map((n) => n.replace(/\/$/u, "")).filter((n) => /^\d{4}-\d{2}$/u.test(n)).sort().reverse();
66572
- }
66573
66625
  /**
66574
- * The owner's own inbox, §5.2. Paged by month because that is how the
66575
- * archive is stored, so a page costs one listing and a read per item and no
66576
- * page is dearer for another month being large.
66626
+ * The owner's own inbox, §5.2: the collection the drain keeps on the pod in
66627
+ * the private container (intake/received.mjs), served under this agent's
66628
+ * address so a client holding no pod credential of its own can read it.
66629
+ * Nothing is built per request; a page is the pod's page.
66577
66630
  */
66578
- async sendInbox(res, url) {
66579
- const id = `${this.urls.base}ap/inbox`;
66631
+ async sendInbox(res, url, origin) {
66632
+ const id = `${origin}ap/inbox`;
66633
+ const own = this.urls.ownInbox;
66580
66634
  const page2 = url?.searchParams?.get("page") || null;
66581
- let months;
66635
+ if (page2 !== null && !/^[1-9]\d{0,6}$/u.test(page2)) return this.send(res, 400, { error: "page is a number" });
66636
+ let doc;
66582
66637
  try {
66583
- months = await this.archiveMonths();
66638
+ doc = await this.agent.remote.getJson(page2 ? `${own}-${page2}` : own);
66584
66639
  } catch (e) {
66585
- return this.send(res, 502, { error: `the archive could not be read: ${e.message}` });
66640
+ return this.send(res, 502, { error: `the inbox collection could not be read: ${e.message}` });
66586
66641
  }
66587
- if (!page2) {
66588
- if (!months.length && this.store.getConfig()?.archiveInbox === false) {
66642
+ if (!doc) {
66643
+ if (page2) return this.send(res, 404, { error: "no such page" });
66644
+ if (this.store.getConfig()?.archiveInbox === false) {
66589
66645
  this.log("inbox read: nothing to show \u2014 this identity does not keep what it receives");
66590
66646
  }
66591
- return this.send(res, 200, {
66592
- "@context": AS_CTX,
66593
- id,
66594
- type: "OrderedCollection",
66595
- ...months.length ? { first: `${id}?page=${months[0]}` } : { orderedItems: [] }
66596
- });
66597
- }
66598
- if (!/^\d{4}-\d{2}$/u.test(page2)) {
66599
- return this.send(res, 400, { error: "page names a month, written 2026-09" });
66600
- }
66601
- const archive = this.agent.intake?.archive;
66602
- let names = [];
66603
- try {
66604
- ({ names } = await archive.list(`${page2}/`));
66605
- } catch (e) {
66606
- return this.send(res, 502, { error: `the archive could not be read: ${e.message}` });
66607
- }
66608
- const files = (names || []).filter((n) => n.endsWith(".json")).sort();
66609
- if (files.length > MAX_INBOX_PAGE) {
66610
- this.log(`inbox read: ${page2} holds ${files.length} items; serving the first ${MAX_INBOX_PAGE}`);
66611
- }
66612
- const kept = [];
66613
- for (const file of files.slice(0, MAX_INBOX_PAGE)) {
66614
- const read2 = await archive.read(`${page2}/${file}`, { accept: "*/*" });
66615
- if (!read2?.ok || !read2.body) continue;
66616
- try {
66617
- const record = JSON.parse(read2.body);
66618
- kept.push({ at: record.receivedAt || "", activity: JSON.parse(record.raw) });
66619
- } catch {
66620
- }
66647
+ return this.send(res, 200, { "@context": AS_CTX, id, type: "OrderedCollection", totalItems: 0, orderedItems: [] });
66621
66648
  }
66622
- kept.sort((a, b) => String(b.at).localeCompare(String(a.at)));
66623
- const older = months.filter((m) => m < page2)[0] || null;
66624
- return this.send(res, 200, {
66625
- "@context": AS_CTX,
66626
- id: `${id}?page=${page2}`,
66627
- type: "OrderedCollectionPage",
66628
- partOf: id,
66629
- ...older ? { next: `${id}?page=${older}` } : {},
66630
- orderedItems: kept.map((k) => k.activity)
66631
- });
66649
+ const here = (v) => typeof v === "string" && v.startsWith(own) ? v === own ? id : v.replace(`${own}-`, `${id}?page=`) : v;
66650
+ for (const k of ["id", "partOf", "first", "last", "next", "prev"]) if (k in doc) doc[k] = here(doc[k]);
66651
+ return this.send(res, 200, doc);
66632
66652
  }
66633
66653
  // The actor a client-to-server client on this machine reads. The same
66634
66654
  // document the pod holds, but for the fields naming where a write goes —
@@ -66719,7 +66739,7 @@ var C2S = class {
66719
66739
  }
66720
66740
  const reader = await this.auth(req, pathname);
66721
66741
  if (!reader.ok) return this.send(res, reader.status, { error: reader.error }, reader.headers || {});
66722
- return this.sendInbox(res, url);
66742
+ return this.sendInbox(res, url, this.localOrigin(req) || this.urls.base);
66723
66743
  }
66724
66744
  if (req.method === "GET" || req.method === "HEAD") {
66725
66745
  const owner = pathname === "/ap/outbox" && req.headers.authorization ? (await this.auth(req, pathname)).ok : false;
@@ -66739,7 +66759,7 @@ var C2S = class {
66739
66759
  }
66740
66760
  const who = await this.auth(req, pathname);
66741
66761
  if (!who.ok) return this.send(res, who.status, { error: who.error }, who.headers || {});
66742
- if (this.agent.viewer) {
66762
+ if (this.agent.viewer || this.agent.sharesState) {
66743
66763
  const took = await this.agent.requestTakeover?.();
66744
66764
  if (!took) return this.send(res, 503, { error: "another agent is active for this pod \u2014 takeover failed, try again" });
66745
66765
  }
@@ -68400,6 +68420,7 @@ function quoteApproval(api, s) {
68400
68420
  }
68401
68421
  function status(api, s, { all, depth = 0 } = {}) {
68402
68422
  const replies = (all || api.store.getStatuses()).filter((x) => x.inReplyTo === s.noteId).length;
68423
+ const counted = totalsFor(api.store, s.noteId);
68403
68424
  return {
68404
68425
  id: api.store.idFor(s.noteId),
68405
68426
  created_at: s.published || (/* @__PURE__ */ new Date()).toISOString(),
@@ -68413,8 +68434,8 @@ function status(api, s, { all, depth = 0 } = {}) {
68413
68434
  uri: s.noteId,
68414
68435
  url: s.link || s.noteId,
68415
68436
  replies_count: replies,
68416
- reblogs_count: 0,
68417
- favourites_count: 0,
68437
+ reblogs_count: counted.shares,
68438
+ favourites_count: counted.likes,
68418
68439
  // True when ANY of the owner's accounts holds it. The flag is really
68419
68440
  // what the next tap will do: an empty star on a post one account has
68420
68441
  // already liked invites a second outward like from a second identity.
@@ -69865,7 +69886,7 @@ var MastoApi = class _MastoApi {
69865
69886
  return send(403, { error: `This action is outside the authorized scopes (needs ${need})` });
69866
69887
  }
69867
69888
  if (!this.agent.configured()) return send(503, { error: "agent not configured" });
69868
- if ((this.agent.viewer || this.agent.copy) && req.method !== "GET" && req.method !== "HEAD") {
69889
+ if ((this.agent.viewer || this.agent.sharesState) && req.method !== "GET" && req.method !== "HEAD") {
69869
69890
  const took = await this.agent.requestTakeover?.();
69870
69891
  if (!took) return send(503, { error: "another agent is active for this pod \u2014 takeover failed, try again" });
69871
69892
  }
@@ -70432,6 +70453,10 @@ var Deliverer = class {
70432
70453
  // passive: signing-only (viewer-mode agents) — no queue drain timer, so a
70433
70454
  // read-only agent never mutates shared delivery state. startQueue() flips
70434
70455
  // it live when a viewer is promoted to active.
70456
+ // defer: nothing is sent now; every delivery goes on the queue, signed, for
70457
+ // drainQueue() to make. For a process with seconds to answer — a door
70458
+ // placing a post as it lands — which records what to send and leaves the
70459
+ // sending to the run it starts.
70435
70460
  constructor({
70436
70461
  store,
70437
70462
  keyId,
@@ -70441,9 +70466,11 @@ var Deliverer = class {
70441
70466
  proofKeyId = null,
70442
70467
  log: log2 = console.log,
70443
70468
  passive = false,
70469
+ defer = false,
70444
70470
  onGone = null
70445
70471
  }) {
70446
70472
  this.store = store;
70473
+ this.defer = defer;
70447
70474
  this.onGone = onGone;
70448
70475
  this.keyId = keyId;
70449
70476
  this.rsaPrivate = rsaPrivate;
@@ -70564,6 +70591,10 @@ var Deliverer = class {
70564
70591
  this._sent(activity);
70565
70592
  if (this._blockedInbox(inbox)) return;
70566
70593
  const signed = await this.proofed(activity);
70594
+ if (this.defer) {
70595
+ this._enqueue({ inbox, activity: signed, attempts: 0, nextAt: Date.now() });
70596
+ return;
70597
+ }
70567
70598
  if (this._queueIfCooling(inbox, signed)) return;
70568
70599
  const [result] = await this.deliverManyNow([{ inbox, activity: signed }]);
70569
70600
  await this._settle(inbox, signed, result);
@@ -70634,6 +70665,10 @@ var Deliverer = class {
70634
70665
  this._sent(activity);
70635
70666
  const signed = await this.proofed(activity);
70636
70667
  const targets = [...new Set(inboxes)].filter((inbox) => !this._blockedInbox(inbox)).map((inbox) => ({ inbox, activity: signed }));
70668
+ if (this.defer) {
70669
+ for (const t of targets) this._enqueue({ inbox: t.inbox, activity: signed, attempts: 0, nextAt: Date.now() });
70670
+ return;
70671
+ }
70637
70672
  for (let i = 0; i < targets.length; i += this.batchSize) {
70638
70673
  const chunk = targets.slice(i, i + this.batchSize).filter((t) => !this._queueIfCooling(t.inbox, signed));
70639
70674
  if (!chunk.length) continue;
@@ -71063,15 +71098,15 @@ var ImportWorker = class {
71063
71098
  if (next.kind === "follow" && Date.now() - this.lastFollowAt < FOLLOW_GAP_MS) return;
71064
71099
  if (next.kind === "domain") {
71065
71100
  const b = agent2.store.getBlocklist();
71066
- const fresh = this.state();
71067
- if (!fresh?.rows) return;
71068
- for (const r of fresh.rows) {
71101
+ const fresh2 = this.state();
71102
+ if (!fresh2?.rows) return;
71103
+ for (const r of fresh2.rows) {
71069
71104
  if (r.status !== "pending" || r.kind !== "domain") continue;
71070
71105
  if (!b.domains.includes(r.value)) b.domains.push(r.value);
71071
71106
  r.status = "done";
71072
71107
  }
71073
71108
  agent2.store.setBlocklist(b);
71074
- agent2.store.write(IMPORT_STATE_DOC, fresh);
71109
+ agent2.store.write(IMPORT_STATE_DOC, fresh2);
71075
71110
  return;
71076
71111
  }
71077
71112
  const outcome = await this.applyRow(next);
@@ -71348,7 +71383,7 @@ var AdminFacade = class {
71348
71383
  image: cfg.image || null,
71349
71384
  fields: cfg.fields || [],
71350
71385
  aliases: cfg.aliases || [],
71351
- autoAcceptFollows: !!cfg.autoAcceptFollows,
71386
+ autoAcceptFollows: cfg.autoAcceptFollows !== false,
71352
71387
  hasUiPassword: !!cfg.uiPassword,
71353
71388
  quiescedAt: cfg.quiescedAt || null,
71354
71389
  movedTo: cfg.movedTo || null,
@@ -71914,10 +71949,10 @@ async function completeGatewayMove(agent2) {
71914
71949
  }
71915
71950
 
71916
71951
  // stub:node:os
71917
- var fail2 = () => {
71952
+ var fail = () => {
71918
71953
  throw new Error("node:os is not available in the browser agent");
71919
71954
  };
71920
- var node_os_default = new Proxy({}, { get: () => fail2 });
71955
+ var node_os_default = new Proxy({}, { get: () => fail });
71921
71956
 
71922
71957
  // lib/shared/files.mjs
71923
71958
  function writeFileAtomic(file, body, { mode = 384 } = {}) {
@@ -73018,6 +73053,205 @@ var AcctFeed = class {
73018
73053
  // web/app/agent.mjs
73019
73054
  init_urls();
73020
73055
 
73056
+ // stub:node:fs/promises
73057
+ var fail2 = () => {
73058
+ throw new Error("node:fs/promises is not available in the browser agent");
73059
+ };
73060
+ var promises_default = new Proxy({}, { get: () => fail2 });
73061
+
73062
+ // lib/core/pod-only.mjs
73063
+ var podOnly = (name) => name === "keys.json" || name === "lease.json" || name.startsWith("conn-");
73064
+
73065
+ // lib/core/storage.mjs
73066
+ var LDP2 = Namespace("http://www.w3.org/ns/ldp#");
73067
+ var slash = (u) => u.endsWith("/") ? u : u + "/";
73068
+ var HttpStorage = class {
73069
+ constructor(base, fetchImpl) {
73070
+ this.base = slash(base);
73071
+ this.fetchImpl = fetchImpl;
73072
+ }
73073
+ get kind() {
73074
+ return "pod";
73075
+ }
73076
+ // The same jail FileStorage has, and for the same reason: a path here can
73077
+ // carry `..`, encodeURI leaves both `.` and `/` alone, and fetch normalises
73078
+ // the segments away — so a name derived from remote input could name a
73079
+ // resource outside the container entirely. A prefix check is not enough on
73080
+ // its own: a concatenated URL always satisfies one.
73081
+ _url(p) {
73082
+ const u = new URL(encodeURI(p), this.base);
73083
+ if (!u.href.startsWith(this.base)) throw new Error(`path escapes the container: ${p}`);
73084
+ return u.href;
73085
+ }
73086
+ async list(sub = "", { etag } = {}) {
73087
+ const url = this._url(sub);
73088
+ const res = await this.fetchImpl(url, {
73089
+ headers: { accept: "text/turtle", ...etag ? { "if-none-match": etag } : {} }
73090
+ });
73091
+ if (res.status === 304) return { notModified: true, names: null, etag };
73092
+ if (res.status === 404) return { notModified: false, names: [], etag: null, missing: true };
73093
+ if (res.status >= 400) throw new Error(`container unreadable (HTTP ${res.status})`);
73094
+ const g = graph();
73095
+ parse2(await res.text(), g, url, "text/turtle");
73096
+ const here = namedNode2(url);
73097
+ const names = g.each(here, LDP2("contains"), null, here).map((n) => n.value).filter((u) => u.startsWith(url) && u !== url).map((u) => decodeURIComponent(u.slice(url.length)));
73098
+ return { notModified: false, names, etag: res.headers.get("etag") };
73099
+ }
73100
+ // `accept` is for callers reading something that is RDF but is wanted as it
73101
+ // was written: asking turtle-first for a JSON-LD document gets turtle back,
73102
+ // because the server is entitled to convert between two RDF syntaxes.
73103
+ async read(p, { etag, accept } = {}) {
73104
+ const res = await this.fetchImpl(this._url(p), {
73105
+ headers: {
73106
+ accept: accept || "text/turtle, application/json;q=0.9, */*;q=0.8",
73107
+ ...etag ? { "if-none-match": etag } : {}
73108
+ }
73109
+ });
73110
+ if (res.status === 304) return { ok: true, notModified: true, status: 304, body: null, etag };
73111
+ if (res.status >= 400) return { ok: false, notModified: false, status: res.status, body: null, etag: null };
73112
+ return { ok: true, notModified: false, status: res.status, body: await res.text(), etag: res.headers.get("etag") };
73113
+ }
73114
+ async write(p, body, contentType) {
73115
+ const url = this._url(p);
73116
+ try {
73117
+ const res = await this.fetchImpl(url, {
73118
+ method: "PUT",
73119
+ headers: { "content-type": contentType },
73120
+ body
73121
+ });
73122
+ if (res.status < 400) return { ok: true, retry: false, why: "" };
73123
+ const retry = res.status >= 500 || res.status === 429;
73124
+ const ra = Number(res.headers?.get?.("retry-after"));
73125
+ return {
73126
+ ok: false,
73127
+ retry,
73128
+ why: `HTTP ${res.status}`,
73129
+ retryAfterMs: Number.isFinite(ra) && ra > 0 ? ra * 1e3 : 0
73130
+ };
73131
+ } catch (e) {
73132
+ return { ok: false, retry: true, why: e.message, retryAfterMs: 0 };
73133
+ }
73134
+ }
73135
+ async remove(p) {
73136
+ const url = this._url(p);
73137
+ try {
73138
+ const res = await this.fetchImpl(url, { method: "DELETE" });
73139
+ return res.status < 400 || res.status === 404;
73140
+ } catch {
73141
+ return false;
73142
+ }
73143
+ }
73144
+ };
73145
+ var StateApiStorage = class {
73146
+ constructor(base, { fetchImpl = globalThis.fetch, token, holder, pod = null, onRefused = null, onGone = null }) {
73147
+ this.base = slash(base);
73148
+ this.fetchImpl = fetchImpl;
73149
+ this.token = token;
73150
+ this.holder = holder;
73151
+ this.pod = pod;
73152
+ this.onRefused = onRefused;
73153
+ this.onGone = onGone;
73154
+ }
73155
+ get kind() {
73156
+ return "copy";
73157
+ }
73158
+ async _gone(res) {
73159
+ if (res.status !== 404 || !this.onGone) return false;
73160
+ const said = await res.clone().json().catch(() => null);
73161
+ if (!said?.gone) return false;
73162
+ this.onGone();
73163
+ return true;
73164
+ }
73165
+ _ask(p, init = {}) {
73166
+ return this.fetchImpl(this.base + encodeURIComponent(p), {
73167
+ ...init,
73168
+ headers: { authorization: `Bearer ${this.token}`, ...init.headers || {} }
73169
+ });
73170
+ }
73171
+ async list(sub = "", { etag } = {}) {
73172
+ if (sub) return { notModified: false, names: [], etag: null };
73173
+ const res = await this._ask("", { headers: etag ? { "if-none-match": etag } : {} });
73174
+ if (res.status === 304) return { notModified: true, names: null, etag };
73175
+ if (res.status >= 400) {
73176
+ await this._gone(res);
73177
+ throw new Error(`the account's copy at the gateway is unreadable (HTTP ${res.status})`);
73178
+ }
73179
+ const { names } = await res.json();
73180
+ return { notModified: false, names, etag: res.headers.get("etag") };
73181
+ }
73182
+ async read(p, opts = {}) {
73183
+ if (podOnly(p)) return this.pod ? this.pod.read(p, opts) : { ok: false, notModified: false, status: 404, body: null, etag: null };
73184
+ const res = await this._ask(p, { headers: opts.etag ? { "if-none-match": opts.etag } : {} });
73185
+ if (res.status === 304) return { ok: true, notModified: true, status: 304, body: null, etag: opts.etag };
73186
+ if (res.status >= 400) {
73187
+ await this._gone(res);
73188
+ return { ok: false, notModified: false, status: res.status, body: null, etag: null };
73189
+ }
73190
+ return { ok: true, notModified: false, status: res.status, body: await res.text(), etag: res.headers.get("etag") };
73191
+ }
73192
+ async write(p, body, contentType = "application/json") {
73193
+ if (podOnly(p)) return this.pod ? this.pod.write(p, body, contentType) : { ok: false, retry: false, why: "no pod for this document" };
73194
+ try {
73195
+ const res = await this._ask(p, { method: "PUT", headers: { "content-type": contentType, "x-fedipod-holder": this.holder }, body });
73196
+ if (res.status < 400) return { ok: true, retry: false, why: "" };
73197
+ if (res.status === 409) {
73198
+ this.onRefused?.();
73199
+ return { ok: false, retry: false, why: "another agent holds this account now", lost: true };
73200
+ }
73201
+ if (await this._gone(res)) return { ok: false, retry: false, why: "the gateway keeps no copy of this account now", lost: true };
73202
+ return { ok: false, retry: res.status >= 500 || res.status === 429, why: `HTTP ${res.status}`, retryAfterMs: 0 };
73203
+ } catch (e) {
73204
+ return { ok: false, retry: true, why: e.message, retryAfterMs: 0 };
73205
+ }
73206
+ }
73207
+ async remove(p) {
73208
+ if (podOnly(p)) return this.pod ? this.pod.remove(p) : false;
73209
+ try {
73210
+ const res = await this._ask(p, { method: "DELETE", headers: { "x-fedipod-holder": this.holder } });
73211
+ if (res.status === 409) {
73212
+ this.onRefused?.();
73213
+ return false;
73214
+ }
73215
+ if (await this._gone(res)) return false;
73216
+ return res.status < 400 || res.status === 404;
73217
+ } catch {
73218
+ return false;
73219
+ }
73220
+ }
73221
+ };
73222
+ var FencedStorage = class {
73223
+ constructor(inner, { fence = null, onRefused = null } = {}) {
73224
+ this.inner = inner;
73225
+ this.fence = fence;
73226
+ this.onRefused = onRefused;
73227
+ }
73228
+ get kind() {
73229
+ return this.inner.kind;
73230
+ }
73231
+ get base() {
73232
+ return this.inner.base;
73233
+ }
73234
+ list(sub, opts) {
73235
+ return this.inner.list(sub, opts);
73236
+ }
73237
+ read(p, opts) {
73238
+ return this.inner.read(p, opts);
73239
+ }
73240
+ async _refused() {
73241
+ if (!this.fence || await this.fence().catch(() => true)) return false;
73242
+ this.onRefused?.();
73243
+ return true;
73244
+ }
73245
+ async write(p, body, contentType) {
73246
+ if (await this._refused()) return { ok: false, retry: false, why: "another agent holds this account now", lost: true };
73247
+ return this.inner.write(p, body, contentType);
73248
+ }
73249
+ async remove(p) {
73250
+ if (await this._refused()) return false;
73251
+ return this.inner.remove(p);
73252
+ }
73253
+ };
73254
+
73021
73255
  // web/app/copy-mode.mjs
73022
73256
  var RENEW_BEFORE_MS = 2 * 36e5;
73023
73257
  var ASK_AGAIN_MS = 15 * 6e4;
@@ -73069,7 +73303,11 @@ function copyStorage(agent2, podState) {
73069
73303
  pod: podState,
73070
73304
  // Refused: another agent (an app at the gateway, another browser) took
73071
73305
  // the lease. This one stops acting at once rather than at its next renewal.
73072
- onRefused: () => standDown(agent2)
73306
+ onRefused: () => standDown(agent2),
73307
+ // No copy any more, and none to be made: the hold turned off.
73308
+ onGone: () => {
73309
+ backToPod(agent2).catch((e) => agent2.log(`back to the pod: ${e.message}`));
73310
+ }
73073
73311
  });
73074
73312
  Object.defineProperty(s, "token", { get: () => agent2.copy.token, set() {
73075
73313
  } });
@@ -73078,11 +73316,28 @@ function copyStorage(agent2, podState) {
73078
73316
  function copyLeaseOf(agent2) {
73079
73317
  return new Lease({ url: `${agent2.copy.base}lease.json`, fetchImpl: tokenFetch(agent2), log: agent2.log, id: agent2.holderId });
73080
73318
  }
73319
+ var podFetchOf = (agent2) => (u, i) => agent2.remote.fetch(u, i);
73320
+ function podStorageOf(agent2) {
73321
+ const s = new FencedStorage(new HttpStorage(agent2.urls.state, podFetchOf(agent2)), { onRefused: () => standDown(agent2) });
73322
+ if (agent2._fenced) s.fence = () => stillMine(agent2);
73323
+ return s;
73324
+ }
73325
+ async function stillMine(agent2) {
73326
+ const cur = await agent2.lease.readFresh().catch(() => null);
73327
+ if (!cur || typeof cur !== "object") return true;
73328
+ return cur.holder === agent2.lease.id && Date.now() < cur.expiresAt;
73329
+ }
73330
+ function fencePod(agent2) {
73331
+ agent2._fenced = !!(agent2._keeper?.kept && agent2._keeper.hold === false && !agent2.copy && !agent2.store?.getConfig()?.keeperOff);
73332
+ const s = agent2.store?.storage;
73333
+ if (s instanceof FencedStorage) s.fence = agent2._fenced ? () => stillMine(agent2) : null;
73334
+ }
73335
+ var byGateway = (holder) => holder === "gateway" || String(holder || "").startsWith("keeper:");
73081
73336
  function useLease(agent2, lease) {
73082
73337
  agent2.lease.stopRenewal();
73083
73338
  agent2.lease = lease;
73084
73339
  if (agent2.intake) agent2.intake.lease = lease;
73085
- lease.onLost = () => agent2.demote();
73340
+ lease.onLost = () => agent2.copy || agent2._fenced ? standDown(agent2) : agent2.demote();
73086
73341
  }
73087
73342
  async function ensureCopyLease(agent2) {
73088
73343
  const cur = await agent2.lease.readFresh().catch(() => null);
@@ -73117,7 +73372,7 @@ function standDown(agent2) {
73117
73372
  await new Promise((r) => setTimeout(r, wait));
73118
73373
  if (!agent2.viewer) return;
73119
73374
  const cur = await agent2.lease.readFresh().catch(() => null);
73120
- if (cur && typeof cur === "object" && cur.holder !== "gateway" && Date.now() < cur.expiresAt) return;
73375
+ if (cur && typeof cur === "object" && !byGateway(cur.holder) && Date.now() < cur.expiresAt) return;
73121
73376
  if (await agent2.lease.acquire()) {
73122
73377
  clearTimeout(agent2._viewerTimer);
73123
73378
  agent2._viewerTimer = null;
@@ -73165,21 +73420,40 @@ async function leaveCopy(agent2) {
73165
73420
  return { ok: false, why: `the gateway could not write the copy to the pod (${res.status || res.e?.message})` };
73166
73421
  }
73167
73422
  agent2.copy = null;
73168
- const podFetch = (u, i) => agent2.remote.fetch(u, i);
73169
- const lease = new Lease({ url: `${agent2.urls.state}lease.json`, fetchImpl: podFetch, log: agent2.log, id: agent2.holderId });
73170
- agent2.store.attach(new HttpStorage(agent2.urls.state, podFetch));
73171
- await agent2.store.load({ force: true });
73423
+ const lease = new Lease({ url: `${agent2.urls.state}lease.json`, fetchImpl: podFetchOf(agent2), log: agent2.log, id: agent2.holderId });
73424
+ agent2.store.attach(podStorageOf(agent2));
73172
73425
  useLease(agent2, lease);
73426
+ fencePod(agent2);
73427
+ await agent2.store.load({ force: true });
73173
73428
  if (await lease.acquire()) lease.startRenewal();
73174
73429
  else agent2.demote();
73175
73430
  agent2.log("working from the pod again");
73176
73431
  return { ok: true };
73177
73432
  }
73433
+ async function backToPod(agent2) {
73434
+ if (!agent2.copy || agent2._backToPod) return;
73435
+ agent2._backToPod = true;
73436
+ try {
73437
+ await agent2.store.discardPending();
73438
+ agent2.lease.stopRenewal();
73439
+ agent2.copy = null;
73440
+ const lease = new Lease({ url: `${agent2.urls.state}lease.json`, fetchImpl: podFetchOf(agent2), log: agent2.log, id: agent2.holderId });
73441
+ agent2.store.attach(podStorageOf(agent2));
73442
+ useLease(agent2, lease);
73443
+ fencePod(agent2);
73444
+ await agent2.store.load({ force: true });
73445
+ if (await lease.acquire()) lease.startRenewal();
73446
+ else agent2.demote();
73447
+ agent2.log("the gateway keeps no copy of this account now: working from the pod");
73448
+ } finally {
73449
+ agent2._backToPod = false;
73450
+ }
73451
+ }
73178
73452
  async function handOverCopy(agent2) {
73179
73453
  if (!agent2.copy) return { ok: true };
73180
73454
  await agent2.store.commit();
73181
- const podFetch = (u, i) => agent2.remote.fetch(u, i);
73182
- const pod = new HttpStorage(agent2.urls.state, podFetch);
73455
+ const podFetch = podFetchOf(agent2);
73456
+ const pod = podStorageOf(agent2);
73183
73457
  const docs = agent2.store.names().filter((n) => !podOnly(n)).map((n) => [n, agent2.store.read(n, null)]).filter(([, v]) => v !== null);
73184
73458
  for (const [name, value] of docs) {
73185
73459
  const w = await pod.write(name, JSON.stringify(value, null, 2) + "\n", "application/json");
@@ -73191,16 +73465,30 @@ async function handOverCopy(agent2) {
73191
73465
  if (res.status !== 200) return { ok: false, why: `the gateway would not let the copy go (${res.status || res.e?.message})` };
73192
73466
  agent2.copy = null;
73193
73467
  agent2.store.attach(pod);
73194
- await agent2.store.load({ force: true });
73195
73468
  useLease(agent2, podLease);
73469
+ fencePod(agent2);
73470
+ await agent2.store.load({ force: true });
73196
73471
  podLease.startRenewal();
73197
73472
  agent2.log(`handed the account's copy over: ${docs.length} documents written to the pod`);
73198
73473
  return { ok: true };
73199
73474
  }
73475
+ async function followHold(agent2, frontOrigin, hold) {
73476
+ if (!agent2._keeper) return;
73477
+ const was = agent2._keeper.hold;
73478
+ agent2._keeper.hold = hold;
73479
+ if (!hold && agent2.copy) {
73480
+ const left = await leaveCopy(agent2);
73481
+ if (!left.ok) agent2.log(`the gateway keeps no copies now; leaving this one: ${left.why}`);
73482
+ } else if (hold && was === false && !agent2.copy && agent2._keeper.kept && !agent2.viewer && !agent2.store.getConfig()?.keeperOff) {
73483
+ await forgetFailedOpen(agent2, frontOrigin);
73484
+ await moveIntoCopy(agent2, frontOrigin);
73485
+ }
73486
+ fencePod(agent2);
73487
+ }
73200
73488
  async function renewCopyToken(agent2, frontOrigin) {
73201
73489
  if (!agent2.copy || agent2.copy.expiresAt - Date.now() > RENEW_BEFORE_MS) return;
73202
- const fresh = await openCopy(agent2, frontOrigin, { handle: agent2.copy.handle });
73203
- if (fresh) agent2.copy = fresh;
73490
+ const fresh2 = await openCopy(agent2, frontOrigin, { handle: agent2.copy.handle });
73491
+ if (fresh2) agent2.copy = fresh2;
73204
73492
  }
73205
73493
 
73206
73494
  // web/app/agent.mjs
@@ -73273,11 +73561,17 @@ var BrowserAgent = class _BrowserAgent {
73273
73561
  configured() {
73274
73562
  return !!this.store?.getConfig();
73275
73563
  }
73564
+ // Whether the gateway acts on this account's state too: its copy there, or
73565
+ // with the gateway's hold off, its pod (copy-mode.mjs). An action here then
73566
+ // makes sure of the lease first (requestTakeover).
73567
+ get sharesState() {
73568
+ return !!(this.copy || this._fenced);
73569
+ }
73276
73570
  // Asked by every write / destructive path before it acts. If we already hold
73277
73571
  // the lease, proceed. If we are a viewer, the owner acting HERE outranks the
73278
73572
  // idle active device: claim the lease outright and become active.
73279
73573
  async requestTakeover() {
73280
- if (!this.viewer && this.copy) await ensureCopyLease(this);
73574
+ if (!this.viewer && this.sharesState) await ensureCopyLease(this);
73281
73575
  if (!this.viewer) return true;
73282
73576
  if (!await this.lease.takeover()) return false;
73283
73577
  clearTimeout(this._viewerTimer);
@@ -73333,6 +73627,7 @@ var BrowserAgent = class _BrowserAgent {
73333
73627
  const said2 = await this.tellGateway("keeper", { handle: this.doorKey, on: false });
73334
73628
  if (said2?.status !== 200) return said2 || { status: 502 };
73335
73629
  this._keeper.kept = false;
73630
+ fencePod(this);
73336
73631
  if (this.masto) this.masto.scheduling = false;
73337
73632
  }
73338
73633
  this.remote.keepers = on ? [webId] : [];
@@ -73344,6 +73639,7 @@ var BrowserAgent = class _BrowserAgent {
73344
73639
  if (said?.status === 200) {
73345
73640
  await forgetFailedOpen(this, this.frontOrigin);
73346
73641
  this._keeper.kept = true;
73642
+ fencePod(this);
73347
73643
  if (this.masto) this.masto.scheduling = true;
73348
73644
  this.log("the gateway keeps this account running while the app is closed");
73349
73645
  }
@@ -73363,6 +73659,9 @@ var BrowserAgent = class _BrowserAgent {
73363
73659
  const said = await this.tellGateway("here", { handle: this.doorKey, nextAt: this.store ? nextDue(this.store) : null });
73364
73660
  if (said?.status === 200) saveMeta(this.webId, { hereAt: Date.now() }).catch(() => {
73365
73661
  });
73662
+ if (said?.status === 200 && typeof said.hold === "boolean") {
73663
+ await followHold(this, this.frontOrigin, said.hold).catch((e) => this.log(`the gateway's hold: ${e.message}`));
73664
+ }
73366
73665
  return said;
73367
73666
  }
73368
73667
  // The hourly "still here". A worker the browser killed and restarted is the
@@ -73397,7 +73696,7 @@ var BrowserAgent = class _BrowserAgent {
73397
73696
  this._openTimer = setInterval(() => {
73398
73697
  this.checkInAtGateway();
73399
73698
  }, _BrowserAgent.OPEN_EVERY_MS);
73400
- this.lease.onLost = () => this.copy ? standDown(this) : this.demote();
73699
+ this.lease.onLost = () => this.sharesState ? standDown(this) : this.demote();
73401
73700
  this.lease.startRenewal();
73402
73701
  try {
73403
73702
  if (!warm) {
@@ -73568,7 +73867,7 @@ var BrowserAgent = class _BrowserAgent {
73568
73867
  ).catch(() => null))?.root || DEFAULT_ROOT;
73569
73868
  this.urls = apUrls2(remotePod, root);
73570
73869
  const podFetch = (u, i) => this.remote.fetch(u, i);
73571
- const podStateStorage = new HttpStorage(this.urls.state, podFetch);
73870
+ const podStateStorage = podStorageOf(this);
73572
73871
  this.store = new PodStore({ storage: this.copy ? copyStorage(this, podStateStorage) : podStateStorage, log: this.log });
73573
73872
  keepInStep(this.store, webId, this.log);
73574
73873
  this.lease = this.copy ? copyLeaseOf(this) : new Lease({ url: this.urls.state + "lease.json", fetchImpl: podFetch, log: this.log, id: this.holderId });
@@ -73626,8 +73925,9 @@ var BrowserAgent = class _BrowserAgent {
73626
73925
  e.code = "address-closed";
73627
73926
  throw e;
73628
73927
  }
73629
- this._keeper = standing?.keeper ? { webId: standing.keeper, kept: !!standing.kept, keptBy: standing.keptBy || null } : null;
73928
+ this._keeper = standing?.keeper ? { webId: standing.keeper, kept: !!standing.kept, keptBy: standing.keptBy || null, hold: standing.hold !== false } : null;
73630
73929
  if (this._keeper && !this.store.getConfig()?.keeperOff) this.remote.keepers = [this._keeper.webId];
73930
+ fencePod(this);
73631
73931
  this.publisher = new Publisher({
73632
73932
  config: this.store.getConfig(),
73633
73933
  remote: this.remote,