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/copy-mode.mjs
CHANGED
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
//
|
|
5
5
|
// The state documents are read and written at the gateway instead of on the
|
|
6
6
|
// pod, and the lease that says which agent acts is the copy's. What stays on
|
|
7
|
-
// the pod (the key, connected-account passwords) is still read there.
|
|
8
|
-
// gateway writes the copy to the pod
|
|
9
|
-
|
|
7
|
+
// the pod (the key, connected-account passwords) is still read there. Every
|
|
8
|
+
// fifteen minutes the gateway writes the copy to the pod and deletes it, and
|
|
9
|
+
// makes it again from the pod when this browser next reads or writes (the
|
|
10
|
+
// hold); the copy's lease stays, so nothing here notices.
|
|
11
|
+
//
|
|
12
|
+
// With the hold off, the gateway keeps no copy, and this browser works on the
|
|
13
|
+
// pod while the gateway acts there too for an app. The pod's lease decides
|
|
14
|
+
// which acts, and every write first checks this browser still holds it
|
|
15
|
+
// (fencePod), as the copy checks every write.
|
|
16
|
+
import { HttpStorage, StateApiStorage, FencedStorage, podOnly } from '../../lib/core/storage.mjs';
|
|
10
17
|
import { Lease } from '../../lib/core/lease.mjs';
|
|
11
18
|
import { kvGet, kvPut, kvDel } from './idb-kv.mjs';
|
|
12
19
|
|
|
@@ -76,6 +83,8 @@ export function copyStorage(agent, podState) {
|
|
|
76
83
|
// Refused: another agent (an app at the gateway, another browser) took
|
|
77
84
|
// the lease. This one stops acting at once rather than at its next renewal.
|
|
78
85
|
onRefused: () => standDown(agent),
|
|
86
|
+
// No copy any more, and none to be made: the hold turned off.
|
|
87
|
+
onGone: () => { backToPod(agent).catch((e) => agent.log(`back to the pod: ${e.message}`)); },
|
|
79
88
|
});
|
|
80
89
|
Object.defineProperty(s, 'token', { get: () => agent.copy.token, set() {} });
|
|
81
90
|
return s;
|
|
@@ -85,12 +94,44 @@ export function copyLeaseOf(agent) {
|
|
|
85
94
|
return new Lease({ url: `${agent.copy.base}lease.json`, fetchImpl: tokenFetch(agent), log: agent.log, id: agent.holderId });
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
const podFetchOf = (agent) => (u, i) => agent.remote.fetch(u, i);
|
|
98
|
+
|
|
99
|
+
/** The account's state on the pod, fenced when the gateway may act there too. */
|
|
100
|
+
export function podStorageOf(agent) {
|
|
101
|
+
const s = new FencedStorage(new HttpStorage(agent.urls.state, podFetchOf(agent)), { onRefused: () => standDown(agent) });
|
|
102
|
+
if (agent._fenced) s.fence = () => stillMine(agent);
|
|
103
|
+
return s;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Whether this browser still holds the pod's lease, asked before a write.
|
|
107
|
+
// Unreadable is a yes: the write goes, as it would have without the check.
|
|
108
|
+
async function stillMine(agent) {
|
|
109
|
+
const cur = await agent.lease.readFresh().catch(() => null);
|
|
110
|
+
if (!cur || typeof cur !== 'object') return true;
|
|
111
|
+
return cur.holder === agent.lease.id && Date.now() < cur.expiresAt;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether this browser's writes to the pod are fenced: the account kept, with
|
|
116
|
+
* the hold off, and no copy. Set on the store's storage as it is now.
|
|
117
|
+
*/
|
|
118
|
+
export function fencePod(agent) {
|
|
119
|
+
agent._fenced = !!(agent._keeper?.kept && agent._keeper.hold === false && !agent.copy && !agent.store?.getConfig()?.keeperOff);
|
|
120
|
+
const s = agent.store?.storage;
|
|
121
|
+
if (s instanceof FencedStorage) s.fence = agent._fenced ? () => stillMine(agent) : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A lease held by the gateway, which gives it back when its piece of work is
|
|
125
|
+
// done: the copy's ('gateway', copy.mjs), or with the hold off the pod's
|
|
126
|
+
// (lib/gateway/account-agent.mjs: keeper:<its WebID>).
|
|
127
|
+
const byGateway = (holder) => holder === 'gateway' || String(holder || '').startsWith('keeper:');
|
|
128
|
+
|
|
88
129
|
// Swap the lease the agent and its drain go by.
|
|
89
130
|
function useLease(agent, lease) {
|
|
90
131
|
agent.lease.stopRenewal();
|
|
91
132
|
agent.lease = lease;
|
|
92
133
|
if (agent.intake) agent.intake.lease = lease;
|
|
93
|
-
lease.onLost = () => agent.demote();
|
|
134
|
+
lease.onLost = () => (agent.copy || agent._fenced ? standDown(agent) : agent.demote());
|
|
94
135
|
}
|
|
95
136
|
|
|
96
137
|
/**
|
|
@@ -137,8 +178,8 @@ export function standDown(agent) {
|
|
|
137
178
|
await new Promise((r) => setTimeout(r, wait));
|
|
138
179
|
if (!agent.viewer) return;
|
|
139
180
|
const cur = await agent.lease.readFresh().catch(() => null);
|
|
140
|
-
// Held by anyone but the gateway
|
|
141
|
-
if (cur && typeof cur === 'object' && cur.holder
|
|
181
|
+
// Held by anyone but the gateway is another browser's.
|
|
182
|
+
if (cur && typeof cur === 'object' && !byGateway(cur.holder) && Date.now() < cur.expiresAt) return;
|
|
142
183
|
if (await agent.lease.acquire()) {
|
|
143
184
|
clearTimeout(agent._viewerTimer); agent._viewerTimer = null;
|
|
144
185
|
// Read what the app wrote, then carry on as a restart moments later
|
|
@@ -195,16 +236,39 @@ export async function leaveCopy(agent) {
|
|
|
195
236
|
return { ok: false, why: `the gateway could not write the copy to the pod (${res.status || res.e?.message})` };
|
|
196
237
|
}
|
|
197
238
|
agent.copy = null;
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
agent.store.attach(new HttpStorage(agent.urls.state, podFetch));
|
|
201
|
-
await agent.store.load({ force: true });
|
|
239
|
+
const lease = new Lease({ url: `${agent.urls.state}lease.json`, fetchImpl: podFetchOf(agent), log: agent.log, id: agent.holderId });
|
|
240
|
+
agent.store.attach(podStorageOf(agent));
|
|
202
241
|
useLease(agent, lease);
|
|
242
|
+
fencePod(agent);
|
|
243
|
+
await agent.store.load({ force: true });
|
|
203
244
|
if (await lease.acquire()) lease.startRenewal(); else agent.demote();
|
|
204
245
|
agent.log('working from the pod again');
|
|
205
246
|
return { ok: true };
|
|
206
247
|
}
|
|
207
248
|
|
|
249
|
+
/**
|
|
250
|
+
* The gateway keeps no copy of this account any more and will make none (its
|
|
251
|
+
* admin turned the hold off): back to the pod, where the gateway wrote the
|
|
252
|
+
* copy. Writes not yet sent are dropped, as when another agent acts, and the
|
|
253
|
+
* state is read afresh from the pod.
|
|
254
|
+
*/
|
|
255
|
+
export async function backToPod(agent) {
|
|
256
|
+
if (!agent.copy || agent._backToPod) return;
|
|
257
|
+
agent._backToPod = true;
|
|
258
|
+
try {
|
|
259
|
+
await agent.store.discardPending();
|
|
260
|
+
agent.lease.stopRenewal();
|
|
261
|
+
agent.copy = null;
|
|
262
|
+
const lease = new Lease({ url: `${agent.urls.state}lease.json`, fetchImpl: podFetchOf(agent), log: agent.log, id: agent.holderId });
|
|
263
|
+
agent.store.attach(podStorageOf(agent));
|
|
264
|
+
useLease(agent, lease);
|
|
265
|
+
fencePod(agent);
|
|
266
|
+
await agent.store.load({ force: true });
|
|
267
|
+
if (await lease.acquire()) lease.startRenewal(); else agent.demote();
|
|
268
|
+
agent.log('the gateway keeps no copy of this account now: working from the pod');
|
|
269
|
+
} finally { agent._backToPod = false; }
|
|
270
|
+
}
|
|
271
|
+
|
|
208
272
|
/**
|
|
209
273
|
* The gateway has a new identity, and this account's copy was kept under the
|
|
210
274
|
* old one, which the new one cannot write back for (the owner's rules name the
|
|
@@ -218,8 +282,8 @@ export async function leaveCopy(agent) {
|
|
|
218
282
|
export async function handOverCopy(agent) {
|
|
219
283
|
if (!agent.copy) return { ok: true };
|
|
220
284
|
await agent.store.commit();
|
|
221
|
-
const podFetch = (
|
|
222
|
-
const pod =
|
|
285
|
+
const podFetch = podFetchOf(agent);
|
|
286
|
+
const pod = podStorageOf(agent);
|
|
223
287
|
const docs = agent.store.names().filter((n) => !podOnly(n)).map((n) => [n, agent.store.read(n, null)]).filter(([, v]) => v !== null);
|
|
224
288
|
for (const [name, value] of docs) {
|
|
225
289
|
const w = await pod.write(name, JSON.stringify(value, null, 2) + '\n', 'application/json');
|
|
@@ -232,13 +296,34 @@ export async function handOverCopy(agent) {
|
|
|
232
296
|
if (res.status !== 200) return { ok: false, why: `the gateway would not let the copy go (${res.status || res.e?.message})` };
|
|
233
297
|
agent.copy = null;
|
|
234
298
|
agent.store.attach(pod);
|
|
235
|
-
await agent.store.load({ force: true });
|
|
236
299
|
useLease(agent, podLease);
|
|
300
|
+
fencePod(agent);
|
|
301
|
+
await agent.store.load({ force: true });
|
|
237
302
|
podLease.startRenewal();
|
|
238
303
|
agent.log(`handed the account's copy over: ${docs.length} documents written to the pod`);
|
|
239
304
|
return { ok: true };
|
|
240
305
|
}
|
|
241
306
|
|
|
307
|
+
/**
|
|
308
|
+
* The gateway's admin turned the hold off or on since this browser last
|
|
309
|
+
* heard (agent.mjs: hereAtGateway): off, the copy goes back to the pod and
|
|
310
|
+
* writes there are fenced; on, the account moves onto a copy again.
|
|
311
|
+
*/
|
|
312
|
+
export async function followHold(agent, frontOrigin, hold) {
|
|
313
|
+
if (!agent._keeper) return;
|
|
314
|
+
const was = agent._keeper.hold;
|
|
315
|
+
agent._keeper.hold = hold;
|
|
316
|
+
// Asked again at every check-in until the copy has gone.
|
|
317
|
+
if (!hold && agent.copy) {
|
|
318
|
+
const left = await leaveCopy(agent);
|
|
319
|
+
if (!left.ok) agent.log(`the gateway keeps no copies now; leaving this one: ${left.why}`);
|
|
320
|
+
} else if (hold && was === false && !agent.copy && agent._keeper.kept && !agent.viewer && !agent.store.getConfig()?.keeperOff) {
|
|
321
|
+
await forgetFailedOpen(agent, frontOrigin);
|
|
322
|
+
await moveIntoCopy(agent, frontOrigin);
|
|
323
|
+
}
|
|
324
|
+
fencePod(agent);
|
|
325
|
+
}
|
|
326
|
+
|
|
242
327
|
/** A token near its end, renewed; the hourly check-in asks. */
|
|
243
328
|
export async function renewCopyToken(agent, frontOrigin) {
|
|
244
329
|
if (!agent.copy || agent.copy.expiresAt - Date.now() > RENEW_BEFORE_MS) return;
|
package/web/app/dist/boot.js
CHANGED
|
@@ -32576,6 +32576,9 @@ function apUrls(remotePod, root, { publicBase = null } = {}) {
|
|
|
32576
32576
|
// reads them there, with a credential the pod checks.
|
|
32577
32577
|
ownOutbox: home + "ap/private/outbox",
|
|
32578
32578
|
liked: home + "ap/private/liked",
|
|
32579
|
+
// The inbox as its owner reads it (§5.2): what the account received,
|
|
32580
|
+
// kept by the drain as a paged collection beside the archive.
|
|
32581
|
+
ownInbox: home + "ap/private/inbox",
|
|
32579
32582
|
profileHtml: face + "ap/profile.html",
|
|
32580
32583
|
// Media stays on the pod even when fronted: attachment urls are not
|
|
32581
32584
|
// identity-checked by remotes, and proxying blobs would be pure cost.
|
|
@@ -34144,12 +34147,13 @@ async function attachRows({ fetch: f, front, config, plain }, { log: log2 = () =
|
|
|
34144
34147
|
}
|
|
34145
34148
|
return { gateway: { front: origin, mode: "trust", secrets }, handles: rows.map((r) => r.handle) };
|
|
34146
34149
|
}
|
|
34147
|
-
async function keepRows({ fetch: f, front, config, on = true }) {
|
|
34150
|
+
async function keepRows({ fetch: f, front, config, on = true, between = null }) {
|
|
34148
34151
|
const origin = String(front).replace(/\/+$/u, "");
|
|
34149
34152
|
const forumHandle = config.handle;
|
|
34150
34153
|
const handles = [...(config.categories || []).map((c) => c.slug), forumHandle];
|
|
34151
34154
|
let keeper = null;
|
|
34152
34155
|
for (const handle of handles) {
|
|
34156
|
+
if (handle === forumHandle && between) await between(keeper);
|
|
34153
34157
|
const res = await f(`${origin}/api/keeper`, {
|
|
34154
34158
|
method: "POST",
|
|
34155
34159
|
headers: { "content-type": "application/json" },
|
|
@@ -34184,8 +34188,13 @@ async function setUpForumAtGateway({ remote, storageFor, fetch: f, front, ownerW
|
|
|
34184
34188
|
await writeConfig2(storage, { ...config, gateway, republish: true });
|
|
34185
34189
|
onStep("attach", "ok");
|
|
34186
34190
|
onStep("keep", "running");
|
|
34187
|
-
const { keeper } = await keepRows({
|
|
34188
|
-
|
|
34191
|
+
const { keeper } = await keepRows({
|
|
34192
|
+
fetch: f,
|
|
34193
|
+
front,
|
|
34194
|
+
config,
|
|
34195
|
+
on: true,
|
|
34196
|
+
between: (k) => nameKeeperInRules(remote, { config, plain, keeper: k, ownerWebId })
|
|
34197
|
+
});
|
|
34189
34198
|
onStep("keep", "ok");
|
|
34190
34199
|
return { handle: config.handle, front: gateway.front, handles, keeper };
|
|
34191
34200
|
}
|