fedipod 1.32.0 → 1.36.6
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.
- package/lib/connections/bskyfeed.mjs +6 -0
- package/lib/core/deliver.mjs +52 -22
- package/lib/core/intake/index.mjs +17 -4
- package/lib/core/lease.mjs +7 -7
- package/lib/core/store.mjs +25 -5
- package/lib/gateway/caches.mjs +36 -0
- package/lib/gateway/front-core.mjs +29 -56
- package/lib/gateway/gateway-core.mjs +5 -2
- package/lib/gateway/headers.mjs +49 -0
- package/lib/gateway/quiet.mjs +7 -2
- package/lib/pod/containers.mjs +5 -1
- package/lib/pod/inbox.mjs +22 -3
- package/lib/pod/transport.mjs +36 -3
- package/lib/session/README.md +14 -0
- package/lib/session/fedi-account.mjs +62 -0
- package/lib/session/package.json +10 -2
- package/package.json +1 -1
- package/run-agent.mjs +16 -0
- package/web/app/agent.mjs +13 -8
- package/web/app/deliver-relay.mjs +47 -7
- package/web/app/dist/boot.js +36 -3
- package/web/app/dist/boot.js.map +2 -2
- package/web/app/dist/sw.js +193 -43
- package/web/app/dist/sw.js.map +3 -3
- package/web/app/update.js +4 -0
package/web/app/dist/sw.js
CHANGED
|
@@ -34286,9 +34286,11 @@ async function provisionPublic(pod, base) {
|
|
|
34286
34286
|
await pod.setAcl(base, ["Read"]);
|
|
34287
34287
|
}
|
|
34288
34288
|
async function provisionPrivate(pod, urls) {
|
|
34289
|
+
if (await exists(pod, urls.state)) return false;
|
|
34289
34290
|
await pod.putJson(keepUrl(urls.state), KEEP, KEEP_CT);
|
|
34290
34291
|
await pod.setAcl(urls.state, []);
|
|
34291
34292
|
await pod.setAcl(urls.home, []);
|
|
34293
|
+
return true;
|
|
34292
34294
|
}
|
|
34293
34295
|
async function repairPrivateAcls(pod, trees, { isPublic } = {}) {
|
|
34294
34296
|
const findings = [];
|
|
@@ -34367,6 +34369,8 @@ function dropFollower(contacts, actor, why) {
|
|
|
34367
34369
|
contacts.removedFollowers = gone.slice(-500);
|
|
34368
34370
|
return contacts;
|
|
34369
34371
|
}
|
|
34372
|
+
var serialise = (obj) => JSON.stringify(obj, null, 2) + "\n";
|
|
34373
|
+
var sameButWhen = (a, b) => JSON.stringify({ ...a, fetchedAt: null }) === JSON.stringify({ ...b, fetchedAt: null });
|
|
34370
34374
|
var PodStore = class {
|
|
34371
34375
|
constructor({ storage = null, log: log2 = console.log } = {}) {
|
|
34372
34376
|
this.lastSkipped = [];
|
|
@@ -34374,6 +34378,7 @@ var PodStore = class {
|
|
|
34374
34378
|
this.log = log2;
|
|
34375
34379
|
this.cache = /* @__PURE__ */ new Map();
|
|
34376
34380
|
this.etags = /* @__PURE__ */ new Map();
|
|
34381
|
+
this.lastText = /* @__PURE__ */ new Map();
|
|
34377
34382
|
this.timers = /* @__PURE__ */ new Map();
|
|
34378
34383
|
this.dirty = /* @__PURE__ */ new Set();
|
|
34379
34384
|
this._held = 0;
|
|
@@ -34387,6 +34392,7 @@ var PodStore = class {
|
|
|
34387
34392
|
if (this.storage && this.storage.base !== storage.base) {
|
|
34388
34393
|
this.cache.clear();
|
|
34389
34394
|
this.etags.clear();
|
|
34395
|
+
this.lastText.clear();
|
|
34390
34396
|
}
|
|
34391
34397
|
this.storage = storage;
|
|
34392
34398
|
}
|
|
@@ -34428,6 +34434,7 @@ var PodStore = class {
|
|
|
34428
34434
|
this.etags.set(name, r.etag);
|
|
34429
34435
|
try {
|
|
34430
34436
|
this.cache.set(name, JSON.parse(r.body));
|
|
34437
|
+
this.lastText.set(name, r.body);
|
|
34431
34438
|
} catch (e) {
|
|
34432
34439
|
this.log(`state load ${name}: unparsable (${e.message})`);
|
|
34433
34440
|
}
|
|
@@ -34450,6 +34457,12 @@ var PodStore = class {
|
|
|
34450
34457
|
write(name, obj) {
|
|
34451
34458
|
this.cache.set(name, structuredClone(obj));
|
|
34452
34459
|
if (!this.storage) return;
|
|
34460
|
+
if (this.lastText.get(name) === serialise(obj)) {
|
|
34461
|
+
clearTimeout(this.timers.get(name));
|
|
34462
|
+
this.timers.delete(name);
|
|
34463
|
+
this.dirty.delete(name);
|
|
34464
|
+
return;
|
|
34465
|
+
}
|
|
34453
34466
|
if (this._held) {
|
|
34454
34467
|
this.dirty.add(name);
|
|
34455
34468
|
return;
|
|
@@ -34493,10 +34506,13 @@ var PodStore = class {
|
|
|
34493
34506
|
// the returned promise so one failure cannot poison the queue.
|
|
34494
34507
|
_put(name) {
|
|
34495
34508
|
const done = this.chain.then(async () => {
|
|
34496
|
-
const body =
|
|
34509
|
+
const body = serialise(this.cache.get(name));
|
|
34497
34510
|
for (let attempt = 1; attempt <= PUT_RETRIES; attempt++) {
|
|
34498
34511
|
const r = await this.storage.write(name, body, "application/json").catch((e) => ({ ok: false, retry: false, why: e.message }));
|
|
34499
|
-
if (r.ok)
|
|
34512
|
+
if (r.ok) {
|
|
34513
|
+
this.lastText.set(name, body);
|
|
34514
|
+
return true;
|
|
34515
|
+
}
|
|
34500
34516
|
if (!r.retry) {
|
|
34501
34517
|
this.log(`state write ${name} refused (${r.why}) \u2014 not retrying`);
|
|
34502
34518
|
return false;
|
|
@@ -34523,6 +34539,7 @@ var PodStore = class {
|
|
|
34523
34539
|
// migrates to the local machine — leaving the copy behind would defeat it).
|
|
34524
34540
|
async remove(name) {
|
|
34525
34541
|
this.cache.delete(name);
|
|
34542
|
+
this.lastText.delete(name);
|
|
34526
34543
|
clearTimeout(this.timers.get(name));
|
|
34527
34544
|
this.timers.delete(name);
|
|
34528
34545
|
if (!this.storage) return true;
|
|
@@ -34773,6 +34790,7 @@ var PodStore = class {
|
|
|
34773
34790
|
}
|
|
34774
34791
|
cacheActor(url, doc) {
|
|
34775
34792
|
const a = this.getActors();
|
|
34793
|
+
const known2 = a[url];
|
|
34776
34794
|
a[url] = {
|
|
34777
34795
|
name: clamp(plainText(doc.name || doc.preferredUsername || ""), MAX_NAME),
|
|
34778
34796
|
preferredUsername: clamp(plainText(doc.preferredUsername || ""), MAX_NAME),
|
|
@@ -34785,9 +34803,13 @@ var PodStore = class {
|
|
|
34785
34803
|
type: doc.type || "Person",
|
|
34786
34804
|
followers: doc.followers || null,
|
|
34787
34805
|
following: doc.following || null,
|
|
34788
|
-
...
|
|
34806
|
+
...known2?.counts ? { counts: known2.counts } : {},
|
|
34789
34807
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34790
34808
|
};
|
|
34809
|
+
if (known2 && sameButWhen(known2, a[url])) {
|
|
34810
|
+
a[url] = known2;
|
|
34811
|
+
return;
|
|
34812
|
+
}
|
|
34791
34813
|
this.write("actors.json", prune(a, ACTOR_CACHE_MAX, this.getContacts()));
|
|
34792
34814
|
}
|
|
34793
34815
|
// @user@host for an actor we have cached. Null when we have not, rather than
|
|
@@ -45487,11 +45509,11 @@ async function writeKeep(pod, urls) {
|
|
|
45487
45509
|
await pod.putJson(urls.inbox + ".keep", { keep: true }, "application/json");
|
|
45488
45510
|
}
|
|
45489
45511
|
async function setPosture(pod, urls, posture) {
|
|
45490
|
-
if (posture === "open") return pod.setAcl(urls.inbox, ["Append"]);
|
|
45491
|
-
if (posture === "closed") return pod.setAcl(urls.inbox, []);
|
|
45512
|
+
if (posture === "open") return pod.setAcl(urls.inbox, ["Append"], { ifChanged: true });
|
|
45513
|
+
if (posture === "closed") return pod.setAcl(urls.inbox, [], { ifChanged: true });
|
|
45492
45514
|
const webId = posture?.gatewayWebId;
|
|
45493
45515
|
if (!webId) throw new Error(`inbox.setPosture: unknown posture ${JSON.stringify(posture)}`);
|
|
45494
|
-
return pod.setAcl(urls.inbox, [], { appendAgents: [webId] });
|
|
45516
|
+
return pod.setAcl(urls.inbox, [], { appendAgents: [webId], ifChanged: true });
|
|
45495
45517
|
}
|
|
45496
45518
|
async function list(pod, urls) {
|
|
45497
45519
|
const children = await pod.listContainer(urls.inbox);
|
|
@@ -45508,6 +45530,9 @@ async function readDeliveryReceipt(pod, itemUrl, { maxBytes, readCapped: readCap
|
|
|
45508
45530
|
return JSON.parse(await readCapped3(res, maxBytes));
|
|
45509
45531
|
}
|
|
45510
45532
|
var dropHandledItem = (pod, url) => pod.delete(url);
|
|
45533
|
+
var dropReceiptBeside = (pod, url) => pod.delete(url + ".receipt.json").catch(() => false);
|
|
45534
|
+
var orphanReceipts = (pod, urls) => pod.orphanReceipts?.(urls.inbox) ?? [];
|
|
45535
|
+
var dropStrayReceipt = (pod, url) => pod.delete(url).catch(() => false);
|
|
45511
45536
|
|
|
45512
45537
|
// lib/pod/collection.mjs
|
|
45513
45538
|
var PUBLIC_READ3 = ["Read"];
|
|
@@ -63870,6 +63895,7 @@ var DRAIN_COOLDOWN_MAX_MS = 30 * 6e4;
|
|
|
63870
63895
|
var DELETE_GAP_MS = 150;
|
|
63871
63896
|
var CHAIN_GAP_MS = 5e3;
|
|
63872
63897
|
var DELETE_BATCH = 10;
|
|
63898
|
+
var ORPHAN_RECEIPTS_PER_SWEEP = 20;
|
|
63873
63899
|
var ATTEMPTS_DOC = "intake-attempts.json";
|
|
63874
63900
|
var ATTEMPTS_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
63875
63901
|
var MAX_ITEM_ATTEMPTS = 5;
|
|
@@ -64140,6 +64166,7 @@ var Intake = class {
|
|
|
64140
64166
|
if (all.length > items.length) this.log(`inbox has ${all.length} items \u2014 processing ${items.length} this sweep`);
|
|
64141
64167
|
let handled = 0;
|
|
64142
64168
|
const pending = [];
|
|
64169
|
+
const withReceipt = /* @__PURE__ */ new Set();
|
|
64143
64170
|
const flush = async () => {
|
|
64144
64171
|
if (!pending.length) return true;
|
|
64145
64172
|
if (!await this._persisted()) {
|
|
@@ -64154,12 +64181,14 @@ var Intake = class {
|
|
|
64154
64181
|
}
|
|
64155
64182
|
this._clearAttempt(url);
|
|
64156
64183
|
handled++;
|
|
64184
|
+
if (withReceipt.has(url)) await dropReceiptBeside(this.remote, url);
|
|
64157
64185
|
await new Promise((r) => setTimeout(r, DELETE_GAP_MS));
|
|
64158
64186
|
}
|
|
64159
64187
|
return true;
|
|
64160
64188
|
};
|
|
64161
|
-
for (const { url, size } of items) {
|
|
64189
|
+
for (const { url, size, receipt: hasReceipt } of items) {
|
|
64162
64190
|
if (url.endsWith(".keep")) continue;
|
|
64191
|
+
if (hasReceipt) withReceipt.add(url);
|
|
64163
64192
|
if (size > MAX_ITEM_BYTES) {
|
|
64164
64193
|
this.store.addDeadLetter({ inboxUrl: url, reason: `oversized (${size} bytes)`, activity: null });
|
|
64165
64194
|
pending.push(url);
|
|
@@ -64185,7 +64214,7 @@ var Intake = class {
|
|
|
64185
64214
|
});
|
|
64186
64215
|
}
|
|
64187
64216
|
}
|
|
64188
|
-
const receipt = activity ? await this._readReceipt(url) : null;
|
|
64217
|
+
const receipt = activity && hasReceipt !== false ? await this._readReceipt(url) : null;
|
|
64189
64218
|
if (activity && this.gatewaySecret()) this._bumpGatewayStat(!!receipt?.verified);
|
|
64190
64219
|
const owned = activity && this.isOwnerPost(receipt);
|
|
64191
64220
|
const rejection = !activity ? "unparsable JSON" : owned ? await this.ownerPostFrom(activity, raw, receipt) : await this.handle(activity, receipt);
|
|
@@ -64212,6 +64241,10 @@ var Intake = class {
|
|
|
64212
64241
|
if (pending.length >= DELETE_BATCH && !await flush()) return;
|
|
64213
64242
|
}
|
|
64214
64243
|
if (!await this._finishSweep(flush)) return;
|
|
64244
|
+
for (const stray of orphanReceipts(this.remote, this.urls).slice(0, ORPHAN_RECEIPTS_PER_SWEEP)) {
|
|
64245
|
+
if (!await dropStrayReceipt(this.remote, stray)) break;
|
|
64246
|
+
await new Promise((r) => setTimeout(r, DELETE_GAP_MS));
|
|
64247
|
+
}
|
|
64215
64248
|
if (handled > 0 && all.length > items.length && !this.stopped) this._drainAgain = true;
|
|
64216
64249
|
}
|
|
64217
64250
|
// The end of a sweep: publish whatever the follow graph did ONCE, then flush.
|
|
@@ -65418,8 +65451,8 @@ var C2S = class {
|
|
|
65418
65451
|
|
|
65419
65452
|
// lib/core/lease.mjs
|
|
65420
65453
|
init_node_crypto();
|
|
65421
|
-
var TTL_MS =
|
|
65422
|
-
var RENEW_MS =
|
|
65454
|
+
var TTL_MS = 9e5;
|
|
65455
|
+
var RENEW_MS = 3e5;
|
|
65423
65456
|
var JITTER = () => 0.85 + Math.random() * 0.3;
|
|
65424
65457
|
var UNREADABLE = /* @__PURE__ */ Symbol("lease-unreadable");
|
|
65425
65458
|
var Lease = class {
|
|
@@ -66495,6 +66528,7 @@ var BskyFeed = class {
|
|
|
66495
66528
|
this.lastSweep = (/* @__PURE__ */ new Date()).toISOString();
|
|
66496
66529
|
const self2 = this.atproto.read()?.did;
|
|
66497
66530
|
let added = 0;
|
|
66531
|
+
this.store.hold?.();
|
|
66498
66532
|
try {
|
|
66499
66533
|
const ownMirrors = new Set(this.store.getStatuses().map((s) => s.atproto?.uri).filter(Boolean));
|
|
66500
66534
|
const tl = await this.atproto.xrpc("app.bsky.feed.getTimeline", { params: { limit: PER_SWEEP } });
|
|
@@ -66543,6 +66577,8 @@ var BskyFeed = class {
|
|
|
66543
66577
|
} catch (e) {
|
|
66544
66578
|
this._backOff(e.status || 0, null);
|
|
66545
66579
|
return;
|
|
66580
|
+
} finally {
|
|
66581
|
+
this.store.release?.();
|
|
66546
66582
|
}
|
|
66547
66583
|
const all = this.store.getStatuses();
|
|
66548
66584
|
const mirrored = all.filter((s) => s.kind === "bsky");
|
|
@@ -68840,7 +68876,29 @@ var PodTransport = class {
|
|
|
68840
68876
|
const podTarget = this.toPod ? this.toPod(targetUrl) : targetUrl;
|
|
68841
68877
|
const url = await this.aclUrlFor(podTarget);
|
|
68842
68878
|
if (!await this.aclWritable(url)) return null;
|
|
68843
|
-
|
|
68879
|
+
const doc = this.aclDoc(podTarget, publicModes, { ...opts, aclUrl: url });
|
|
68880
|
+
if (opts.ifChanged && await this.aclSame(url, doc)) return { status: 304, unchanged: true };
|
|
68881
|
+
return this.put(url, doc, "text/turtle");
|
|
68882
|
+
}
|
|
68883
|
+
// Whether the pod's rule at `aclUrl` states exactly what `doc` states.
|
|
68884
|
+
// Compared as graphs, not bytes: the pod serialises what it holds its own
|
|
68885
|
+
// way. Every rule this file writes names its subjects, so triple sets are
|
|
68886
|
+
// enough; anything unreadable or with blank nodes reads as different.
|
|
68887
|
+
async aclSame(aclUrl, doc) {
|
|
68888
|
+
try {
|
|
68889
|
+
const res = await this.fetch(aclUrl, { headers: { accept: "text/turtle" } });
|
|
68890
|
+
if (res.status !== 200) return false;
|
|
68891
|
+
const triples = (text) => {
|
|
68892
|
+
const g = graph();
|
|
68893
|
+
parse2(text, g, aclUrl, "text/turtle");
|
|
68894
|
+
if (g.statements.some((st2) => st2.subject.termType === "BlankNode" || st2.object.termType === "BlankNode")) return null;
|
|
68895
|
+
return g.statements.map((st2) => `${st2.subject.value} ${st2.predicate.value} ${st2.object.value}`).sort().join("\n");
|
|
68896
|
+
};
|
|
68897
|
+
const theirs = triples(await res.text());
|
|
68898
|
+
return theirs !== null && theirs === triples(doc);
|
|
68899
|
+
} catch {
|
|
68900
|
+
return false;
|
|
68901
|
+
}
|
|
68844
68902
|
}
|
|
68845
68903
|
// Child documents of an LDP container (URLs under it, excluding aux docs).
|
|
68846
68904
|
// Revalidated: the inbox is polled every couple of minutes and is usually
|
|
@@ -68864,10 +68922,15 @@ var PodTransport = class {
|
|
|
68864
68922
|
parse2(body, g, url, "text/turtle");
|
|
68865
68923
|
const here = namedNode2(url);
|
|
68866
68924
|
const seen = /* @__PURE__ */ new Set();
|
|
68925
|
+
const receipts = /* @__PURE__ */ new Set();
|
|
68867
68926
|
const list3 = [];
|
|
68868
68927
|
for (const child of g.each(here, LDP2("contains"), null, here)) {
|
|
68869
68928
|
const u = child.value;
|
|
68870
|
-
if (
|
|
68929
|
+
if (u.endsWith(".receipt.json")) {
|
|
68930
|
+
receipts.add(u);
|
|
68931
|
+
continue;
|
|
68932
|
+
}
|
|
68933
|
+
if (!u.startsWith(url) || u === url || /\.(acl|meta)$/.test(u) || seen.has(u)) continue;
|
|
68871
68934
|
seen.add(u);
|
|
68872
68935
|
list3.push({
|
|
68873
68936
|
url: u,
|
|
@@ -68875,10 +68938,16 @@ var PodTransport = class {
|
|
|
68875
68938
|
modified: g.any(child, DC("modified"), null, here)?.value || null
|
|
68876
68939
|
});
|
|
68877
68940
|
}
|
|
68941
|
+
for (const item of list3) item.receipt = receipts.has(item.url + ".receipt.json");
|
|
68942
|
+
const orphans = [...receipts].filter((r) => !seen.has(r.slice(0, -".receipt.json".length)));
|
|
68878
68943
|
list3.sort((a, b) => String(a.modified || "").localeCompare(String(b.modified || "")));
|
|
68879
|
-
this._listCache.set(url, { etag: res.headers.get("etag"), children: list3 });
|
|
68944
|
+
this._listCache.set(url, { etag: res.headers.get("etag"), children: list3, orphans });
|
|
68880
68945
|
return list3;
|
|
68881
68946
|
}
|
|
68947
|
+
/** Receipts in the last listing of `url` whose item is gone. */
|
|
68948
|
+
orphanReceipts(url) {
|
|
68949
|
+
return this._listCache?.get(url)?.orphans ?? [];
|
|
68950
|
+
}
|
|
68882
68951
|
/**
|
|
68883
68952
|
* The WebID profile advertises the actor as an account:
|
|
68884
68953
|
* <webId> foaf:account <actor> .
|
|
@@ -69208,6 +69277,7 @@ var Deliverer = class {
|
|
|
69208
69277
|
this.edPrivate = edPrivate;
|
|
69209
69278
|
this.proofKeyId = proofKeyId;
|
|
69210
69279
|
this.log = log2;
|
|
69280
|
+
this.batchSize = 1;
|
|
69211
69281
|
if (!passive) this.startQueue();
|
|
69212
69282
|
}
|
|
69213
69283
|
startQueue() {
|
|
@@ -69284,31 +69354,56 @@ var Deliverer = class {
|
|
|
69284
69354
|
return activity;
|
|
69285
69355
|
}
|
|
69286
69356
|
}
|
|
69357
|
+
// One attempt per target, answered in order: `{ ok: true }` or `{ error }`
|
|
69358
|
+
// with the error deliverNow would have thrown. Here one at a time; the
|
|
69359
|
+
// relay deliverer sends the whole list in one call.
|
|
69360
|
+
async deliverManyNow(targets) {
|
|
69361
|
+
const out = [];
|
|
69362
|
+
for (const t of targets) {
|
|
69363
|
+
try {
|
|
69364
|
+
await this.deliverNow(t.inbox, t.activity);
|
|
69365
|
+
out.push({ ok: true });
|
|
69366
|
+
} catch (error2) {
|
|
69367
|
+
out.push({ error: error2 });
|
|
69368
|
+
}
|
|
69369
|
+
}
|
|
69370
|
+
return out;
|
|
69371
|
+
}
|
|
69287
69372
|
async deliver(inbox, activity) {
|
|
69288
69373
|
const signed = await this.proofed(activity);
|
|
69374
|
+
if (this._queueIfCooling(inbox, signed)) return;
|
|
69375
|
+
const [result] = await this.deliverManyNow([{ inbox, activity: signed }]);
|
|
69376
|
+
await this._settle(inbox, signed, result);
|
|
69377
|
+
}
|
|
69378
|
+
// A host we already know is refusing: queue without asking again. This is
|
|
69379
|
+
// the path a FRESH activity takes, so without it a fan-out to a struggling
|
|
69380
|
+
// server opened one socket per follower before any of this applied.
|
|
69381
|
+
_queueIfCooling(inbox, signed) {
|
|
69289
69382
|
const host = hostOf(inbox);
|
|
69290
69383
|
const until = this._cooling?.get(host);
|
|
69291
|
-
if (until
|
|
69292
|
-
|
|
69293
|
-
|
|
69384
|
+
if (!until || until <= Date.now()) return false;
|
|
69385
|
+
this.log(`${host} is cooling \u2014 queueing ${signed.type} rather than asking again`);
|
|
69386
|
+
this._enqueue({ inbox, activity: signed, attempts: 1, nextAt: until });
|
|
69387
|
+
return true;
|
|
69388
|
+
}
|
|
69389
|
+
// What one attempt's outcome means for the queue.
|
|
69390
|
+
async _settle(inbox, signed, result) {
|
|
69391
|
+
if (result.ok) {
|
|
69392
|
+
this.log(`delivered ${signed.type} \u2192 ${inbox}`);
|
|
69294
69393
|
return;
|
|
69295
69394
|
}
|
|
69296
|
-
|
|
69297
|
-
|
|
69298
|
-
this.
|
|
69299
|
-
|
|
69300
|
-
|
|
69301
|
-
|
|
69302
|
-
|
|
69303
|
-
|
|
69304
|
-
this.
|
|
69305
|
-
|
|
69306
|
-
if (aboutTheHost(e)) {
|
|
69307
|
-
this._cooling ||= /* @__PURE__ */ new Map();
|
|
69308
|
-
this._cooling.set(host, Date.now() + wait);
|
|
69309
|
-
}
|
|
69310
|
-
this._enqueue({ inbox, activity: signed, attempts: 1, nextAt: Date.now() + wait });
|
|
69395
|
+
const e = result.error;
|
|
69396
|
+
if (unsalvageable(e)) {
|
|
69397
|
+
await this._unsalvageable(inbox, e);
|
|
69398
|
+
return;
|
|
69399
|
+
}
|
|
69400
|
+
this.log(`delivery failed (${e.message}) \u2014 queued`);
|
|
69401
|
+
const wait = e.retryAfterMs || 6e4;
|
|
69402
|
+
if (aboutTheHost(e)) {
|
|
69403
|
+
this._cooling ||= /* @__PURE__ */ new Map();
|
|
69404
|
+
this._cooling.set(hostOf(inbox), Date.now() + wait);
|
|
69311
69405
|
}
|
|
69406
|
+
this._enqueue({ inbox, activity: signed, attempts: 1, nextAt: Date.now() + wait });
|
|
69312
69407
|
}
|
|
69313
69408
|
// Not retried, and on a 410 the followers who received there are dropped,
|
|
69314
69409
|
// so the next fan-out stops asking; the followers collection is republished
|
|
@@ -69344,7 +69439,13 @@ var Deliverer = class {
|
|
|
69344
69439
|
}
|
|
69345
69440
|
async deliverToAll(inboxes, activity) {
|
|
69346
69441
|
const signed = await this.proofed(activity);
|
|
69347
|
-
|
|
69442
|
+
const targets = [...new Set(inboxes)].map((inbox) => ({ inbox, activity: signed }));
|
|
69443
|
+
for (let i = 0; i < targets.length; i += this.batchSize) {
|
|
69444
|
+
const chunk = targets.slice(i, i + this.batchSize).filter((t) => !this._queueIfCooling(t.inbox, signed));
|
|
69445
|
+
if (!chunk.length) continue;
|
|
69446
|
+
const results = await this.deliverManyNow(chunk);
|
|
69447
|
+
for (let k = 0; k < chunk.length; k++) await this._settle(chunk[k].inbox, signed, results[k]);
|
|
69448
|
+
}
|
|
69348
69449
|
}
|
|
69349
69450
|
// Serialized, for the same reason Intake.drain is: the tick is 60s and a
|
|
69350
69451
|
// drain over slow peers outlasts it, so a second run started on top of the
|
|
@@ -69444,6 +69545,7 @@ var Deliverer = class {
|
|
|
69444
69545
|
|
|
69445
69546
|
// web/app/deliver-relay.mjs
|
|
69446
69547
|
init_fedify_sig();
|
|
69548
|
+
var RELAY_MAX_REQUESTS = 20;
|
|
69447
69549
|
function doorKeyOf(doorInboxUrl) {
|
|
69448
69550
|
try {
|
|
69449
69551
|
const seg2 = new URL(doorInboxUrl).pathname.split("/");
|
|
@@ -69458,6 +69560,7 @@ var RelayDeliverer = class extends Deliverer {
|
|
|
69458
69560
|
this.relayUrl = opts.relayUrl;
|
|
69459
69561
|
this.handle = opts.handle;
|
|
69460
69562
|
this.sessionFetch = opts.sessionFetch;
|
|
69563
|
+
this.batchSize = RELAY_MAX_REQUESTS;
|
|
69461
69564
|
}
|
|
69462
69565
|
// Same contract as Deliverer.signedFetch, DEFAULT INCLUDED: an init with no
|
|
69463
69566
|
// method is a read. The Node one builds a `Request`, whose default is GET, and
|
|
@@ -69467,9 +69570,41 @@ var RelayDeliverer = class extends Deliverer {
|
|
|
69467
69570
|
// never saw the document it asked for: a Follow from anyone new was rejected
|
|
69468
69571
|
// with "actor fetch failed", and nothing needing a lookup could be ingested.
|
|
69469
69572
|
async signedFetch(url, init = {}) {
|
|
69573
|
+
const req = await this._signedRequest(url, init);
|
|
69574
|
+
const [r0] = await this._relay([req]);
|
|
69575
|
+
return this._outcome(r0, url, init.method || "GET");
|
|
69576
|
+
}
|
|
69577
|
+
// A fan-out in one call: the relay takes a list, so a post to twenty
|
|
69578
|
+
// followers is one call, not twenty (Deliverer.deliverToAll, batchSize).
|
|
69579
|
+
async deliverManyNow(targets) {
|
|
69580
|
+
const reqs = await Promise.all(targets.map((t) => this._signedRequest(t.inbox, {
|
|
69581
|
+
method: "POST",
|
|
69582
|
+
headers: { "content-type": "application/activity+json" },
|
|
69583
|
+
body: JSON.stringify(t.activity)
|
|
69584
|
+
})));
|
|
69585
|
+
let results;
|
|
69586
|
+
try {
|
|
69587
|
+
results = await this._relay(reqs);
|
|
69588
|
+
} catch (error2) {
|
|
69589
|
+
return targets.map(() => ({ error: error2 }));
|
|
69590
|
+
}
|
|
69591
|
+
return targets.map((t, i) => {
|
|
69592
|
+
try {
|
|
69593
|
+
this._outcome(results[i] || {}, t.inbox, "POST");
|
|
69594
|
+
return { ok: true };
|
|
69595
|
+
} catch (error2) {
|
|
69596
|
+
return { error: error2 };
|
|
69597
|
+
}
|
|
69598
|
+
});
|
|
69599
|
+
}
|
|
69600
|
+
// Signed here, sent verbatim by the relay. Every signed header goes along,
|
|
69601
|
+
// `accept` included: the signature covers it, so a relay request missing it
|
|
69602
|
+
// carries an invalid signature — and a read without it gets the HTML page
|
|
69603
|
+
// instead of the document.
|
|
69604
|
+
async _signedRequest(url, init = {}) {
|
|
69470
69605
|
const body = typeof init.body === "string" ? init.body : init.body ? new TextDecoder().decode(init.body) : "";
|
|
69471
69606
|
const s = await sign({ url, method: init.method || "GET", headers: init.headers || {}, body }, this.rsaPrivate, this.keyId);
|
|
69472
|
-
|
|
69607
|
+
return {
|
|
69473
69608
|
url: s.url,
|
|
69474
69609
|
method: s.method,
|
|
69475
69610
|
body,
|
|
@@ -69481,25 +69616,42 @@ var RelayDeliverer = class extends Deliverer {
|
|
|
69481
69616
|
signature: s.headers.signature
|
|
69482
69617
|
}
|
|
69483
69618
|
};
|
|
69619
|
+
}
|
|
69620
|
+
// One relay call for a list of requests; the results in the same order.
|
|
69621
|
+
// The relay's OWN answer, apart from the recipients': unreachable and a
|
|
69622
|
+
// refusal are hiccups the queue retries. Its 404 is not — it says this
|
|
69623
|
+
// account has no row here, and no retry changes that. It used to be read as
|
|
69624
|
+
// a hiccup too, and a tab whose account the site did not know retried its
|
|
69625
|
+
// deliveries every minute for three days.
|
|
69626
|
+
async _relay(requests) {
|
|
69484
69627
|
let res;
|
|
69485
69628
|
try {
|
|
69486
69629
|
res = await this.sessionFetch(this.relayUrl, {
|
|
69487
69630
|
method: "POST",
|
|
69488
69631
|
headers: { "content-type": "application/json" },
|
|
69489
|
-
body: JSON.stringify({ handle: this.handle, requests
|
|
69632
|
+
body: JSON.stringify({ handle: this.handle, requests })
|
|
69490
69633
|
});
|
|
69491
69634
|
} catch (e) {
|
|
69492
69635
|
const err = new Error(`relay unreachable: ${e.message}`);
|
|
69493
69636
|
err.status = 0;
|
|
69494
69637
|
throw err;
|
|
69495
69638
|
}
|
|
69639
|
+
if (res.status === 404) {
|
|
69640
|
+
const err = new Error("relay: no such account here");
|
|
69641
|
+
err.status = 404;
|
|
69642
|
+
throw err;
|
|
69643
|
+
}
|
|
69496
69644
|
if (res.status >= 400) {
|
|
69497
69645
|
const err = new Error(`relay ${res.status}`);
|
|
69498
69646
|
err.status = 502;
|
|
69499
69647
|
throw err;
|
|
69500
69648
|
}
|
|
69501
69649
|
const out = await res.json().catch(() => ({}));
|
|
69502
|
-
|
|
69650
|
+
return Array.isArray(out.results) ? out.results : [];
|
|
69651
|
+
}
|
|
69652
|
+
// What the far server answered, as the Node deliverer would have seen it:
|
|
69653
|
+
// a Response for a read, a thrown error carrying the status for a refusal.
|
|
69654
|
+
_outcome(r0, url, method) {
|
|
69503
69655
|
const status2 = r0.status || 0;
|
|
69504
69656
|
if (status2 === 0) {
|
|
69505
69657
|
const err = new Error(r0.error || "relay could not send");
|
|
@@ -69507,7 +69659,7 @@ var RelayDeliverer = class extends Deliverer {
|
|
|
69507
69659
|
throw err;
|
|
69508
69660
|
}
|
|
69509
69661
|
if (status2 >= 400) {
|
|
69510
|
-
const err = new Error(`${
|
|
69662
|
+
const err = new Error(`${method} ${url} \u2192 ${status2}`);
|
|
69511
69663
|
err.status = status2;
|
|
69512
69664
|
if (r0.retryAfter) {
|
|
69513
69665
|
const secs = Number(r0.retryAfter);
|
|
@@ -71775,7 +71927,8 @@ var BrowserAgent = class _BrowserAgent {
|
|
|
71775
71927
|
this.lease.onLost = () => this.demote();
|
|
71776
71928
|
this.lease.startRenewal();
|
|
71777
71929
|
try {
|
|
71778
|
-
await this.store.load({ force:
|
|
71930
|
+
await this.store.load({ force: !!this._watched }).catch((e) => this.log(`re-reading state: ${e.message}`));
|
|
71931
|
+
this._watched = false;
|
|
71779
71932
|
await this.publisher.healStatuses().catch((e) => this.log(`healing the timeline index: ${e.message}`));
|
|
71780
71933
|
await this.publisher.publishProfilePage().catch((e) => this.log(`profile page: ${e.message}`));
|
|
71781
71934
|
this.deliverer?.startQueue?.();
|
|
@@ -71796,6 +71949,7 @@ var BrowserAgent = class _BrowserAgent {
|
|
|
71796
71949
|
demote() {
|
|
71797
71950
|
if (this.viewer) return;
|
|
71798
71951
|
this.viewer = true;
|
|
71952
|
+
this._watched = true;
|
|
71799
71953
|
this.log("another device took over \u2014 read-only here");
|
|
71800
71954
|
this.lease.stopRenewal();
|
|
71801
71955
|
clearInterval(this._openTimer);
|
|
@@ -71913,12 +72067,7 @@ var BrowserAgent = class _BrowserAgent {
|
|
|
71913
72067
|
sessionFetch: session.fetch,
|
|
71914
72068
|
onGone: () => this.publisher.publishCollections({ followers: true })
|
|
71915
72069
|
});
|
|
71916
|
-
this.gatewayApi = null;
|
|
71917
|
-
try {
|
|
71918
|
-
if (config.gateway?.url) this.gatewayApi = `${new URL(config.gateway.url).origin}/api`;
|
|
71919
|
-
} catch {
|
|
71920
|
-
this.gatewayApi = null;
|
|
71921
|
-
}
|
|
72070
|
+
this.gatewayApi = config.gateway?.url ? `${frontOrigin.replace(/\/$/, "")}/api` : null;
|
|
71922
72071
|
this.doorKey = doorKeyOf(config.gateway?.url) || config.handle;
|
|
71923
72072
|
this.gatewayStanding = null;
|
|
71924
72073
|
const standing = await this.openAtGateway();
|
|
@@ -71996,6 +72145,7 @@ var BrowserAgent = class _BrowserAgent {
|
|
|
71996
72145
|
await this.fediaccts.load();
|
|
71997
72146
|
this.viewer = !await this.lease.acquire();
|
|
71998
72147
|
if (this.viewer) {
|
|
72148
|
+
this._watched = true;
|
|
71999
72149
|
this.log(`read-only viewer: another device is active on @${config.handle}`);
|
|
72000
72150
|
this.startViewerPoll();
|
|
72001
72151
|
return;
|