fedipod 1.45.0 → 1.48.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 +11 -16
- package/architecture.md +6 -3
- package/gateway.md +51 -23
- package/gui.md +3 -2
- package/lib/client/c2s.mjs +2 -1
- 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 +4 -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 +4 -4
- package/lib/gateway/held-mail.mjs +11 -4
- package/lib/gateway/keeper.mjs +9 -6
- package/lib/gateway/masto-gateway.mjs +140 -40
- package/lib/gateway/private-read.mjs +10 -3
- package/lib/gateway/quiet.mjs +4 -3
- package/lib/gateway/state-api.mjs +73 -18
- package/lib/pod/notes.mjs +25 -0
- package/lib/server/embed.mjs +1 -1
- package/lib/session/README.md +2 -135
- package/lib/surface/routes/owner.mjs +2 -2
- package/lib/surface/surface.mjs +4 -3
- package/package.json +5 -3
- package/scripts/check-netlify-bundles.mjs +0 -4
- package/scripts/check-pod-calls.mjs +5 -108
- package/scripts/check-sizes.mjs +6 -3
- package/scripts/dependents.mjs +76 -0
- package/scripts/stage-site.mjs +10 -8
- package/scripts/start.mjs +2 -2
- package/scripts/test-dependents.mjs +37 -0
- 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 +68 -57
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +583 -283
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/forum-support.mjs +2 -2
- package/web/app/oidc-session.mjs +2 -2
- package/lib/session/demo.html +0 -94
- package/lib/session/fedi-account.mjs +0 -417
- package/lib/session/fedi-login.mjs +0 -123
- package/lib/session/oidc-session.mjs +0 -233
- package/lib/session/package.json +0 -49
package/web/admin/index.html
CHANGED
|
@@ -340,16 +340,8 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
340
340
|
title=" Go back to your pod's own inbox">Detach from gateway</button>
|
|
341
341
|
<!-- Shown only when the agent reports the account's standing at the gateway
|
|
342
342
|
(the browser build does; a DeviceAgent's admin does not). -->
|
|
343
|
-
<button type="button" id="gateway-pause" class="inline" hidden
|
|
344
|
-
title=" Stop keeping the posts sent to you until you resume; follows still arrive">Pause my account</button>
|
|
345
|
-
<button type="button" id="gateway-resume" class="inline" hidden
|
|
346
|
-
title=" Keep the posts sent to you again">Resume my account</button>
|
|
347
343
|
<button type="button" id="gateway-keep-on" class="inline" hidden
|
|
348
344
|
title=" Let this gateway accept follows, try again what failed to go out and publish scheduled posts while FediPod is closed">Keep my account running while I'm away</button>
|
|
349
|
-
<button type="button" id="gateway-keep-off" class="inline" hidden
|
|
350
|
-
title=" Act only while FediPod is open; the rule naming this gateway comes off your pod">Stop keeping it running</button>
|
|
351
|
-
<button type="button" id="gateway-close" class="inline danger" hidden data-confirm="close-address"
|
|
352
|
-
title=" Close your address at this gateway for good; nothing on your pod is touched">Close this address</button>
|
|
353
345
|
</span>
|
|
354
346
|
|
|
355
347
|
<section id="pane-group" hidden>
|
|
@@ -545,14 +537,6 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
545
537
|
<label for="confirm-handle">Type the handle to confirm</label>
|
|
546
538
|
<input type="text" id="confirm-handle" autocomplete="off">
|
|
547
539
|
</div>
|
|
548
|
-
<div id="warn-close-address" class="warn" hidden>
|
|
549
|
-
<p><strong>This cannot be undone.</strong> Your address at the gateway is closed for good:
|
|
550
|
-
mail to it is turned away, other servers drop the account the next time they look, and
|
|
551
|
-
nobody can take the name. Nothing on your pod is touched — your posts, key and data stay
|
|
552
|
-
where they are.</p>
|
|
553
|
-
<label for="confirm-handle-close">Type the handle to confirm</label>
|
|
554
|
-
<input type="text" id="confirm-handle-close" autocomplete="off">
|
|
555
|
-
</div>
|
|
556
540
|
<!-- "Move private data" (POST /state-move) had a panel here and no way to
|
|
557
541
|
open it: nothing in this page has ever carried data-confirm="move-state",
|
|
558
542
|
so the panel was only ever hidden. Removed 2026-09-09 rather than left as
|
package/web/admin/record.js
CHANGED
|
@@ -94,7 +94,6 @@ function resetConfirm() {
|
|
|
94
94
|
for (const k of Object.keys(LIFECYCLE)) $(`warn-${k}`)?.hidden !== undefined && ($(`warn-${k}`).hidden = true);
|
|
95
95
|
$('confirm-handle').value = '';
|
|
96
96
|
$('confirm-handle-move').value = '';
|
|
97
|
-
$('confirm-handle-close').value = '';
|
|
98
97
|
$('move-target').value = '';
|
|
99
98
|
}
|
|
100
99
|
function closePanels(keep = null) {
|
|
@@ -325,7 +324,7 @@ function render() {
|
|
|
325
324
|
dd.textContent = '';
|
|
326
325
|
dd.append(FOLLOWS_CTL);
|
|
327
326
|
FOLLOWS_CTL.hidden = false;
|
|
328
|
-
FOLLOWS_PICK.value = config.autoAcceptFollows ? 'auto' : 'approve';
|
|
327
|
+
FOLLOWS_PICK.value = config.autoAcceptFollows !== false ? 'auto' : 'approve';
|
|
329
328
|
}
|
|
330
329
|
if (k === 'gateway') {
|
|
331
330
|
dd.textContent = '';
|
package/web/admin/upkeep.js
CHANGED
|
@@ -76,9 +76,6 @@ const LIFECYCLE = {
|
|
|
76
76
|
retire: { path: '/retire', title: 'Retire this identity', go: 'Retire it', danger: true, done: (r) => `retired ${r.deletedAt}: Delete delivered to ${r.inboxes} inbox(es)` },
|
|
77
77
|
move: { path: '/move', title: 'Transfer this account away', go: 'Transfer it', focus: 'move-target',
|
|
78
78
|
done: (r) => `transferred to ${r.target}: Move delivered to ${r.inboxes} inbox(es), unfollowed ${r.unfollowed}/${r.following}` },
|
|
79
|
-
// The address at the gateway, not the identity: the pod keeps everything.
|
|
80
|
-
'close-address': { path: '/gateway/close', title: 'Close this address', go: 'Close it', danger: true, focus: 'confirm-handle-close',
|
|
81
|
-
done: (r) => (r.closed ? 'closed — this address is gone for good; everything on your pod is untouched' : 'not closed') },
|
|
82
79
|
};
|
|
83
80
|
let pending = null;
|
|
84
81
|
|
|
@@ -122,8 +119,7 @@ $('confirm-form').addEventListener('submit', async (ev) => {
|
|
|
122
119
|
const what = pending;
|
|
123
120
|
const body = what === 'retire' ? { confirm: $('confirm-handle').value.trim() }
|
|
124
121
|
: what === 'move' ? { target: $('move-target').value.trim(), confirm: $('confirm-handle-move').value.trim() }
|
|
125
|
-
:
|
|
126
|
-
: {};
|
|
122
|
+
: {};
|
|
127
123
|
if (what === 'move' && !body.target) { say('name the account to transfer to', 'err'); return; }
|
|
128
124
|
$('confirm-go').disabled = true;
|
|
129
125
|
say(`${what} — this talks to the pod and to other servers, so it takes a moment`);
|
package/web/app/admin-facade.mjs
CHANGED
|
@@ -120,7 +120,7 @@ export class AdminFacade {
|
|
|
120
120
|
name: cfg.name || null, summary: cfg.summary || null, icon: cfg.icon || null,
|
|
121
121
|
image: cfg.image || null, fields: cfg.fields || [],
|
|
122
122
|
aliases: cfg.aliases || [],
|
|
123
|
-
autoAcceptFollows:
|
|
123
|
+
autoAcceptFollows: cfg.autoAcceptFollows !== false,
|
|
124
124
|
hasUiPassword: !!cfg.uiPassword,
|
|
125
125
|
quiescedAt: cfg.quiescedAt || null, movedTo: cfg.movedTo || null,
|
|
126
126
|
mode: a.status().mode,
|
package/web/app/agent.mjs
CHANGED
|
@@ -13,7 +13,6 @@ import { findAccount } from '../../lib/core/place.mjs';
|
|
|
13
13
|
import * as containers from '../../lib/pod/containers.mjs';
|
|
14
14
|
import * as podState from '../../lib/pod/state.mjs';
|
|
15
15
|
import { PodStore } from '../../lib/core/store.mjs';
|
|
16
|
-
import { HttpStorage } from '../../lib/core/storage.mjs';
|
|
17
16
|
import { Publisher } from '../../lib/core/publisher/index.mjs';
|
|
18
17
|
import { Intake } from '../../lib/core/intake/index.mjs';
|
|
19
18
|
import { C2S } from '../../lib/client/c2s.mjs';
|
|
@@ -34,7 +33,8 @@ import { AcctFeed } from '../../lib/connections/acctfeed.mjs';
|
|
|
34
33
|
import { followActor, unfollowActor, resolveHandle } from '../../lib/core/social.mjs';
|
|
35
34
|
import { podBaseOfWebId } from '../../lib/pod/urls.mjs';
|
|
36
35
|
import { ImportWorker } from '../../lib/connections/import.mjs';
|
|
37
|
-
import { openCopy, copyStorage, copyLeaseOf, moveIntoCopy, leaveCopy, renewCopyToken, standDown, ensureCopyLease, handOverCopy, forgetFailedOpen
|
|
36
|
+
import { openCopy, copyStorage, copyLeaseOf, moveIntoCopy, leaveCopy, renewCopyToken, standDown, ensureCopyLease, handOverCopy, forgetFailedOpen,
|
|
37
|
+
podStorageOf, fencePod, followHold } from './copy-mode.mjs';
|
|
38
38
|
|
|
39
39
|
// The authorities this identity answers on: exactly one, this origin. The Node
|
|
40
40
|
// agent gets this from lib/guard.mjs, which is not in the browser bundle and
|
|
@@ -129,13 +129,19 @@ export class BrowserAgent {
|
|
|
129
129
|
|
|
130
130
|
configured() { return !!this.store?.getConfig(); }
|
|
131
131
|
|
|
132
|
+
// Whether the gateway acts on this account's state too: its copy there, or
|
|
133
|
+
// with the gateway's hold off, its pod (copy-mode.mjs). An action here then
|
|
134
|
+
// makes sure of the lease first (requestTakeover).
|
|
135
|
+
get sharesState() { return !!(this.copy || this._fenced); }
|
|
136
|
+
|
|
132
137
|
// Asked by every write / destructive path before it acts. If we already hold
|
|
133
138
|
// the lease, proceed. If we are a viewer, the owner acting HERE outranks the
|
|
134
139
|
// idle active device: claim the lease outright and become active.
|
|
135
140
|
async requestTakeover() {
|
|
136
|
-
// Working from the copy,
|
|
137
|
-
// since this browser last looked: asked
|
|
138
|
-
|
|
141
|
+
// Working from the copy, or on the pod with the hold off, an app at the
|
|
142
|
+
// gateway may have taken the lease since this browser last looked: asked
|
|
143
|
+
// now, before acting (copy-mode.mjs).
|
|
144
|
+
if (!this.viewer && this.sharesState) await ensureCopyLease(this);
|
|
139
145
|
if (!this.viewer) return true;
|
|
140
146
|
if (!(await this.lease.takeover())) return false;
|
|
141
147
|
clearTimeout(this._viewerTimer); this._viewerTimer = null;
|
|
@@ -182,6 +188,7 @@ export class BrowserAgent {
|
|
|
182
188
|
const said = await this.tellGateway('keeper', { handle: this.doorKey, on: false });
|
|
183
189
|
if (said?.status !== 200) return said || { status: 502 };
|
|
184
190
|
this._keeper.kept = false;
|
|
191
|
+
fencePod(this);
|
|
185
192
|
if (this.masto) this.masto.scheduling = false;
|
|
186
193
|
}
|
|
187
194
|
this.remote.keepers = on ? [webId] : [];
|
|
@@ -194,6 +201,7 @@ export class BrowserAgent {
|
|
|
194
201
|
// A copy that could not be had while the account was not kept may be now.
|
|
195
202
|
await forgetFailedOpen(this, this.frontOrigin);
|
|
196
203
|
this._keeper.kept = true;
|
|
204
|
+
fencePod(this);
|
|
197
205
|
if (this.masto) this.masto.scheduling = true;
|
|
198
206
|
this.log('the gateway keeps this account running while the app is closed');
|
|
199
207
|
}
|
|
@@ -211,6 +219,10 @@ export class BrowserAgent {
|
|
|
211
219
|
async hereAtGateway() {
|
|
212
220
|
const said = await this.tellGateway('here', { handle: this.doorKey, nextAt: this.store ? nextDue(this.store) : null });
|
|
213
221
|
if (said?.status === 200) saveMeta(this.webId, { hereAt: Date.now() }).catch(() => {});
|
|
222
|
+
// The gateway's admin may have turned the hold off or on (copy-mode.mjs).
|
|
223
|
+
if (said?.status === 200 && typeof said.hold === 'boolean') {
|
|
224
|
+
await followHold(this, this.frontOrigin, said.hold).catch((e) => this.log(`the gateway's hold: ${e.message}`));
|
|
225
|
+
}
|
|
214
226
|
return said;
|
|
215
227
|
}
|
|
216
228
|
|
|
@@ -244,9 +256,10 @@ export class BrowserAgent {
|
|
|
244
256
|
// Reading here is being here: the gateway hears so once an hour.
|
|
245
257
|
clearInterval(this._openTimer);
|
|
246
258
|
this._openTimer = setInterval(() => { this.checkInAtGateway(); }, BrowserAgent.OPEN_EVERY_MS);
|
|
247
|
-
// Working from the copy,
|
|
248
|
-
// it acts, and this browser takes it
|
|
249
|
-
|
|
259
|
+
// Working from the copy, or on the pod with the hold off, an app at the
|
|
260
|
+
// gateway takes the lease only while it acts, and this browser takes it
|
|
261
|
+
// back (copy-mode.mjs).
|
|
262
|
+
this.lease.onLost = () => (this.sharesState ? standDown(this) : this.demote());
|
|
250
263
|
this.lease.startRenewal();
|
|
251
264
|
try {
|
|
252
265
|
// Forced, not revalidated, when this device WATCHED first: the active
|
|
@@ -441,7 +454,8 @@ export class BrowserAgent {
|
|
|
441
454
|
// the deletion deny-list that every other write on this agent observes.
|
|
442
455
|
// The Node agent has always passed remote.fetch here.
|
|
443
456
|
const podFetch = (u, i) => this.remote.fetch(u, i);
|
|
444
|
-
|
|
457
|
+
// Fenced once the gateway is known to act on the pod too (fencePod below).
|
|
458
|
+
const podStateStorage = podStorageOf(this);
|
|
445
459
|
this.store = new PodStore({ storage: this.copy ? copyStorage(this, podStateStorage) : podStateStorage, log: this.log });
|
|
446
460
|
keepInStep(this.store, webId, this.log);
|
|
447
461
|
// Single-active-agent lease (lib/lease.mjs): the inbox drain is a
|
|
@@ -545,8 +559,11 @@ export class BrowserAgent {
|
|
|
545
559
|
// The gateway's own pod identity, when it can act for this account while
|
|
546
560
|
// the app is closed (lib/gateway/keeper.mjs). Unless the owner turned that
|
|
547
561
|
// off, every rule this agent writes names it beside the owner.
|
|
548
|
-
|
|
562
|
+
// `hold`: whether it keeps a copy of the account at all (copy-mode.mjs).
|
|
563
|
+
this._keeper = standing?.keeper
|
|
564
|
+
? { webId: standing.keeper, kept: !!standing.kept, keptBy: standing.keptBy || null, hold: standing.hold !== false } : null;
|
|
549
565
|
if (this._keeper && !this.store.getConfig()?.keeperOff) this.remote.keepers = [this._keeper.webId];
|
|
566
|
+
fencePod(this);
|
|
550
567
|
|
|
551
568
|
this.publisher = new Publisher({
|
|
552
569
|
config: this.store.getConfig(), remote: this.remote, store: this.store,
|
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
|
@@ -32544,52 +32544,6 @@ var REL = {
|
|
|
32544
32544
|
};
|
|
32545
32545
|
|
|
32546
32546
|
// lib/pod/urls.mjs
|
|
32547
|
-
function apUrls(remotePod, root, { publicBase = null } = {}) {
|
|
32548
|
-
if (!root) throw new Error("apUrls: a container root is required \u2014 the caller states it, this library does not guess");
|
|
32549
|
-
const base = remotePod.endsWith("/") ? remotePod : remotePod + "/";
|
|
32550
|
-
const home = base + (root.endsWith("/") ? root : root + "/");
|
|
32551
|
-
const face = publicBase ? publicBase.endsWith("/") ? publicBase : publicBase + "/" : home;
|
|
32552
|
-
const urls = {
|
|
32553
|
-
base,
|
|
32554
|
-
home,
|
|
32555
|
-
webfinger: base + ".well-known/webfinger",
|
|
32556
|
-
actor: face + "ap/actor",
|
|
32557
|
-
inbox: face + "ap/inbox/",
|
|
32558
|
-
outbox: face + "ap/outbox",
|
|
32559
|
-
followers: face + "ap/followers",
|
|
32560
|
-
following: face + "ap/following",
|
|
32561
|
-
notes: face + "ap/notes/",
|
|
32562
|
-
privateNotes: face + "ap/private/",
|
|
32563
|
-
featured: face + "ap/featured",
|
|
32564
|
-
// FEP-1b12: the moderator roster a recipient validates announced
|
|
32565
|
-
// moderation against. Published only when moderators are configured.
|
|
32566
|
-
moderators: face + "ap/moderators",
|
|
32567
|
-
// FEP-4ccd and FEP-c648: follows in limbo and the block list, as
|
|
32568
|
-
// collections. They live in the private container so the owner-only ACL
|
|
32569
|
-
// is inherited, not re-stated per document.
|
|
32570
|
-
pendingFollowers: face + "ap/private/pending-followers",
|
|
32571
|
-
pendingFollowing: face + "ap/private/pending-following",
|
|
32572
|
-
blocked: face + "ap/private/blocked",
|
|
32573
|
-
// The outbox as its owner reads it (every message, §5.1) and what the
|
|
32574
|
-
// owner has liked (§5.5): the owner's alone, so they sit here too — and
|
|
32575
|
-
// at the pod's own address even for a fronted identity, because the owner
|
|
32576
|
-
// reads them there, with a credential the pod checks.
|
|
32577
|
-
ownOutbox: home + "ap/private/outbox",
|
|
32578
|
-
liked: home + "ap/private/liked",
|
|
32579
|
-
profileHtml: face + "ap/profile.html",
|
|
32580
|
-
// Media stays on the pod even when fronted: attachment urls are not
|
|
32581
|
-
// identity-checked by remotes, and proxying blobs would be pure cost.
|
|
32582
|
-
media: home + "ap/media/",
|
|
32583
|
-
state: home + "ap-state/"
|
|
32584
|
-
};
|
|
32585
|
-
if (publicBase) {
|
|
32586
|
-
urls.podHome = home;
|
|
32587
|
-
urls.publicHome = face;
|
|
32588
|
-
urls.toPod = (u) => typeof u === "string" && u.startsWith(face) ? home + u.slice(face.length) : u;
|
|
32589
|
-
urls.toPublic = (u) => typeof u === "string" && u.startsWith(home) ? face + u.slice(home.length) : u;
|
|
32590
|
-
}
|
|
32591
|
-
return urls;
|
|
32592
|
-
}
|
|
32593
32547
|
function podBaseOfWebId(webId) {
|
|
32594
32548
|
const u = new URL(webId);
|
|
32595
32549
|
u.hash = "";
|
|
@@ -33724,7 +33678,7 @@ async function moveIn(answers, { session, onStep = () => {
|
|
|
33724
33678
|
return { config, address: `@${handle}@${new URL(origin).host}`, movedFrom: config.movedFrom };
|
|
33725
33679
|
}
|
|
33726
33680
|
|
|
33727
|
-
//
|
|
33681
|
+
// node_modules/fedipod-bb/src/setup.mjs
|
|
33728
33682
|
var setup_exports = {};
|
|
33729
33683
|
__export(setup_exports, {
|
|
33730
33684
|
ROOT: () => ROOT,
|
|
@@ -33737,7 +33691,7 @@ __export(setup_exports, {
|
|
|
33737
33691
|
writeForumConfig: () => writeForumConfig
|
|
33738
33692
|
});
|
|
33739
33693
|
|
|
33740
|
-
// lib/pod/containers.mjs
|
|
33694
|
+
// node_modules/fedipod/lib/pod/containers.mjs
|
|
33741
33695
|
var KEEP = { keep: true };
|
|
33742
33696
|
var KEEP_CT = "application/json";
|
|
33743
33697
|
var keepUrl = (base) => `${base}.keep`;
|
|
@@ -33983,7 +33937,58 @@ var node_crypto_default = {
|
|
|
33983
33937
|
scryptSync
|
|
33984
33938
|
};
|
|
33985
33939
|
|
|
33986
|
-
//
|
|
33940
|
+
// node_modules/fedipod/lib/pod/urls.mjs
|
|
33941
|
+
function apUrls2(remotePod, root, { publicBase = null } = {}) {
|
|
33942
|
+
if (!root) throw new Error("apUrls: a container root is required \u2014 the caller states it, this library does not guess");
|
|
33943
|
+
const base = remotePod.endsWith("/") ? remotePod : remotePod + "/";
|
|
33944
|
+
const home = base + (root.endsWith("/") ? root : root + "/");
|
|
33945
|
+
const face = publicBase ? publicBase.endsWith("/") ? publicBase : publicBase + "/" : home;
|
|
33946
|
+
const urls = {
|
|
33947
|
+
base,
|
|
33948
|
+
home,
|
|
33949
|
+
webfinger: base + ".well-known/webfinger",
|
|
33950
|
+
actor: face + "ap/actor",
|
|
33951
|
+
inbox: face + "ap/inbox/",
|
|
33952
|
+
outbox: face + "ap/outbox",
|
|
33953
|
+
followers: face + "ap/followers",
|
|
33954
|
+
following: face + "ap/following",
|
|
33955
|
+
notes: face + "ap/notes/",
|
|
33956
|
+
privateNotes: face + "ap/private/",
|
|
33957
|
+
featured: face + "ap/featured",
|
|
33958
|
+
// FEP-1b12: the moderator roster a recipient validates announced
|
|
33959
|
+
// moderation against. Published only when moderators are configured.
|
|
33960
|
+
moderators: face + "ap/moderators",
|
|
33961
|
+
// FEP-4ccd and FEP-c648: follows in limbo and the block list, as
|
|
33962
|
+
// collections. They live in the private container so the owner-only ACL
|
|
33963
|
+
// is inherited, not re-stated per document.
|
|
33964
|
+
pendingFollowers: face + "ap/private/pending-followers",
|
|
33965
|
+
pendingFollowing: face + "ap/private/pending-following",
|
|
33966
|
+
blocked: face + "ap/private/blocked",
|
|
33967
|
+
// The outbox as its owner reads it (every message, §5.1) and what the
|
|
33968
|
+
// owner has liked (§5.5): the owner's alone, so they sit here too — and
|
|
33969
|
+
// at the pod's own address even for a fronted identity, because the owner
|
|
33970
|
+
// reads them there, with a credential the pod checks.
|
|
33971
|
+
ownOutbox: home + "ap/private/outbox",
|
|
33972
|
+
liked: home + "ap/private/liked",
|
|
33973
|
+
// The inbox as its owner reads it (§5.2): what the account received,
|
|
33974
|
+
// kept by the drain as a paged collection beside the archive.
|
|
33975
|
+
ownInbox: home + "ap/private/inbox",
|
|
33976
|
+
profileHtml: face + "ap/profile.html",
|
|
33977
|
+
// Media stays on the pod even when fronted: attachment urls are not
|
|
33978
|
+
// identity-checked by remotes, and proxying blobs would be pure cost.
|
|
33979
|
+
media: home + "ap/media/",
|
|
33980
|
+
state: home + "ap-state/"
|
|
33981
|
+
};
|
|
33982
|
+
if (publicBase) {
|
|
33983
|
+
urls.podHome = home;
|
|
33984
|
+
urls.publicHome = face;
|
|
33985
|
+
urls.toPod = (u) => typeof u === "string" && u.startsWith(face) ? home + u.slice(face.length) : u;
|
|
33986
|
+
urls.toPublic = (u) => typeof u === "string" && u.startsWith(home) ? face + u.slice(home.length) : u;
|
|
33987
|
+
}
|
|
33988
|
+
return urls;
|
|
33989
|
+
}
|
|
33990
|
+
|
|
33991
|
+
// node_modules/fedipod-bb/src/urls.mjs
|
|
33987
33992
|
var ROOT = "fedipod-bb/";
|
|
33988
33993
|
var SLUG = /^[a-z0-9][a-z0-9-]{0,62}$/u;
|
|
33989
33994
|
var isSlug = (s) => typeof s === "string" && SLUG.test(s);
|
|
@@ -33993,7 +33998,7 @@ var cacheKey = (postId) => node_crypto_default.createHash("sha256").update(Strin
|
|
|
33993
33998
|
function forumUrls(remotePod, root = ROOT, { publicBase = null, front = null, handle = null } = {}) {
|
|
33994
33999
|
const origin = front ? String(front).replace(/\/$/u, "") : null;
|
|
33995
34000
|
if (origin && !handle) throw new Error("forumUrls: a fronted forum needs its handle");
|
|
33996
|
-
const site =
|
|
34001
|
+
const site = apUrls2(remotePod, root, { publicBase: publicBase || (origin ? `${origin}/u/${handle}/` : null) });
|
|
33997
34002
|
const face = site.actor.slice(0, -"ap/actor".length);
|
|
33998
34003
|
site.front = origin;
|
|
33999
34004
|
site.categories = face + "ap/categories";
|
|
@@ -34012,7 +34017,7 @@ function forumUrls(remotePod, root = ROOT, { publicBase = null, front = null, ha
|
|
|
34012
34017
|
return site;
|
|
34013
34018
|
}
|
|
34014
34019
|
function categoryUrls(remotePod, root, { publicBase = null, forumInbox = null } = {}) {
|
|
34015
|
-
const urls =
|
|
34020
|
+
const urls = apUrls2(remotePod, root, { publicBase });
|
|
34016
34021
|
const face = urls.actor.slice(0, -"ap/actor".length);
|
|
34017
34022
|
urls.forumInbox = forumInbox || urls.inbox;
|
|
34018
34023
|
urls.topics = face + "ap/topics";
|
|
@@ -34031,7 +34036,7 @@ function categoryUrls(remotePod, root, { publicBase = null, forumInbox = null }
|
|
|
34031
34036
|
return urls;
|
|
34032
34037
|
}
|
|
34033
34038
|
|
|
34034
|
-
//
|
|
34039
|
+
// node_modules/fedipod-bb/src/provision.mjs
|
|
34035
34040
|
async function provisionForum(remote, site, { moderatorWebIds = [] } = {}) {
|
|
34036
34041
|
await provisionPrivate(remote, site);
|
|
34037
34042
|
await provisionPublic(remote, site.notes);
|
|
@@ -34051,7 +34056,7 @@ async function provisionCategory(remote, cat, { memberWebIds = null } = {}) {
|
|
|
34051
34056
|
}
|
|
34052
34057
|
}
|
|
34053
34058
|
|
|
34054
|
-
//
|
|
34059
|
+
// node_modules/fedipod-bb/src/setup.mjs
|
|
34055
34060
|
function forumConfig(existing = {}, {
|
|
34056
34061
|
remotePod,
|
|
34057
34062
|
root = ROOT,
|
|
@@ -34144,12 +34149,13 @@ async function attachRows({ fetch: f, front, config, plain }, { log: log2 = () =
|
|
|
34144
34149
|
}
|
|
34145
34150
|
return { gateway: { front: origin, mode: "trust", secrets }, handles: rows.map((r) => r.handle) };
|
|
34146
34151
|
}
|
|
34147
|
-
async function keepRows({ fetch: f, front, config, on = true }) {
|
|
34152
|
+
async function keepRows({ fetch: f, front, config, on = true, between = null }) {
|
|
34148
34153
|
const origin = String(front).replace(/\/+$/u, "");
|
|
34149
34154
|
const forumHandle = config.handle;
|
|
34150
34155
|
const handles = [...(config.categories || []).map((c) => c.slug), forumHandle];
|
|
34151
34156
|
let keeper = null;
|
|
34152
34157
|
for (const handle of handles) {
|
|
34158
|
+
if (handle === forumHandle && between) await between(keeper);
|
|
34153
34159
|
const res = await f(`${origin}/api/keeper`, {
|
|
34154
34160
|
method: "POST",
|
|
34155
34161
|
headers: { "content-type": "application/json" },
|
|
@@ -34184,8 +34190,13 @@ async function setUpForumAtGateway({ remote, storageFor, fetch: f, front, ownerW
|
|
|
34184
34190
|
await writeConfig2(storage, { ...config, gateway, republish: true });
|
|
34185
34191
|
onStep("attach", "ok");
|
|
34186
34192
|
onStep("keep", "running");
|
|
34187
|
-
const { keeper } = await keepRows({
|
|
34188
|
-
|
|
34193
|
+
const { keeper } = await keepRows({
|
|
34194
|
+
fetch: f,
|
|
34195
|
+
front,
|
|
34196
|
+
config,
|
|
34197
|
+
on: true,
|
|
34198
|
+
between: (k) => nameKeeperInRules(remote, { config, plain, keeper: k, ownerWebId })
|
|
34199
|
+
});
|
|
34189
34200
|
onStep("keep", "ok");
|
|
34190
34201
|
return { handle: config.handle, front: gateway.front, handles, keeper };
|
|
34191
34202
|
}
|
|
@@ -34381,7 +34392,7 @@ async function readIssuer(actorUrl, fetchImpl = fetch) {
|
|
|
34381
34392
|
}
|
|
34382
34393
|
}
|
|
34383
34394
|
|
|
34384
|
-
//
|
|
34395
|
+
// node_modules/fediverse-session/oidc-session.mjs
|
|
34385
34396
|
var STORE = "session";
|
|
34386
34397
|
var b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
34387
34398
|
var enc = (o) => b64u(new TextEncoder().encode(JSON.stringify(o)));
|