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