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.
- package/README.md +8 -13
- package/architecture.md +5 -2
- package/gateway.md +56 -24
- package/gui.md +3 -2
- package/lib/client/c2s.mjs +23 -74
- package/lib/client/masto/index.mjs +5 -4
- package/lib/client/masto/render.mjs +4 -1
- package/lib/core/intake/activities.mjs +8 -9
- package/lib/core/intake/counts.mjs +143 -0
- package/lib/core/intake/index.mjs +12 -1
- package/lib/core/intake/received.mjs +78 -0
- package/lib/core/publisher/notes.mjs +4 -1
- package/lib/core/social.mjs +2 -0
- package/lib/core/storage.mjs +54 -5
- package/lib/core/wire.mjs +16 -5
- package/lib/gateway/account-agent.mjs +20 -4
- package/lib/gateway/copy.mjs +95 -5
- package/lib/gateway/front-core.mjs +18 -9
- package/lib/gateway/gateway-core.mjs +46 -14
- package/lib/gateway/held-mail.mjs +11 -4
- package/lib/gateway/httpsig.mjs +6 -1
- package/lib/gateway/keeper.mjs +9 -6
- package/lib/gateway/masto-gateway.mjs +140 -40
- package/lib/gateway/private-read.mjs +98 -0
- package/lib/gateway/quiet.mjs +4 -3
- package/lib/gateway/state-api.mjs +73 -18
- package/lib/pod/notes.mjs +25 -0
- package/lib/pod/urls.mjs +3 -0
- package/lib/server/embed.mjs +25 -0
- package/lib/surface/routes/owner.mjs +2 -2
- package/package.json +2 -2
- package/web/admin/gateway.js +8 -24
- package/web/admin/index.html +0 -16
- package/web/admin/record.js +1 -2
- package/web/admin/upkeep.js +1 -5
- package/web/app/admin-facade.mjs +1 -1
- package/web/app/agent.mjs +27 -10
- package/web/app/copy-mode.mjs +98 -13
- package/web/app/dist/boot.js +12 -3
- package/web/app/dist/boot.js.map +2 -2
- package/web/app/dist/sw.js +582 -282
- package/web/app/dist/sw.js.map +4 -4
package/web/app/dist/sw.js
CHANGED
|
@@ -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" &&
|
|
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
|
|
29988
|
+
let load2;
|
|
29976
29989
|
if (typeof options.documentLoader === "function") {
|
|
29977
|
-
|
|
29990
|
+
load2 = options.documentLoader;
|
|
29978
29991
|
} else {
|
|
29979
|
-
|
|
29992
|
+
load2 = jsonld2.documentLoader;
|
|
29980
29993
|
}
|
|
29981
|
-
const remoteDoc = await
|
|
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 =
|
|
33270
|
-
exports.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
|
|
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
|
|
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
|
|
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 (!
|
|
46139
|
+
if (!fresh2.length && !raise) return { added: false, merged: false, status: row };
|
|
46124
46140
|
all[at] = {
|
|
46125
46141
|
...row,
|
|
46126
|
-
...
|
|
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
|
-
//
|
|
46299
|
-
|
|
46300
|
-
|
|
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
|
|
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
|
|
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) >
|
|
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" &&
|
|
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)
|
|
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
|
|
66575
|
-
*
|
|
66576
|
-
*
|
|
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 = `${
|
|
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
|
-
|
|
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
|
-
|
|
66638
|
+
doc = await this.agent.remote.getJson(page2 ? `${own}-${page2}` : own);
|
|
66584
66639
|
} catch (e) {
|
|
66585
|
-
return this.send(res, 502, { error: `the
|
|
66640
|
+
return this.send(res, 502, { error: `the inbox collection could not be read: ${e.message}` });
|
|
66586
66641
|
}
|
|
66587
|
-
if (!
|
|
66588
|
-
if (
|
|
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
|
-
|
|
66623
|
-
const
|
|
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:
|
|
68417
|
-
favourites_count:
|
|
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.
|
|
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
|
|
71067
|
-
if (!
|
|
71068
|
-
for (const r of
|
|
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,
|
|
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:
|
|
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
|
|
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: () =>
|
|
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
|
|
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
|
|
73169
|
-
|
|
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 = (
|
|
73182
|
-
const pod =
|
|
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
|
|
73203
|
-
if (
|
|
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.
|
|
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.
|
|
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 =
|
|
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,
|