fedipod 1.50.2 → 1.56.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 +2 -1
- package/gateway.md +25 -0
- package/gui.md +11 -3
- package/lib/client/masto/timelines.mjs +5 -0
- package/lib/connections/acctfeed.mjs +4 -1
- package/lib/connections/tagfeed.mjs +4 -1
- package/lib/core/intake/index.mjs +35 -2
- package/lib/core/intake/verify.mjs +3 -1
- package/lib/core/pod-only.mjs +4 -2
- package/lib/core/publisher/index.mjs +19 -5
- package/lib/gateway/account-agent.mjs +11 -5
- package/lib/gateway/check-letter.mjs +123 -0
- package/lib/gateway/connected-feeds.mjs +116 -0
- package/lib/gateway/copy.mjs +19 -0
- package/lib/gateway/forum-pages.mjs +59 -0
- package/lib/gateway/front-core.mjs +7 -4
- package/lib/gateway/held-mail.mjs +25 -1
- package/lib/gateway/keeper.mjs +2 -1
- package/lib/gateway/masto-gateway.mjs +47 -7
- package/lib/surface/routes/owner.mjs +7 -4
- package/package.json +3 -4
- package/scripts/build-app.mjs +3 -0
- package/scripts/check-netlify-bundles.mjs +3 -0
- package/scripts/forum-package.mjs +26 -0
- package/scripts/stage-site.mjs +34 -13
- package/vendor/sanitize-html.cjs +358 -171
- package/web/admin/bar.js +45 -0
- package/web/admin/group.js +17 -0
- package/web/admin/index.html +12 -8
- package/web/admin/record.js +19 -41
- package/web/admin/upkeep.js +3 -0
- package/web/app/README.md +3 -3
- package/web/app/admin-facade.mjs +19 -16
- package/web/app/agent.mjs +65 -2
- package/web/app/atproto-browser.mjs +28 -22
- package/web/app/deliver-relay.mjs +2 -1
- package/web/app/dist/boot.js +1 -0
- package/web/app/dist/boot.js.map +2 -2
- package/web/app/dist/sw.js +567 -265
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/fediacct-browser.mjs +32 -30
- package/web/app/idb-kv.mjs +4 -4
- package/web/app/shims/prelude.js +1 -0
- package/web/app/sw-src.mjs +44 -3
|
@@ -2,33 +2,31 @@
|
|
|
2
2
|
// OTHER servers, in the browser.
|
|
3
3
|
//
|
|
4
4
|
// Same FediAccounts client the Node agent uses (lib/fediacct.mjs): the same
|
|
5
|
-
// OAuth dance and authenticated calls.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// connection is a re-write to the other backend. App registrations and the
|
|
14
|
-
// short-lived OAuth "pending" state stay in IndexedDB (per browser), and the
|
|
15
|
-
// pending state is persisted so a connection survives the worker being
|
|
16
|
-
// idle-killed between the redirect out and the code coming back.
|
|
5
|
+
// OAuth dance and authenticated calls. The credentials live in the owner-only
|
|
6
|
+
// pod state, on the pod only (lib/core/pod-only.mjs), so they follow the owner
|
|
7
|
+
// to any browser they sign in from. Records an earlier build kept in this
|
|
8
|
+
// browser's IndexedDB are still read, and moveToPod() takes them to the pod.
|
|
9
|
+
// App registrations and the short-lived OAuth "pending" state stay in
|
|
10
|
+
// IndexedDB (per browser), and the pending state is persisted so a connection
|
|
11
|
+
// survives the worker being idle-killed between the redirect out and the code
|
|
12
|
+
// coming back.
|
|
17
13
|
import { FediAccounts } from '../../lib/connections/fediacct.mjs';
|
|
18
14
|
import { kvAll, kvPut, kvDel } from './idb-kv.mjs';
|
|
19
15
|
|
|
20
|
-
const POD_DOC = 'fediaccts.json'; // { id: record }
|
|
16
|
+
const POD_DOC = 'fediaccts.json'; // { id: record }
|
|
21
17
|
const PEND_TTL_MS = 10 * 60_000;
|
|
22
18
|
|
|
23
19
|
export class BrowserFediAccounts extends FediAccounts {
|
|
24
20
|
constructor({ store, actorId = null, log = console.log } = {}) {
|
|
25
21
|
super({ localDir: '/fediaccts', actorId, log }); // localDir unused; storage below
|
|
26
22
|
this.store = store;
|
|
27
|
-
this._recs = new Map(); // browser (IndexedDB)
|
|
23
|
+
this._recs = new Map(); // records still in this browser (IndexedDB), id → record
|
|
28
24
|
this._apps = new Map(); // host → app registration
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
// `fromPod`: the pod's records read as well (agent.mjs says when).
|
|
28
|
+
async load({ fromPod = false } = {}) {
|
|
29
|
+
if (fromPod) await this.store.refresh?.([POD_DOC], { ifChanged: true }).catch((e) => this.log(`connected accounts: ${e.message}`));
|
|
32
30
|
const all = await kvAll().catch(() => ({}));
|
|
33
31
|
for (const [k, v] of Object.entries(all)) {
|
|
34
32
|
if (k.startsWith('rec:')) this._recs.set(k.slice(4), v);
|
|
@@ -47,8 +45,8 @@ export class BrowserFediAccounts extends FediAccounts {
|
|
|
47
45
|
|
|
48
46
|
read(id) {
|
|
49
47
|
let rec = null; let where = null;
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const p = this._podMap()[id];
|
|
49
|
+
if (p) { rec = p; where = 'pod'; } else if (this._recs.has(id)) { rec = this._recs.get(id); where = 'browser'; }
|
|
52
50
|
if (!rec) return null;
|
|
53
51
|
if (rec.mintedFor && this.actorId && rec.mintedFor !== this.actorId) {
|
|
54
52
|
this.log(`fediaccts/${id} belongs to ${rec.mintedFor} — not reusing it for ${this.actorId}`);
|
|
@@ -58,17 +56,23 @@ export class BrowserFediAccounts extends FediAccounts {
|
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
write(rec) {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
59
|
+
const clean = { ...rec, storage: 'pod' };
|
|
60
|
+
const map = { ...this._podMap(), [clean.id]: clean };
|
|
61
|
+
this.store.write(POD_DOC, map); this.store.flush?.().catch(() => {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The records still in this browser, onto the pod, and out of the browser
|
|
65
|
+
// once the pod has them. One the pod already holds is newer there.
|
|
66
|
+
async moveToPod() {
|
|
67
|
+
const moving = [...this._recs.values()].filter((r) => !r.mintedFor || !this.actorId || r.mintedFor === this.actorId);
|
|
68
|
+
if (!moving.length) return 0;
|
|
69
|
+
const map = { ...this._podMap() };
|
|
70
|
+
for (const r of moving) if (!(r.id in map)) map[r.id] = { ...r, storage: 'pod' };
|
|
71
|
+
this.store.write(POD_DOC, map);
|
|
72
|
+
if (!await this.store.commit()) return 0;
|
|
73
|
+
for (const r of moving) { this._recs.delete(r.id); await kvDel('rec:' + r.id).catch(() => {}); }
|
|
74
|
+
this.log(`${moving.length} connected account(s) moved from this browser to the pod`);
|
|
75
|
+
return moving.length;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
// Best-effort: tell the server to revoke our token (RFC 7009), so a
|
|
@@ -109,8 +113,6 @@ export class BrowserFediAccounts extends FediAccounts {
|
|
|
109
113
|
}));
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
// Move a connection between backends (the Options menu's Storage control).
|
|
113
|
-
setStorage(id, where) { const rec = this.read(id); if (rec && (where === 'pod' || where === 'browser')) { this.write({ ...rec, storage: where }); return this.roster().find(r => r.id === id) || null; } return null; }
|
|
114
116
|
|
|
115
117
|
// One app registration per host, remembered — over IndexedDB.
|
|
116
118
|
async appFor(host, redirectUri) {
|
package/web/app/idb-kv.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// idb-kv.mjs — a tiny per-origin key/value store on IndexedDB, for the
|
|
2
|
-
// agent
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// idb-kv.mjs — a tiny per-origin key/value store on IndexedDB, for what the
|
|
2
|
+
// browser agent keeps on this device: app registrations, the short OAuth
|
|
3
|
+
// pending state, and connected-account credentials an earlier build kept here
|
|
4
|
+
// until they move to the pod. Shared by atproto-browser and fediacct-browser.
|
|
5
5
|
const DB = 'fedipod-accounts';
|
|
6
6
|
|
|
7
7
|
function open() {
|
package/web/app/shims/prelude.js
CHANGED
|
@@ -54,5 +54,6 @@ globalThis.Buffer ??= (() => {
|
|
|
54
54
|
concat: (list) => { const n = list.reduce((s, x) => s + x.length, 0); const o = new Buf(n); let i = 0; for (const x of list) { o.set(x, i); i += x.length; } return o; },
|
|
55
55
|
alloc: (n) => new Buf(n),
|
|
56
56
|
isBuffer: (x) => x instanceof Uint8Array,
|
|
57
|
+
byteLength: (d, enc) => (typeof d === 'string' ? fromString(d, enc).length : d.byteLength ?? d.length),
|
|
57
58
|
};
|
|
58
59
|
})();
|
package/web/app/sw-src.mjs
CHANGED
|
@@ -10,10 +10,28 @@ import { BrowserAgent } from './agent.mjs';
|
|
|
10
10
|
import { getSession } from './oidc-session.mjs';
|
|
11
11
|
import { ADMIN_PATHS } from './admin-facade.mjs';
|
|
12
12
|
import { bridge } from '../../lib/client/masto/bridge.mjs';
|
|
13
|
+
import { kvGet, kvPut } from './idb-kv.mjs';
|
|
13
14
|
|
|
14
15
|
let agent = null;
|
|
15
16
|
let booting = null;
|
|
16
17
|
|
|
18
|
+
// What the agent has said lately, for the manage page's Show the log: the last
|
|
19
|
+
// 300 lines, kept in this browser across the worker's restarts, each with this
|
|
20
|
+
// browser's own date and time.
|
|
21
|
+
const LOG_KEEP = 300;
|
|
22
|
+
let logLines = [];
|
|
23
|
+
let logTimer = null;
|
|
24
|
+
const logRead = kvGet('log').then((kept) => { logLines = [...(Array.isArray(kept) ? kept : []), ...logLines].slice(-LOG_KEEP); }).catch(() => {});
|
|
25
|
+
function agentLog(m) {
|
|
26
|
+
console.log('[sw-agent]', m);
|
|
27
|
+
const at = new Date().toLocaleString(undefined, { month: '2-digit', day: '2-digit', hour: 'numeric', minute: '2-digit', second: '2-digit' });
|
|
28
|
+
logLines.push(`${at} ${m}`);
|
|
29
|
+
if (logLines.length > LOG_KEEP) logLines.splice(0, logLines.length - LOG_KEEP);
|
|
30
|
+
clearTimeout(logTimer);
|
|
31
|
+
logTimer = setTimeout(() => { logRead.then(() => kvPut('log', logLines)).catch(() => {}); }, 1500);
|
|
32
|
+
}
|
|
33
|
+
const recentLog = async () => { await logRead; return logLines; };
|
|
34
|
+
|
|
17
35
|
const FACADE = /^\/(api\/v[12]\/|oauth\/|nodeinfo\/)/; // Mastodon paths only — never the front's own /api/handle, /api/attach, /api/relay
|
|
18
36
|
const FACADE_EXACT = new Set(['/.well-known/nodeinfo', '/.well-known/oauth-authorization-server', '/nodeinfo/2.0']);
|
|
19
37
|
const isFacade = (p) => FACADE.test(p) || FACADE_EXACT.has(p);
|
|
@@ -109,7 +127,8 @@ self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
|
|
|
109
127
|
async function bootFromSession(frontOrigin, restart) {
|
|
110
128
|
const oidc = await getSession();
|
|
111
129
|
if (!oidc) throw new Error('no session — sign in first');
|
|
112
|
-
const a = new BrowserAgent({ log:
|
|
130
|
+
const a = new BrowserAgent({ log: agentLog });
|
|
131
|
+
a.recentLog = recentLog;
|
|
113
132
|
await a.boot({ oidc, frontOrigin: frontOrigin || self.location.origin, restart });
|
|
114
133
|
agent = a;
|
|
115
134
|
return a;
|
|
@@ -135,7 +154,9 @@ self.addEventListener('message', (e) => {
|
|
|
135
154
|
return;
|
|
136
155
|
}
|
|
137
156
|
if (e.data?.type !== 'boot') return;
|
|
138
|
-
ensureBooting(e.data.frontOrigin)
|
|
157
|
+
const booted = ensureBooting(e.data.frontOrigin);
|
|
158
|
+
e.waitUntil(stillWorking(booted));
|
|
159
|
+
booted
|
|
139
160
|
.then(() => e.source?.postMessage({ type: 'booted' }))
|
|
140
161
|
// `code` matters: a boot that failed only because this browser has never
|
|
141
162
|
// opened the signing key is not a failure the page should show as one — it
|
|
@@ -151,9 +172,29 @@ self.addEventListener('fetch', (e) => {
|
|
|
151
172
|
// sending its owner back to that sign-in: the gateway's, not this browser's
|
|
152
173
|
// agent, whose own client signs in from a page on this origin.
|
|
153
174
|
if (url.pathname.startsWith('/oauth/') && !sameOriginReferrer(e.request)) return;
|
|
154
|
-
|
|
175
|
+
const served = serve(e.request, url);
|
|
176
|
+
e.respondWith(served);
|
|
177
|
+
e.waitUntil(stillWorking(served));
|
|
155
178
|
});
|
|
156
179
|
|
|
180
|
+
// A browser stops a worker about thirty seconds after its last event, however
|
|
181
|
+
// far the agent's own work has got. Each request keeps it up until the agent's
|
|
182
|
+
// start and its inbox rounds are done, for at most four minutes (a browser
|
|
183
|
+
// allows one event five); the next request carries it on.
|
|
184
|
+
const STILL_WORKING_MS = 4 * 60_000;
|
|
185
|
+
function stillWorking(first) {
|
|
186
|
+
const work = (async () => {
|
|
187
|
+
await Promise.resolve(first).catch(() => {});
|
|
188
|
+
const a = agent;
|
|
189
|
+
if (!a) return;
|
|
190
|
+
await a.provisioning?.catch(() => {});
|
|
191
|
+
await a.intake?.settled?.();
|
|
192
|
+
})().catch(() => {});
|
|
193
|
+
let timer;
|
|
194
|
+
const cap = new Promise((r) => { timer = setTimeout(r, STILL_WORKING_MS); });
|
|
195
|
+
return Promise.race([work, cap]).finally(() => clearTimeout(timer));
|
|
196
|
+
}
|
|
197
|
+
|
|
157
198
|
async function serve(request, url) {
|
|
158
199
|
// Before anything is booted or read: is this caller allowed to be here at all?
|
|
159
200
|
const refuse = notAllowed(request, url);
|