fedipod 1.49.2 → 1.50.2

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.
@@ -45791,6 +45791,7 @@ var PodStore = class {
45791
45791
  this._held = 0;
45792
45792
  this.chain = Promise.resolve();
45793
45793
  this.verdicts = /* @__PURE__ */ new Map();
45794
+ this.unread = /* @__PURE__ */ new Set();
45794
45795
  this.generation = 0;
45795
45796
  this.onLoaded = null;
45796
45797
  this.onWriting = null;
@@ -45804,6 +45805,7 @@ var PodStore = class {
45804
45805
  this.cache.clear();
45805
45806
  this.etags.clear();
45806
45807
  this.lastText.clear();
45808
+ this.unread.clear();
45807
45809
  }
45808
45810
  this.storage = storage;
45809
45811
  }
@@ -45826,8 +45828,8 @@ var PodStore = class {
45826
45828
  if (listing.missing && this.storage.kind === "pod") {
45827
45829
  throw new Error(`state container missing (HTTP 404) at ${this.base} \u2014 pod unreachable or state gone`);
45828
45830
  }
45829
- this.etags.set("", listing.etag);
45830
45831
  const names = listing.names.filter((n) => n.endsWith(".json"));
45832
+ for (const name of this.unread) if (!names.includes(name)) this.unread.delete(name);
45831
45833
  let fetched = 0;
45832
45834
  const skipped = [];
45833
45835
  for (const name of names) {
@@ -45838,10 +45840,12 @@ var PodStore = class {
45838
45840
  if (name === "config.json" && !this.cache.has(name)) {
45839
45841
  throw new Error(`state doc ${name} unreadable (HTTP ${r.status})`);
45840
45842
  }
45843
+ if (!this.lastText.has(name)) this.unread.add(name);
45841
45844
  skipped.push(`${name} (HTTP ${r.status})`);
45842
45845
  continue;
45843
45846
  }
45844
45847
  fetched++;
45848
+ this.unread.delete(name);
45845
45849
  this.etags.set(name, r.etag);
45846
45850
  try {
45847
45851
  this.cache.set(name, JSON.parse(r.body));
@@ -45851,7 +45855,9 @@ var PodStore = class {
45851
45855
  }
45852
45856
  }
45853
45857
  this.lastSkipped = skipped;
45854
- if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(", ")}`);
45858
+ if (skipped.length) this.etags.delete("");
45859
+ else this.etags.set("", listing.etag);
45860
+ if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(", ")}` + (this.unread.size ? ` \u2014 not written until read: ${[...this.unread].join(", ")}` : ""));
45855
45861
  this.log(`state loaded: ${this.cache.size} doc(s) from ${this.base} (${fetched} re-fetched)`);
45856
45862
  this.onLoaded?.();
45857
45863
  }
@@ -45866,6 +45872,7 @@ var PodStore = class {
45866
45872
  continue;
45867
45873
  }
45868
45874
  this.lastText.set(name, text);
45875
+ this.unread.delete(name);
45869
45876
  if (etag) this.etags.set(name, etag);
45870
45877
  }
45871
45878
  if (listingEtag) this.etags.set("", listingEtag);
@@ -45884,12 +45891,14 @@ var PodStore = class {
45884
45891
  this.cache.delete(name);
45885
45892
  this.lastText.delete(name);
45886
45893
  this.etags.delete(name);
45894
+ this.unread.delete(name);
45887
45895
  } else if (!r.ok) {
45888
45896
  throw new Error(`state doc ${name} unreadable (HTTP ${r.status})`);
45889
45897
  } else {
45890
45898
  this.cache.set(name, JSON.parse(r.body));
45891
45899
  this.lastText.set(name, r.body);
45892
45900
  this.etags.set(name, r.etag);
45901
+ this.unread.delete(name);
45893
45902
  }
45894
45903
  this.onSettled?.(name);
45895
45904
  }
@@ -45908,6 +45917,10 @@ var PodStore = class {
45908
45917
  write(name, obj) {
45909
45918
  this.cache.set(name, structuredClone(obj));
45910
45919
  if (!this.storage) return;
45920
+ if (this.unread.has(name)) {
45921
+ this.verdicts.set(name, false);
45922
+ return;
45923
+ }
45911
45924
  if (this.lastText.get(name) === serialise(obj)) {
45912
45925
  clearTimeout(this.timers.get(name));
45913
45926
  this.timers.delete(name);
@@ -45957,6 +45970,7 @@ var PodStore = class {
45957
45970
  // the returned promise so one failure cannot poison the queue.
45958
45971
  _put(name) {
45959
45972
  const done = this.chain.then(async () => {
45973
+ if (this.unread.has(name)) return false;
45960
45974
  const body = serialise(this.cache.get(name));
45961
45975
  await Promise.resolve(this.onWriting?.(name, body)).catch(() => {
45962
45976
  });
@@ -46011,6 +46025,8 @@ var PodStore = class {
46011
46025
  // `since`: the generation the caller's work began in. Anything dropped since
46012
46026
  // makes the answer false.
46013
46027
  async commit({ since } = {}) {
46028
+ if (this.unread.size && this.storage) await this.refresh([...this.unread]).catch(() => {
46029
+ });
46014
46030
  const pending = [];
46015
46031
  for (const name of /* @__PURE__ */ new Set([...this.timers.keys(), ...this.dirty])) {
46016
46032
  const t = this.timers.get(name);
@@ -46164,6 +46180,15 @@ var PodStore = class {
46164
46180
  getStatuses() {
46165
46181
  return this.read("statuses.json", []);
46166
46182
  }
46183
+ // When each open poll closes, read in place: asked every 30 seconds, where
46184
+ // a copy of the whole timeline each time was the whole cost of asking.
46185
+ openPollEnds() {
46186
+ const out = [];
46187
+ for (const s of this.cache.get("statuses.json") || []) {
46188
+ if (s?.kind === "post" && s.poll && !s.poll.closed && s.poll.expiresAt) out.push(Date.parse(s.poll.expiresAt));
46189
+ }
46190
+ return out;
46191
+ }
46167
46192
  // A count must not pay for a clone: nodeinfo and the instance document are
46168
46193
  // public and answer strangers, so they read the cache directly.
46169
46194
  countStatuses(kind = null) {
@@ -46205,6 +46230,9 @@ var PodStore = class {
46205
46230
  const all = this.getStatuses();
46206
46231
  const i = all.findIndex((x) => x.noteId === noteId);
46207
46232
  if (i < 0) return null;
46233
+ if (typeof patch?.content === "string" && patch.content.length > MAX_CONTENT) {
46234
+ patch = { ...patch, content: clamp(patch.content, MAX_CONTENT), truncated: true };
46235
+ }
46208
46236
  all[i] = { ...all[i], ...patch };
46209
46237
  this.write("statuses.json", all);
46210
46238
  return all[i];
@@ -57157,6 +57185,7 @@ function fromLiteral(term3) {
57157
57185
  }
57158
57186
  return term3.value;
57159
57187
  }
57188
+ var isTagged = (term3) => term3.termType === "Literal" && !!term3.language;
57160
57189
  function toValue(term3, ctx, depth, path) {
57161
57190
  if (term3.termType === "Literal") return fromLiteral(term3);
57162
57191
  if (isListHead(term3, ctx)) return readList(term3.value, ctx, depth, path);
@@ -57175,8 +57204,12 @@ function readProperty(subject, name, ctx, depth = 0, path = /* @__PURE__ */ new
57175
57204
  }
57176
57205
  const term3 = TERMS.get(name);
57177
57206
  if (!term3) return void 0;
57178
- const values = preds.get(term3.iri);
57207
+ let values = preds.get(term3.iri);
57179
57208
  if (!values || !values.length) return void 0;
57209
+ if (values.some(isTagged)) {
57210
+ const plain = values.filter((v) => !isTagged(v));
57211
+ values = plain.length ? plain : [values.find(isTagged)];
57212
+ }
57180
57213
  const listed = values.filter((v) => isListHead(v, ctx));
57181
57214
  if (term3.list) {
57182
57215
  if (!listed.length) return void 0;
@@ -57691,7 +57724,9 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
57691
57724
  reply?.inbox,
57692
57725
  ...addressedInboxes
57693
57726
  ].filter(Boolean))];
57694
- await publisher.deliverer.deliverToAll(inboxes, create);
57727
+ const fanout = publisher.deliverer.deliverToAll(inboxes, create);
57728
+ if (publisher.background) fanout.catch((e) => publisher.log(`delivering ${note.id}: ${e.message}`));
57729
+ else await fanout;
57695
57730
  publisher.log(`note published: ${note.id} \u2192 ${inboxes.length} inbox(es)`);
57696
57731
  if (request) {
57697
57732
  const [inbox] = await inboxesFor(publisher, [quote.actor]);
@@ -58090,6 +58125,7 @@ async function rebuildStatuses(publisher, { fromNotes = false } = {}) {
58090
58125
  }
58091
58126
  async function healStatuses(publisher) {
58092
58127
  const { urls, store } = publisher;
58128
+ if (store.unread?.has("statuses.json")) return { missing: 0, recovered: 0 };
58093
58129
  const own = store.read("outbox.json", []).map((i) => typeof i === "string" ? i : null).filter((id) => id && id.startsWith(urls.notes));
58094
58130
  if (!own.length) return { missing: 0, recovered: 0 };
58095
58131
  const have = new Set(store.getStatuses().map((s) => s.noteId));
@@ -58502,7 +58538,8 @@ var Publisher = class {
58502
58538
  probeFetch = null,
58503
58539
  resolveMention = null,
58504
58540
  resolveActor = null,
58505
- clientOrigin = null
58541
+ clientOrigin = null,
58542
+ background = false
58506
58543
  }) {
58507
58544
  this.config = config;
58508
58545
  this.remote = remote;
@@ -58512,6 +58549,7 @@ var Publisher = class {
58512
58549
  this.publicKeyPem = publicKeyPem;
58513
58550
  this.assertionKey = assertionKey;
58514
58551
  this.clientOrigin = clientOrigin;
58552
+ this.background = background;
58515
58553
  const publicBase = config.gateway?.frontActor ? config.gateway.frontActor.replace(/ap\/actor\/?$/, "") : null;
58516
58554
  this.urls = apUrls2(config.remotePod, config.root, { publicBase });
58517
58555
  if (this.urls.toPod && this.remote?.setUrlMap) this.remote.setUrlMap(this.urls.toPod);
@@ -65361,6 +65399,7 @@ function describeShapeFailure(failure) {
65361
65399
  // lib/core/intake/index.mjs
65362
65400
  var POLL_MS = 2 * 6e4;
65363
65401
  var POLL_PUSH_OK_MS = 10 * 6e4;
65402
+ var POLL_IN_PROCESS_MS = 60 * 6e4;
65364
65403
  var DRAIN_COOLDOWN_MIN_MS = 2 * 6e4;
65365
65404
  var DRAIN_COOLDOWN_MAX_MS = 30 * 6e4;
65366
65405
  var DELETE_GAP_MS = 150;
@@ -65417,13 +65456,15 @@ var Intake = class {
65417
65456
  // what makes the fallback poll a slow one.
65418
65457
  _pollMs() {
65419
65458
  if (this.pollSeconds) return this.pollSeconds * 1e3;
65420
- return this.wsState === "open" || this.wsState === "in-process" ? POLL_PUSH_OK_MS : POLL_MS;
65459
+ if (this.wsState === "in-process") return POLL_IN_PROCESS_MS;
65460
+ return this.wsState === "open" ? POLL_PUSH_OK_MS : POLL_MS;
65421
65461
  }
65422
65462
  // `drainNow: false` and `subscribe: false` are for a browser worker the
65423
65463
  // browser restarted: it drained moments ago, and a socket would die with it.
65424
65464
  async start({ drainNow = true, subscribe: subscribe2 = true } = {}) {
65425
65465
  this.stopped = false;
65426
65466
  if (drainNow) await this.drain().catch((e) => this.log(`drain: ${e.message}`));
65467
+ if (!this.push) this.wsState = "in-process";
65427
65468
  const tick = () => {
65428
65469
  this.pollTimer = setTimeout(() => {
65429
65470
  this.drain().catch((e) => this.log(`drain: ${e.message}`)).finally(() => {
@@ -65433,8 +65474,7 @@ var Intake = class {
65433
65474
  this.pollTimer.unref?.();
65434
65475
  };
65435
65476
  tick();
65436
- if (!this.push) this.wsState = "in-process";
65437
- else if (subscribe2) this.subscribe().catch((e) => this.log(`subscribe: ${e.message}`));
65477
+ if (this.push && subscribe2) this.subscribe().catch((e) => this.log(`subscribe: ${e.message}`));
65438
65478
  }
65439
65479
  stop() {
65440
65480
  this.stopped = true;
@@ -67324,10 +67364,11 @@ var web_push_default = { generateVAPIDKeys, setVapidDetails, sendNotification };
67324
67364
  // lib/client/webpush.mjs
67325
67365
  var Push = class {
67326
67366
  constructor({ store, subject, log: log2 = () => {
67327
- } }) {
67367
+ }, liveKeys = null }) {
67328
67368
  this.store = store;
67329
67369
  this.subject = subject;
67330
67370
  this.log = log2;
67371
+ this.liveKeys = liveKeys;
67331
67372
  }
67332
67373
  state() {
67333
67374
  return this.store.read("webpush.json", { subs: {} });
@@ -67387,14 +67428,27 @@ var Push = class {
67387
67428
  }
67388
67429
  // Push one notification to every subscription whose alerts allow the type.
67389
67430
  // A push service answering 404/410 means the browser dropped the
67390
- // subscription — it is removed rather than retried forever.
67431
+ // subscription — it is removed rather than retried forever. With
67432
+ // `liveKeys`, every subscription whose sign-in is no longer in force is
67433
+ // forgotten first: an app signed out, expired or pushed off the list hears
67434
+ // nothing more.
67391
67435
  async notify(n, payload) {
67392
67436
  const st2 = this.state();
67437
+ let dropped = false;
67438
+ const live = this.liveKeys?.() || null;
67439
+ if (live) {
67440
+ for (const key of Object.keys(st2.subs)) if (!live.has(key)) {
67441
+ delete st2.subs[key];
67442
+ dropped = true;
67443
+ }
67444
+ }
67393
67445
  const entries = Object.entries(st2.subs);
67394
- if (!entries.length) return;
67446
+ if (!entries.length) {
67447
+ if (dropped) this.save(st2);
67448
+ return;
67449
+ }
67395
67450
  const { publicKey, privateKey } = this.vapid();
67396
67451
  const details = { subject: this.subject(), publicKey, privateKey };
67397
- let dropped = false;
67398
67452
  for (const [key, sub] of entries) {
67399
67453
  if (sub.alerts && sub.alerts[n.type] === false) continue;
67400
67454
  try {
@@ -67545,103 +67599,54 @@ function readMultipart(req, limit = 12e6) {
67545
67599
 
67546
67600
  // lib/client/masto/oauth.mjs
67547
67601
  var TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
67548
- var AUTHZ_WINDOW_MS = 6e4;
67549
- var AUTHZ_MAX_ATTEMPTS = 5;
67550
67602
  var CODE_TTL_MS = 5 * 6e4;
67551
67603
  var MAX_APPS = 200;
67604
+ var MAX_REDIRECTS = 10;
67605
+ var MAX_REDIRECT_CHARS = 2048;
67606
+ var REGISTRATION_WINDOW_MS = 6e4;
67607
+ var MAX_REGISTRATIONS = 10;
67552
67608
  var CLIENT_DOC_TTL_MS = 10 * 6e4;
67553
67609
  var CLIENT_DOC_MAX_CACHED = 200;
67554
67610
  var CLIENT_DOC_WINDOW_MS = 6e4;
67555
67611
  var CLIENT_DOC_MAX_FETCHES = 20;
67556
67612
  var CLIENT_DOC_MAX = 64 * 1024;
67557
- function hashPassword(password) {
67558
- const salt = node_crypto_default.randomBytes(16);
67559
- const hash = node_crypto_default.scryptSync(String(password), salt, 32);
67560
- return { saltHex: salt.toString("hex"), hashHex: hash.toString("hex") };
67561
- }
67562
- function checkPassword(rec, password) {
67563
- try {
67564
- const hash = node_crypto_default.scryptSync(String(password), Buffer.from(rec.saltHex, "hex"), 32);
67565
- return node_crypto_default.timingSafeEqual(hash, Buffer.from(rec.hashHex, "hex"));
67566
- } catch {
67567
- return false;
67568
- }
67569
- }
67570
67613
  var escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
67571
67614
  var parseRedirects = (v) => (Array.isArray(v) ? v : String(v || "").split(/\s+/)).map((s) => s.trim()).filter(Boolean);
67572
- function sendPodSigninPage(res, client, webId, mount = "") {
67573
- let asking = "";
67574
- if (client && (client.name || client.redirect)) {
67575
- let where = "";
67576
- try {
67577
- where = client.redirect ? new URL(client.redirect).host : "";
67578
- } catch {
67579
- }
67580
- const who = client.name ? escapeHtml(client.name) : where ? escapeHtml(where) : "A client";
67581
- asking = `<p><strong>${who}</strong> is asking to use your account${where ? `, and you will be sent back to <code>${escapeHtml(where)}</code>` : ""}.</p>`;
67615
+ var ownAddress = (api) => {
67616
+ const handle7 = api.store.getConfig()?.handle;
67617
+ return handle7 && api.host ? `@${handle7}@${api.host}` : null;
67618
+ };
67619
+ function sendPodSigninPage(res, client, webId, mount = "", address = null) {
67620
+ let where = "";
67621
+ try {
67622
+ where = client?.redirect ? new URL(client.redirect).host : "";
67623
+ } catch {
67582
67624
  }
67625
+ const app = escapeHtml(client?.name || where || "this app");
67626
+ const question = `Allow ${app} to access your ${address ? `${escapeHtml(address)} ` : ""}account?`;
67583
67627
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" });
67584
67628
  res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
67585
- <title>Sign in with your pod</title>
67629
+ <title>${question}</title>
67586
67630
  <style>:root{color-scheme:light dark;--heading:#1a4f8a;--btn:#3d6b35;--line:#9a9a9a}
67587
67631
  body{font:20px Arial,Helvetica,sans-serif;max-width:28rem;margin:12vh auto;padding:0 1rem}
67588
67632
  h1{color:var(--heading);font-size:1.5rem}
67589
- code{word-break:break-all;background:rgba(120,120,120,.12);padding:.05em .35em;border-radius:4px}
67633
+ h1,p{overflow-wrap:anywhere}
67590
67634
  button{font:inherit;padding:.6rem 1.1rem;background:var(--btn);color:#fff;border:none;border-radius:9px;cursor:pointer}
67591
67635
  button[hidden]{display:none}
67636
+ #cancel{background:transparent;color:inherit;border:2px solid currentColor;margin-left:.6rem}
67592
67637
  #signin-status{min-height:1.5em;padding:.6rem .8rem;border:1px solid var(--line);border-radius:8px;margin-top:1rem}
67593
- @media (prefers-color-scheme:dark){:root{--heading:#7fb3e8;--btn:#5a8a4f}}</style></head><body>
67638
+ #signin-status:empty{min-height:0;padding:0;border:0;margin:0}
67639
+ @media (prefers-color-scheme:dark){:root{--heading:#7fb3e8;--btn:#4a7a40}}</style></head><body>
67594
67640
  <main>
67595
- <h1>Sign in with your pod</h1>
67596
- ${asking}
67597
- <p>This account belongs to <a id="webid" href="${escapeHtml(webId)}">${escapeHtml(webId)}</a>. Sign in at that pod to allow it.</p>
67598
- <p><button type="button" id="signin" hidden>Sign in with your pod</button></p>
67641
+ <h1>${question}</h1>
67642
+ <a id="webid" href="${escapeHtml(webId)}" hidden></a>
67643
+ <p><button type="button" id="signin" hidden>Allow</button><button type="button" id="cancel" hidden>Cancel</button></p>
67599
67644
  <p id="signin-status" role="status" aria-live="polite"></p>
67600
67645
  </main>
67601
67646
  <script type="module" src="${escapeHtml(mount)}/oauth/session/signin.mjs"><\/script>
67602
67647
  </body></html>`);
67603
67648
  return true;
67604
67649
  }
67605
- function sendLoginForm(res, params, error2 = "", client = null, status2 = null, headers = {}) {
67606
- const hidden = [...params.entries()].filter(([k]) => k !== "password").map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`).join("\n");
67607
- let asking = "<p>Enter the agent password to authorize this client.</p>";
67608
- if (client && (client.name || client.redirect)) {
67609
- let where = "";
67610
- try {
67611
- where = client.redirect ? new URL(client.redirect).host : "";
67612
- } catch {
67613
- }
67614
- const who = client.name ? escapeHtml(client.name) : where ? escapeHtml(where) : "A client";
67615
- asking = `<p><strong>${who}</strong> is asking to access your account${where ? `, sending the authorization to <code>${escapeHtml(where)}</code>` : ""}.</p><p>Scope: <code>${escapeHtml(client.scope || "read")}</code>. Enter the agent password to allow it.</p>`;
67616
- }
67617
- res.writeHead(
67618
- status2 || (error2 ? 401 : 200),
67619
- { "content-type": "text/html; charset=utf-8", ...headers }
67620
- );
67621
- res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
67622
- <title>FediPod \u2014 authorize</title>
67623
- <style>:root{color-scheme:light dark;font-size:125%;--heading:#1a4f8a}
67624
- body{font:1rem system-ui,sans-serif;max-width:22rem;margin:15vh auto;padding:0 1rem}
67625
- h1{color:var(--heading)}
67626
- code{word-break:break-all}
67627
- @media (prefers-color-scheme:dark){:root{--heading:#7fb3e8}}
67628
- input,button{font:inherit;width:100%;padding:.5rem;margin:.3rem 0;box-sizing:border-box}
67629
- .err{color:#b00020}
67630
- @media (prefers-color-scheme:dark){.err{color:#ff8a8a}}</style></head><body>
67631
- <main>
67632
- <h1>FediPod</h1>
67633
- ${asking}
67634
- ${error2 ? `<p class="err" id="login-err" role="alert">${escapeHtml(error2)}</p>` : ""}
67635
- <form method="POST" action="/oauth/authorize">
67636
- ${hidden}
67637
- <label for="password">Agent password</label>
67638
- <input type="password" id="password" name="password" autofocus autocomplete="current-password"
67639
- ${error2 ? 'aria-invalid="true" aria-describedby="login-err"' : ""}>
67640
- <button type="submit">Authorize</button>
67641
- </form>
67642
- </main></body></html>`);
67643
- return true;
67644
- }
67645
67650
  function scopeFor(method, pathname) {
67646
67651
  if (/^\/api\/v\d\/push\//u.test(pathname)) return "push";
67647
67652
  const relationship2 = /^\/api\/v1\/(accounts\/[a-f0-9]+\/(follow|unfollow|block|unblock|mute|unmute|remove_from_followers)|follow_requests\/)/u;
@@ -67780,12 +67785,43 @@ function registerApp(api, { name, website, redirectUris, scopes }) {
67780
67785
  name: String(name || "client").slice(0, 200),
67781
67786
  website: String(website || "").slice(0, 500),
67782
67787
  redirectUris,
67783
- scopes: String(scopes || "read"),
67784
- createdAt: Date.now()
67788
+ scopes: String(scopes || "read").slice(0, 500),
67789
+ createdAt: Date.now(),
67790
+ // Set when the owner first lets it sign in (touchApp): a client nobody
67791
+ // has used is the first to go when the list is full.
67792
+ usedAt: null
67785
67793
  };
67786
- api.store.write("oauth-apps.json", [...api.apps(), app].slice(-MAX_APPS));
67794
+ const kept = api.apps();
67795
+ while (kept.length >= MAX_APPS) kept.splice(kept.indexOf(leastUsed(kept)), 1);
67796
+ api.store.write("oauth-apps.json", [...kept, app]);
67787
67797
  return app;
67788
67798
  }
67799
+ function leastUsed(list3) {
67800
+ const never = list3.filter((a) => a.usedAt === null);
67801
+ const when = (a) => a.usedAt ?? a.createdAt ?? 0;
67802
+ return (never.length ? never : list3).reduce((a, b) => when(b) < when(a) ? b : a);
67803
+ }
67804
+ function touchApp(api, clientId) {
67805
+ const all = api.apps();
67806
+ const app = all.find((a) => a.clientId === clientId);
67807
+ if (!app) return;
67808
+ app.usedAt = Date.now();
67809
+ api.store.write("oauth-apps.json", all);
67810
+ }
67811
+ function redirectsProblem(raw) {
67812
+ if (Array.isArray(raw) && raw.some((v) => typeof v !== "string")) return "redirect_uris must be text";
67813
+ const list3 = parseRedirects(raw);
67814
+ if (list3.length > MAX_REDIRECTS) return `at most ${MAX_REDIRECTS} redirect_uris`;
67815
+ if (list3.some((u) => u.length > MAX_REDIRECT_CHARS)) return `a redirect_uri is at most ${MAX_REDIRECT_CHARS} characters`;
67816
+ return null;
67817
+ }
67818
+ function registrationLimited(api) {
67819
+ const now = Date.now();
67820
+ api.appRegistrations = (api.appRegistrations || []).filter((t) => now - t < REGISTRATION_WINDOW_MS);
67821
+ if (api.appRegistrations.length >= MAX_REGISTRATIONS) return true;
67822
+ api.appRegistrations.push(now);
67823
+ return false;
67824
+ }
67789
67825
  function mintCode(api, { clientId, redirectUri, scope, challenge = null, challengeMethod = null }) {
67790
67826
  const code = node_crypto_default.randomBytes(24).toString("hex");
67791
67827
  const now = Date.now();
@@ -67804,6 +67840,10 @@ function mintCode(api, { clientId, redirectUri, scope, challenge = null, challen
67804
67840
  }].slice(-50));
67805
67841
  return code;
67806
67842
  }
67843
+ function peekCode(api, code) {
67844
+ const now = Date.now();
67845
+ return api.store.read("oauth-codes.json", []).find((c) => c.code === code && now - c.createdAt < CODE_TTL_MS) || null;
67846
+ }
67807
67847
  function consumeCode(api, code) {
67808
67848
  const now = Date.now();
67809
67849
  const all = api.store.read("oauth-codes.json", []);
@@ -67828,22 +67868,27 @@ function redirectAllowed(api, redirect) {
67828
67868
  try {
67829
67869
  const u = new URL(redirect);
67830
67870
  if (u.protocol !== "http:" && u.protocol !== "https:") return false;
67831
- return api.allowed.has(u.host.toLowerCase());
67871
+ if (!api.allowed.has(u.host.toLowerCase())) return false;
67872
+ const mount = api.mount || "";
67873
+ return !mount || u.pathname === mount || u.pathname.startsWith(`${mount}/`);
67832
67874
  } catch {
67833
67875
  return false;
67834
67876
  }
67835
67877
  }
67836
- function rateLimited(api) {
67837
- const now = Date.now();
67838
- api.authzAttempts = api.authzAttempts.filter((t) => now - t < AUTHZ_WINDOW_MS);
67839
- if (api.authzAttempts.length >= AUTHZ_MAX_ATTEMPTS) return true;
67840
- api.authzAttempts.push(now);
67841
- return false;
67842
- }
67843
67878
  async function handle(api, ctx) {
67844
67879
  const { req, res, pathname, url, send } = ctx;
67845
67880
  if (pathname === "/api/v1/apps" && req.method === "POST") {
67846
67881
  const body = await readBody2(req);
67882
+ const problem = redirectsProblem(body.redirect_uris);
67883
+ if (problem) return send(422, { error: problem });
67884
+ if (registrationLimited(api)) {
67885
+ api.log("app registration refused: too many this minute");
67886
+ return send(
67887
+ 429,
67888
+ { error: "too many apps registered just now \u2014 try again in a minute" },
67889
+ { "retry-after": String(REGISTRATION_WINDOW_MS / 1e3) }
67890
+ );
67891
+ }
67847
67892
  const redirectUris = parseRedirects(body.redirect_uris);
67848
67893
  const app = api.registerApp({
67849
67894
  name: body.client_name,
@@ -67862,7 +67907,6 @@ async function handle(api, ctx) {
67862
67907
  });
67863
67908
  }
67864
67909
  if (pathname === "/oauth/authorize" && (req.method === "GET" || req.method === "POST")) {
67865
- const pw = api.store.getConfig()?.uiPassword;
67866
67910
  let params = url.searchParams;
67867
67911
  let body = null;
67868
67912
  if (req.method === "POST") {
@@ -67878,7 +67922,7 @@ async function handle(api, ctx) {
67878
67922
  }
67879
67923
  const ownerWebId = api.ownerWebId?.() || null;
67880
67924
  if (req.method === "GET" && !params.get("client_id") && params.get("code") && params.get("state") && ownerWebId) {
67881
- return sendPodSigninPage(res, null, ownerWebId, api.mount || "");
67925
+ return sendPodSigninPage(res, null, ownerWebId, api.mount || "", ownAddress(api));
67882
67926
  }
67883
67927
  const app = api.findApp(params.get("client_id") || "");
67884
67928
  const doc = app ? null : await api.resolveClientDocument(params.get("client_id") || "");
@@ -67888,11 +67932,15 @@ async function handle(api, ctx) {
67888
67932
  return send(403, { error: "a cross-site navigation may not authorize a client" });
67889
67933
  }
67890
67934
  const client = { name: app?.name || doc?.name || null, redirect, scope: params.get("scope") || "read" };
67891
- if (external && !proved && !api.store.getConfig()?.uiPassword && !api.redirectAllowed(redirect)) {
67892
- if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "");
67893
- api.log(`authorize refused: no UI password, and "${redirect}" is not an address of this agent`);
67935
+ const challengeMethod = params.get("code_challenge_method");
67936
+ if (params.get("code_challenge") && challengeMethod && challengeMethod !== "S256" && challengeMethod !== "plain") {
67937
+ return send(400, { error: "code_challenge_method must be S256 or plain" });
67938
+ }
67939
+ if (external && !proved && !api.redirectAllowed(redirect)) {
67940
+ if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "", ownAddress(api));
67941
+ api.log(`authorize refused: "${redirect}" is not an address of this agent, and nothing approved it`);
67894
67942
  return send(403, {
67895
- 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."
67943
+ error: "this client asks to be sent somewhere other than this account; approve it by signing in at your pod"
67896
67944
  });
67897
67945
  }
67898
67946
  if (app) {
@@ -67913,28 +67961,11 @@ async function handle(api, ctx) {
67913
67961
  api.log(`authorize refused: redirect_uri "${redirect}" is not this agent`);
67914
67962
  return send(400, { error: "redirect_uri must be an address of this agent" });
67915
67963
  }
67916
- if (req.method === "POST" && !proved) {
67917
- if (api.rateLimited()) {
67918
- api.log("authorize rate limited");
67919
- return sendLoginForm(
67920
- res,
67921
- params,
67922
- "too many attempts \u2014 wait a minute",
67923
- client,
67924
- 429,
67925
- { "retry-after": String(Math.ceil(AUTHZ_WINDOW_MS / 1e3)) }
67926
- );
67927
- }
67928
- if (!pw || !checkPassword(pw, body.password || "")) {
67929
- return sendLoginForm(res, params, "wrong password \u2014 try again", client);
67930
- }
67931
- } else if (pw && !proved) {
67932
- return sendLoginForm(res, params, "", client);
67933
- } else if (!proved && api.allowed && !api.allowed.isLocalRequest(req) && !api.throughDoor?.(req)) {
67934
- if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "");
67935
- api.log(`authorize refused: no UI password, and "${req.headers.host}" is not this machine`);
67964
+ if (!proved && api.allowed && !api.allowed.isLocalRequest(req) && !api.throughDoor?.(req)) {
67965
+ if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "", ownAddress(api));
67966
+ api.log(`authorize refused: not approved at the pod, and "${req.headers.host}" is not this machine`);
67936
67967
  return send(403, {
67937
- 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"
67968
+ error: api.embedded ? "Open your account's management page first, then sign in from there." : "Signing in from another machine goes through your pod: sign in there to approve this app."
67938
67969
  });
67939
67970
  }
67940
67971
  const code = external ? api.mintCode({
@@ -67944,6 +67975,7 @@ async function handle(api, ctx) {
67944
67975
  challenge: params.get("code_challenge") || null,
67945
67976
  challengeMethod: params.get("code_challenge_method") || null
67946
67977
  }) : api.mintToken(client.scope);
67978
+ if (app) touchApp(api, app.clientId);
67947
67979
  if (!redirect || redirect === "urn:ietf:wg:oauth:2.0:oob") return send(200, { code });
67948
67980
  const target = new URL(redirect);
67949
67981
  target.searchParams.set("code", code);
@@ -67957,7 +67989,7 @@ async function handle(api, ctx) {
67957
67989
  const body = await readBody2(req);
67958
67990
  const app = api.findApp(body.client_id || "");
67959
67991
  if (!app && body.code_verifier && /^https:\/\//iu.test(String(body.client_id || ""))) {
67960
- const rec = api.consumeCode(body.code || "");
67992
+ const rec = api.peekCode(body.code || "");
67961
67993
  if (!rec || rec.clientId !== body.client_id || body.redirect_uri && rec.redirectUri !== body.redirect_uri) {
67962
67994
  api.log("token refused: code is not a live authorization for that client document");
67963
67995
  return send(400, { error: "invalid_grant" });
@@ -67966,6 +67998,7 @@ async function handle(api, ctx) {
67966
67998
  api.log("token refused: the verifier does not answer the challenge this code was made with");
67967
67999
  return send(400, { error: "invalid_grant" });
67968
68000
  }
68001
+ if (!api.consumeCode(body.code || "")) return send(400, { error: "invalid_grant" });
67969
68002
  return send(200, {
67970
68003
  access_token: api.mintToken(rec.scope || "read"),
67971
68004
  token_type: "Bearer",
@@ -67975,7 +68008,7 @@ async function handle(api, ctx) {
67975
68008
  });
67976
68009
  }
67977
68010
  if (app && body.code_verifier) {
67978
- const rec = api.consumeCode(body.code || "");
68011
+ const rec = api.peekCode(body.code || "");
67979
68012
  if (!rec || rec.clientId !== app.clientId || body.redirect_uri && rec.redirectUri !== body.redirect_uri) {
67980
68013
  api.log("token refused: code is not a live authorization for this client");
67981
68014
  return send(400, { error: "invalid_grant" });
@@ -67984,6 +68017,7 @@ async function handle(api, ctx) {
67984
68017
  api.log("token refused: the verifier does not answer the challenge this code was made with");
67985
68018
  return send(400, { error: "invalid_grant" });
67986
68019
  }
68020
+ if (!api.consumeCode(body.code || "")) return send(400, { error: "invalid_grant" });
67987
68021
  return send(200, {
67988
68022
  access_token: api.mintToken(rec.scope || "read"),
67989
68023
  token_type: "Bearer",
@@ -68000,7 +68034,7 @@ async function handle(api, ctx) {
68000
68034
  api.log("token refused: client secret mismatch");
68001
68035
  return send(401, { error: "invalid_client" });
68002
68036
  }
68003
- const rec = api.consumeCode(body.code || "");
68037
+ const rec = api.peekCode(body.code || "");
68004
68038
  if (!rec || rec.clientId !== app.clientId || body.redirect_uri && rec.redirectUri !== body.redirect_uri) {
68005
68039
  api.log("token refused: code is not a live authorization for this client");
68006
68040
  return send(400, { error: "invalid_grant" });
@@ -68009,6 +68043,7 @@ async function handle(api, ctx) {
68009
68043
  api.log("token refused: this code was made with a challenge and the verifier does not answer it");
68010
68044
  return send(400, { error: "invalid_grant" });
68011
68045
  }
68046
+ if (!api.consumeCode(body.code || "")) return send(400, { error: "invalid_grant" });
68012
68047
  return send(200, {
68013
68048
  access_token: api.mintToken(rec.scope || "read"),
68014
68049
  token_type: "Bearer",
@@ -68039,6 +68074,7 @@ async function handle(api, ctx) {
68039
68074
  if (gone) {
68040
68075
  const kept = api.tokenRecords().filter((r) => r.token !== gone);
68041
68076
  api.store.write("masto-tokens.json", kept);
68077
+ if (api.push.get(gone)) api.push.drop(gone);
68042
68078
  api.log("client token revoked");
68043
68079
  }
68044
68080
  return send(200, {});
@@ -68274,6 +68310,10 @@ var BskyFeed = class {
68274
68310
  };
68275
68311
 
68276
68312
  // lib/client/masto/render.mjs
68313
+ var once = (html) => {
68314
+ const m = (html.length - 1) / 2;
68315
+ return Number.isInteger(m) && html[m] === "," && html.slice(0, m) === html.slice(m + 1) ? html.slice(0, m) : html;
68316
+ };
68277
68317
  var TRANSPARENT_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
68278
68318
  var DEFAULT_AVATAR = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABd0lEQVR42u3ZoW4CQRDG8Xk6NBLbVCJ5gBpMTQ2iBlFR29RXNsFhz1JHXV9hydfkEkoobW5n2F34i0kIHMfN73ZvdwYbTabpmsMAAAAAAAAAAAAAAAAAAACiY3wzS/ePz+n1bZW6zTZ9fH59h17rPX2mYy4OQEktnl5+JP1b6Bgdey6IcIDb2Ty9r7s/Ez8MfUffbRpACfznrp8aDdEIFjnsh9z5YyMhcjqEAWge5ybfh87VFIDuWM7QPzYVokZBCICWM6/k+9A5mwHQmu4NoHM2A+A5/PenQTMA3sn3AQBTgIcgyyAbIbbCFEOUwzREaInRFKUtDgAAFQLcPSxDCqH9Z4V+o0oAXVhU4ofhiWCtJe+NYKV3e6V3idkAHvv9nDqhKIBnxVeqUrRaav5SvQKrqetTols0GGDdbaoB0LWcFUBP31qS72PoimCtD//caWC1tL1Ltc2ttbXfe08wCKC25HP+OgMAAAAAAAAAAAAAAAAArhNgB1PE/x/JOiuNAAAAAElFTkSuQmCC";
68279
68319
  var selfIcon = (api, self2, cached) => (self2 ? api.store.getConfig()?.icon : null) || cached.icon;
@@ -68497,7 +68537,7 @@ function status(api, s, { all, depth = 0 } = {}) {
68497
68537
  in_reply_to_id: s.inReplyTo ? api.store.idFor(s.inReplyTo) : null,
68498
68538
  in_reply_to_account_id: null,
68499
68539
  sensitive: !!(s.spoiler || s.sensitive),
68500
- spoiler_text: s.spoiler || "",
68540
+ spoiler_text: once(s.spoiler || ""),
68501
68541
  visibility: s.visibility || "public",
68502
68542
  language: null,
68503
68543
  edited_at: s.editedAt || null,
@@ -68514,7 +68554,7 @@ function status(api, s, { all, depth = 0 } = {}) {
68514
68554
  muted: false,
68515
68555
  bookmarked: !!s.bookmarked,
68516
68556
  pinned: !!s.pinned,
68517
- content: s.content || "",
68557
+ content: once(s.content || ""),
68518
68558
  reblog: null,
68519
68559
  application: null,
68520
68560
  account: api.account(s.actor),
@@ -68556,7 +68596,7 @@ function status(api, s, { all, depth = 0 } = {}) {
68556
68596
  }
68557
68597
  function filtersFor(api, s) {
68558
68598
  const now = Date.now();
68559
- const hay = `${s.content || ""} ${s.spoiler || ""}`.replace(/<[^>]*>/gu, " ").toLowerCase();
68599
+ const hay = `${once(s.content || "")} ${once(s.spoiler || "")}`.replace(/<[^>]*>/gu, " ").toLowerCase();
68560
68600
  if (!hay.trim()) return [];
68561
68601
  const out = [];
68562
68602
  for (const f of api.store.getFilters?.() || []) {
@@ -68643,7 +68683,7 @@ function pushNotify(api, n) {
68643
68683
  notification_type: n.type,
68644
68684
  preferred_locale: "en",
68645
68685
  title: `${acct.display_name || acct.acct} ${verbs[n.type] || n.type}`,
68646
- body: s ? htmlToText(s.content || "").slice(0, 140) : "",
68686
+ body: s ? htmlToText(once(s.content || "")).slice(0, 140) : "",
68647
68687
  icon: acct.avatar || ""
68648
68688
  });
68649
68689
  }
@@ -68878,6 +68918,11 @@ init_node_crypto();
68878
68918
  var write6 = (pod, url, bytes, contentType) => pod.put(url, bytes, contentType);
68879
68919
 
68880
68920
  // lib/client/masto/accounts.mjs
68921
+ var PROFILE_IMAGE_EXT = { "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp", "image/avif": "avif" };
68922
+ var profileImageType = (f) => {
68923
+ const t = String(f?.contentType || "").split(";")[0].trim().toLowerCase();
68924
+ return PROFILE_IMAGE_EXT[t] ? t : null;
68925
+ };
68881
68926
  async function handle3(api, ctx) {
68882
68927
  const { req, res, pathname, url, send } = ctx;
68883
68928
  if (pathname === "/api/v1/accounts/verify_credentials") {
@@ -68902,12 +68947,17 @@ async function handle3(api, ctx) {
68902
68947
  if (ct.includes("multipart/form-data")) ({ fields: form, files } = await readMultipart(req));
68903
68948
  else form = await readBody2(req);
68904
68949
  const cfg = { ...api.store.getConfig() };
68950
+ for (const f of [files.avatar, files.header]) {
68951
+ if (f?.data?.length && !profileImageType(f)) {
68952
+ return send(422, { error: "a profile picture or header must be a PNG, JPEG, GIF, WebP or AVIF image" });
68953
+ }
68954
+ }
68905
68955
  const putImage = async (f) => {
68906
- const ext = (f.filename || "").includes(".") ? f.filename.split(".").pop().replace(/[^\w]/g, "") : "bin";
68907
- const slug = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex") + "." + ext;
68956
+ const type = profileImageType(f);
68957
+ const slug = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex") + "." + PROFILE_IMAGE_EXT[type];
68908
68958
  const url2 = api.urls.media + slug;
68909
68959
  await api.agent.publisher.ensureMediaContainer();
68910
- await write6(api.agent.remote, url2, f.data, f.contentType);
68960
+ await write6(api.agent.remote, url2, f.data, type);
68911
68961
  return url2;
68912
68962
  };
68913
68963
  if ("display_name" in form) cfg.name = String(form.display_name).trim() || cfg.handle;
@@ -69886,7 +69936,6 @@ var MastoApi = class _MastoApi {
69886
69936
  this.streaming = streaming;
69887
69937
  this.webPush = webPush;
69888
69938
  this.scheduling = scheduling;
69889
- this.authzAttempts = [];
69890
69939
  }
69891
69940
  get store() {
69892
69941
  return this.agent.store;
@@ -69898,7 +69947,8 @@ var MastoApi = class _MastoApi {
69898
69947
  this._push ||= new Push({
69899
69948
  store: this.store,
69900
69949
  subject: () => this.urls?.actor || "https://localhost/",
69901
- log: this.log
69950
+ log: this.log,
69951
+ liveKeys: () => new Set(this.tokens().map((t) => this._push.keyOf(t)))
69902
69952
  });
69903
69953
  return this._push;
69904
69954
  }
@@ -69997,6 +70047,9 @@ var MastoApi = class _MastoApi {
69997
70047
  consumeCode(...a) {
69998
70048
  return consumeCode(this, ...a);
69999
70049
  }
70050
+ peekCode(...a) {
70051
+ return peekCode(this, ...a);
70052
+ }
70000
70053
  tokenOf(...a) {
70001
70054
  return tokenOf(this, ...a);
70002
70055
  }
@@ -70006,9 +70059,6 @@ var MastoApi = class _MastoApi {
70006
70059
  redirectAllowed(...a) {
70007
70060
  return redirectAllowed(this, ...a);
70008
70061
  }
70009
- rateLimited(...a) {
70010
- return rateLimited(this, ...a);
70011
- }
70012
70062
  // render.mjs
70013
70063
  selfAccount(...a) {
70014
70064
  return selfAccount(this, ...a);
@@ -70106,6 +70156,20 @@ var DEFAULTS2 = {
70106
70156
  intervalMin: 15
70107
70157
  };
70108
70158
  var PER_TAG = 20;
70159
+ var SHARED_TTL_MS = 12 * 6e4;
70160
+ var sharedTimelines = /* @__PURE__ */ new Map();
70161
+ function timelineOnce(key, load2) {
70162
+ const now = Date.now();
70163
+ const held = sharedTimelines.get(key);
70164
+ if (held && now - held.at < SHARED_TTL_MS) return held.answer;
70165
+ const answer = load2().catch((e) => {
70166
+ sharedTimelines.delete(key);
70167
+ throw e;
70168
+ });
70169
+ sharedTimelines.set(key, { at: now, answer });
70170
+ if (sharedTimelines.size > 500) sharedTimelines.delete(sharedTimelines.keys().next().value);
70171
+ return answer;
70172
+ }
70109
70173
  var MAX_NEW_PER_SWEEP = 8;
70110
70174
  var MAX_NEW_PER_TAG = 2;
70111
70175
  var TAGS_PER_SWEEP = 4;
@@ -70197,12 +70261,17 @@ var TagFeed = class {
70197
70261
  try {
70198
70262
  const { safeFetch: safeFetch2, retryAfterMs: retryAfterMs3, readCapped: readCapped3 } = await Promise.resolve().then(() => (init_safefetch(), safefetch_exports));
70199
70263
  const url = `${instance}/api/v1/timelines/tag/${encodeURIComponent(tag)}?limit=${PER_TAG}`;
70200
- const res = this.fetcher === globalThis.fetch ? await safeFetch2(url, { headers: { accept: "application/json" } }) : await this.fetcher(url, { headers: { accept: "application/json" } });
70201
- if (res.status >= 400) {
70202
- this._backOff(res.status, retryAfterMs3(res));
70264
+ const load2 = async () => {
70265
+ const res = this.fetcher === globalThis.fetch ? await safeFetch2(url, { headers: { accept: "application/json" } }) : await this.fetcher(url, { headers: { accept: "application/json" } });
70266
+ if (res.status >= 400) return { status: res.status, retryAfter: retryAfterMs3(res), list: null };
70267
+ return { status: res.status, list: JSON.parse(await readCapped3(res, MAX_TIMELINE_BYTES)) };
70268
+ };
70269
+ const got = this.fetcher === globalThis.fetch ? await timelineOnce(`${instance}|${tag}`, load2) : await load2();
70270
+ if (got.status >= 400) {
70271
+ this._backOff(got.status, got.retryAfter);
70203
70272
  return;
70204
70273
  }
70205
- list3 = JSON.parse(await readCapped3(res, MAX_TIMELINE_BYTES));
70274
+ list3 = got.list;
70206
70275
  } catch (e) {
70207
70276
  this.log(`tagfeed #${tag}: ${e.message}`);
70208
70277
  this._backOff(0, null);
@@ -70480,7 +70549,7 @@ async function attachProof(activity, { privateKey, verificationMethod, created =
70480
70549
 
70481
70550
  // lib/core/deliver.mjs
70482
70551
  init_safefetch();
70483
- var MAX_REDIRECTS = 3;
70552
+ var MAX_REDIRECTS2 = 3;
70484
70553
  async function withQueryInTarget(signed, privateKey) {
70485
70554
  const url = new URL(signed.url);
70486
70555
  if (!url.search) return signed;
@@ -70537,7 +70606,8 @@ var Deliverer = class {
70537
70606
  log: log2 = console.log,
70538
70607
  passive = false,
70539
70608
  defer = false,
70540
- onGone = null
70609
+ onGone = null,
70610
+ parallel = 1
70541
70611
  }) {
70542
70612
  this.store = store;
70543
70613
  this.defer = defer;
@@ -70548,7 +70618,8 @@ var Deliverer = class {
70548
70618
  this.edPrivate = edPrivate;
70549
70619
  this.proofKeyId = proofKeyId;
70550
70620
  this.log = log2;
70551
- this.batchSize = 1;
70621
+ this.batchSize = parallel;
70622
+ this.parallel = parallel;
70552
70623
  if (!passive) this.startQueue();
70553
70624
  }
70554
70625
  startQueue() {
@@ -70568,7 +70639,7 @@ var Deliverer = class {
70568
70639
  signal: init.signal || AbortSignal.timeout(HTTP_TIMEOUT_MS),
70569
70640
  headers: { "user-agent": USER_AGENT, ...init.headers || {} }
70570
70641
  };
70571
- for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
70642
+ for (let hop = 0; hop <= MAX_REDIRECTS2; hop++) {
70572
70643
  const dispatcher = await pinnedFor(current);
70573
70644
  const req = new Request(current, withUa);
70574
70645
  const signed = await withQueryInTarget(
@@ -70629,6 +70700,9 @@ var Deliverer = class {
70629
70700
  // with the error deliverNow would have thrown. Here one at a time; the
70630
70701
  // relay deliverer sends the whole list in one call.
70631
70702
  async deliverManyNow(targets) {
70703
+ if (this.parallel > 1) {
70704
+ return Promise.all(targets.map((t) => this.deliverNow(t.inbox, t.activity).then(() => ({ ok: true }), (error2) => ({ error: error2 }))));
70705
+ }
70632
70706
  const out = [];
70633
70707
  for (const t of targets) {
70634
70708
  try {
@@ -71454,7 +71528,6 @@ var AdminFacade = class {
71454
71528
  fields: cfg.fields || [],
71455
71529
  aliases: cfg.aliases || [],
71456
71530
  autoAcceptFollows: cfg.autoAcceptFollows !== false,
71457
- hasUiPassword: !!cfg.uiPassword,
71458
71531
  quiescedAt: cfg.quiescedAt || null,
71459
71532
  movedTo: cfg.movedTo || null,
71460
71533
  mode: a.status().mode,
@@ -71570,10 +71643,6 @@ var AdminFacade = class {
71570
71643
  cfg.fields = (Array.isArray(body.fields) ? body.fields : []).filter((f) => f?.name?.trim()).map((f) => ({ name: String(f.name).trim(), value: String(f.value ?? "").trim() }));
71571
71644
  }
71572
71645
  if ("autoAcceptFollows" in body) cfg.autoAcceptFollows = !!body.autoAcceptFollows;
71573
- if ("password" in body) {
71574
- if (body.password) cfg.uiPassword = hashPassword(body.password);
71575
- else delete cfg.uiPassword;
71576
- }
71577
71646
  const republish = WIRE_CONFIG.some((k) => k in body);
71578
71647
  a.store.setConfig(cfg);
71579
71648
  if ("autoAcceptFollows" in body && a.publisher) a.publisher.config.autoAcceptFollows = cfg.autoAcceptFollows;
@@ -73979,7 +74048,7 @@ var BrowserAgent = class _BrowserAgent {
73979
74048
  const fullLoad = () => this.store.load().catch((e) => {
73980
74049
  unread = e;
73981
74050
  });
73982
- if (this._warm) await this.store.refresh(this._warm.pending).catch(fullLoad);
74051
+ if (this._warm && warmFrom.listingEtag) await this.store.refresh(this._warm.pending).catch(fullLoad);
73983
74052
  else await fullLoad();
73984
74053
  const cfg = config || this.store.getConfig();
73985
74054
  if (!cfg) throw await accountNotRead({ unread, podBase: remotePod, state: this.urls.state, webId, actorUrl: this.urls.actor });