fedipod 1.36.6 → 1.40.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 +29 -8
- package/architecture.md +8 -0
- package/bin/fedipod.mjs +6 -0
- package/cli.md +7 -1
- package/device-agent.md +3 -3
- package/gateway.md +69 -8
- package/groups.md +2 -1
- package/gui.md +3 -1
- package/lib/client/c2s.mjs +97 -43
- package/lib/client/masto/accounts.mjs +1 -0
- package/lib/client/masto/bridge.mjs +61 -0
- package/lib/client/masto/index.mjs +4 -1
- package/lib/client/masto/oauth.mjs +1 -1
- package/lib/client/masto/statuses.mjs +3 -0
- package/lib/connections/acctfeed.mjs +13 -9
- package/lib/connections/bskyfeed.mjs +13 -9
- package/lib/connections/tagfeed.mjs +14 -9
- package/lib/core/deliver.mjs +17 -1
- package/lib/core/intake/activities.mjs +18 -2
- package/lib/core/intake/group.mjs +3 -1
- package/lib/core/intake/index.mjs +226 -48
- package/lib/core/intake/notes.mjs +12 -5
- package/lib/core/intake/verify.mjs +11 -0
- package/lib/core/lease.mjs +15 -1
- package/lib/core/place.mjs +82 -0
- package/lib/core/pod-only.mjs +4 -0
- package/lib/core/publisher/collections.mjs +37 -2
- package/lib/core/publisher/index.mjs +25 -1
- package/lib/core/publisher/notes.mjs +73 -7
- package/lib/core/publisher/own.mjs +143 -0
- package/lib/core/publisher/questions.mjs +5 -3
- package/lib/core/scheduled.mjs +39 -0
- package/lib/core/social.mjs +62 -32
- package/lib/core/storage.mjs +67 -0
- package/lib/core/store.mjs +77 -5
- package/lib/core/wire.mjs +51 -15
- package/lib/device/admin/routes/lifecycle.mjs +1 -1
- package/lib/device/admin/routes/setup.mjs +17 -1
- package/lib/device/cli/commands/setup.mjs +50 -8
- package/lib/device/cli/context.mjs +1 -1
- package/lib/device/migrate.mjs +1 -1
- package/lib/device/setup.mjs +45 -8
- package/lib/gateway/account-agent.mjs +111 -0
- package/lib/gateway/copy.mjs +381 -0
- package/lib/gateway/front-core.mjs +74 -63
- package/lib/gateway/gateway-core.mjs +107 -9
- package/lib/gateway/held-mail.mjs +198 -0
- package/lib/gateway/keeper-due.mjs +39 -0
- package/lib/gateway/keeper-session.mjs +10 -0
- package/lib/gateway/keeper.mjs +72 -0
- package/lib/gateway/masto-gateway.mjs +510 -0
- package/lib/gateway/quiet.mjs +7 -2
- package/lib/gateway/relay-extras.mjs +89 -0
- package/lib/gateway/state-api.mjs +207 -0
- package/lib/gateway/token-claims.mjs +16 -0
- package/lib/pod/containers.mjs +17 -0
- package/lib/pod/location.mjs +52 -0
- package/lib/pod/notes.mjs +2 -4
- package/lib/pod/transport.mjs +214 -24
- package/lib/pod/type-index.mjs +101 -0
- package/lib/pod/urls.mjs +6 -0
- package/lib/server/embed.mjs +7 -6
- package/lib/session/README.md +5 -5
- package/lib/session/demo.html +1 -1
- package/lib/session/fedi-account.mjs +19 -10
- package/lib/session/package.json +2 -2
- package/package.json +2 -2
- package/run-agent.mjs +6 -16
- package/scripts/stage-site.mjs +17 -4
- package/web/admin/actors.js +2 -0
- package/web/admin/gateway.js +14 -1
- package/web/admin/index.html +13 -0
- package/web/admin/oauth-signin.mjs +1 -1
- package/web/admin/record.js +4 -1
- package/web/admin/setup/index.html +17 -1
- package/web/admin/setup/setup.js +18 -5
- package/web/app/README.md +2 -2
- package/web/app/admin-facade.mjs +12 -2
- package/web/app/agent.mjs +228 -51
- package/web/app/boot.mjs +75 -32
- package/web/app/copy-mode.mjs +223 -0
- package/web/app/dist/boot.js +498 -88
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +4016 -2539
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/index.html +16 -0
- package/web/app/signup.mjs +65 -26
- package/web/app/sw-src.mjs +15 -53
- package/web/app/update.js +3 -2
- package/web/app/warm-start.mjs +115 -0
- package/web/app-signin/app-signin.mjs +78 -0
- package/web/app-signin/index.html +41 -0
- package/web/front/run.html +7 -1
- package/web/front/run.js +30 -4
package/web/app/boot.mjs
CHANGED
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
// fedipodMoveIn(answers) brings the address here.
|
|
15
15
|
// Returning: fedipodSignin({ address }) — redirect to the pod's login.
|
|
16
16
|
// On every load: fedipodOnLoad() — finish a redirect, or restore, then boot.
|
|
17
|
-
import { signUp, moveIn, readAccount, handleProblem
|
|
17
|
+
import { signUp, moveIn, readAccount, handleProblem } from './signup.mjs';
|
|
18
|
+
import { podRootPath, chosenRoot } from '../../lib/core/place.mjs';
|
|
18
19
|
import * as podActor from '../../lib/pod/actor.mjs';
|
|
19
20
|
import * as podState from '../../lib/pod/state.mjs';
|
|
20
21
|
import { podBaseOfWebId } from '../../lib/pod/urls.mjs';
|
|
21
22
|
import { resourceExists } from '../../lib/pod/root.mjs';
|
|
22
23
|
import { BrowserRemotePod } from './pod-remote.mjs';
|
|
23
24
|
import { beginLogin, completeLogin, getSession, signOut } from './oidc-session.mjs';
|
|
25
|
+
import { deleteWarm } from './warm-start.mjs';
|
|
24
26
|
import { generateKeys, unwrapKeys, isKeyEnvelope } from './keystore.mjs';
|
|
25
27
|
import { cacheOpenedKeys } from './keys-browser.mjs';
|
|
26
28
|
|
|
@@ -76,16 +78,15 @@ async function bootWorker({ reset = false } = {}) {
|
|
|
76
78
|
async function readAccountState() {
|
|
77
79
|
const session = await getSession();
|
|
78
80
|
if (!session) throw new Error('Sign in first.');
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
// Wherever its owner put it: the type index says, or it is an older account
|
|
82
|
+
// at `fedipod/` (signup.mjs readAccount).
|
|
83
|
+
const here = await readAccount(session);
|
|
84
|
+
if (!here) throw new Error(`could not find a FediPod account on ${podBaseOfWebId(session.webId)}`);
|
|
81
85
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: 'signup', log: () => {} });
|
|
82
|
-
const urls = { state };
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (!cfg) throw new Error(`could not read this account's config under ${state}`);
|
|
87
|
-
const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
|
|
88
|
-
return { remote, urls, cfg, doc, actorUrl };
|
|
86
|
+
const urls = { state: `${here.pod}${here.root}ap-state/` };
|
|
87
|
+
const doc = await podState.readKeys(remote, urls);
|
|
88
|
+
const actorUrl = `${here.config.remotePod || here.pod}${here.config.root || here.root}ap/actor`;
|
|
89
|
+
return { remote, urls, cfg: here.config, doc, actorUrl };
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
window.fedipodUnlock = async (password) => {
|
|
@@ -161,22 +162,33 @@ export function parseAddress(input) {
|
|
|
161
162
|
if (!handle || !host || !host.includes('.')) return null;
|
|
162
163
|
return { handle, host };
|
|
163
164
|
}
|
|
164
|
-
async function
|
|
165
|
+
async function issuerForActor(actorUrl) {
|
|
165
166
|
// The pod's actor says where a client signs in (oauthAuthorizationEndpoint's
|
|
166
|
-
// origin)
|
|
167
|
-
//
|
|
167
|
+
// origin). Failing that: a host that answers OpenID discovery itself is the
|
|
168
|
+
// provider (a suffixed pod), and otherwise the provider is
|
|
169
|
+
// the pod host's parent domain (a subdomain pod).
|
|
168
170
|
try {
|
|
169
|
-
const authz = await podActor.readIssuer(
|
|
171
|
+
const authz = await podActor.readIssuer(actorUrl);
|
|
170
172
|
if (authz) return new URL(authz).origin;
|
|
171
173
|
} catch { /* fall through */ }
|
|
172
|
-
const u = new URL(
|
|
173
|
-
|
|
174
|
+
const u = new URL(actorUrl);
|
|
175
|
+
const own = await fetch(`${u.origin}/.well-known/openid-configuration`, { headers: { accept: 'application/json' } }).catch(() => null);
|
|
176
|
+
if (own?.ok) return u.origin;
|
|
174
177
|
const parent = u.host.split('.').slice(1).join('.');
|
|
175
178
|
return `https://${parent || u.host}`;
|
|
176
179
|
}
|
|
180
|
+
// An address on a pod names its actor through the pod host's own WebFinger;
|
|
181
|
+
// an older pod that does not answer is taken to keep it at `fedipod/`.
|
|
182
|
+
async function actorForPodAddress(handle, host) {
|
|
183
|
+
const res = await fetch(`https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${host}`)}`,
|
|
184
|
+
{ headers: { accept: 'application/jrd+json, application/json' } }).catch(() => null);
|
|
185
|
+
const doc = res?.ok ? await res.json().catch(() => ({})) : {};
|
|
186
|
+
const self = (doc.links || []).find((l) => l.rel === 'self' && /activity\+json|ld\+json/u.test(l.type || ''));
|
|
187
|
+
return self?.href || `https://${host}/fedipod/ap/actor`;
|
|
188
|
+
}
|
|
177
189
|
// An address at this site names a fronted identity. Its documents live on a
|
|
178
190
|
// pod this site's WebFinger names as an alias — the pod's own actor id.
|
|
179
|
-
async function
|
|
191
|
+
async function actorForFrontedAddress(handle) {
|
|
180
192
|
const res = await fetch(`/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${location.host}`)}`,
|
|
181
193
|
{ headers: { accept: 'application/jrd+json, application/json' } }).catch(() => null);
|
|
182
194
|
if (res?.status === 410) throw new Error(`@${handle}@${location.host} is closed: nothing on the pod behind it was touched, but the address is gone for good.`);
|
|
@@ -184,19 +196,17 @@ async function podForFrontedAddress(handle) {
|
|
|
184
196
|
const doc = await res.json().catch(() => ({}));
|
|
185
197
|
const podActorId = (doc.aliases || []).find((a) => /\/ap\/actor$/u.test(String(a)));
|
|
186
198
|
if (!podActorId) throw new Error(`@${handle}@${location.host} lives here but names no pod to sign in to`);
|
|
187
|
-
|
|
188
|
-
if (!podActorId.endsWith(tail)) throw new Error(`the pod actor ${podActorId} is not where a FediPod pod keeps one`);
|
|
189
|
-
return podActorId.slice(0, -tail.length);
|
|
199
|
+
return podActorId;
|
|
190
200
|
}
|
|
191
201
|
window.fedipodSignin = async ({ address }) => {
|
|
192
202
|
const parsed = parseAddress(address);
|
|
193
203
|
if (!parsed) throw new Error('Enter your address as @you@yourpod (for example @alice@alice.solidcommunity.net).');
|
|
194
204
|
const bad = handleProblem(parsed.handle);
|
|
195
205
|
if (bad) throw new Error(bad);
|
|
196
|
-
const
|
|
197
|
-
? await
|
|
198
|
-
:
|
|
199
|
-
const issuer = await
|
|
206
|
+
const actor = parsed.host === location.host.toLowerCase()
|
|
207
|
+
? await actorForFrontedAddress(parsed.handle)
|
|
208
|
+
: await actorForPodAddress(parsed.handle, parsed.host);
|
|
209
|
+
const issuer = await issuerForActor(actor);
|
|
200
210
|
const { authorizationUrl } = await beginLogin({ issuer, redirectUri: REDIRECT });
|
|
201
211
|
location.href = authorizationUrl;
|
|
202
212
|
};
|
|
@@ -238,6 +248,8 @@ window.fedipodSignOut = async () => {
|
|
|
238
248
|
let req; try { req = indexedDB.deleteDatabase('fedipod-accounts'); } catch { res(); return; }
|
|
239
249
|
req.onsuccess = req.onerror = req.onblocked = () => res();
|
|
240
250
|
});
|
|
251
|
+
// The copy of the account's state kept for worker restarts goes with it.
|
|
252
|
+
await deleteWarm();
|
|
241
253
|
};
|
|
242
254
|
window.fedipodHandleProblem = handleProblem;
|
|
243
255
|
|
|
@@ -445,6 +457,10 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
445
457
|
$('loading').hidden = true; $('hero').hidden = true; $('landing').hidden = true; $('running').hidden = true;
|
|
446
458
|
$('brand').hidden = false; $('pane-form').hidden = false;
|
|
447
459
|
$('signed-pod').textContent = signedPod;
|
|
460
|
+
// The container that holds `fedipod/`: the pod's own root unless they say
|
|
461
|
+
// otherwise. A move keeps the place the account already has.
|
|
462
|
+
$('container').placeholder = podRootPath(signedPod);
|
|
463
|
+
$('container-field').hidden = !!moveFrom;
|
|
448
464
|
$('movein-note').hidden = !moveFrom;
|
|
449
465
|
if (moveFrom) {
|
|
450
466
|
$('movein-from').textContent = moveFrom.address;
|
|
@@ -484,7 +500,13 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
484
500
|
const pathPod = () => { try { return new URL(signedPod).pathname !== '/'; } catch { return false; } };
|
|
485
501
|
// A move between gateways is an address at a gateway: no choice either.
|
|
486
502
|
const shape = () => ((pathPod() || moveFrom) ? 'front' : f().shape.value);
|
|
487
|
-
const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape() });
|
|
503
|
+
const answers = () => ({ handle: f().handle.value.trim().toLowerCase(), shape: shape(), container: f().container.value.trim() });
|
|
504
|
+
// Where the data will be, in full, as the person types.
|
|
505
|
+
const previewPlace = () => {
|
|
506
|
+
if (!signedPod) return;
|
|
507
|
+
const r = chosenRoot(signedPod, f().container.value);
|
|
508
|
+
$('place').textContent = r.problem ? '…' : signedPod + r.root;
|
|
509
|
+
};
|
|
488
510
|
const previewAddr = () => {
|
|
489
511
|
const handle = f().handle.value.trim().toLowerCase();
|
|
490
512
|
let host = '';
|
|
@@ -507,7 +529,7 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
507
529
|
$('register-link').href = url || '#';
|
|
508
530
|
$('register-link').textContent = providerHost() ? `Create your pod at ${providerHost()}` : 'Create your pod at your provider';
|
|
509
531
|
};
|
|
510
|
-
for (const el of $('form').elements) for (const evt of ['input', 'change']) el.addEventListener(evt, () => { applyMode(); applyShape(); previewAddr(); });
|
|
532
|
+
for (const el of $('form').elements) for (const evt of ['input', 'change']) el.addEventListener(evt, () => { applyMode(); applyShape(); previewAddr(); previewPlace(); });
|
|
511
533
|
applyMode();
|
|
512
534
|
|
|
513
535
|
// Step machine: one screen at a time, each gated by its own validation.
|
|
@@ -516,7 +538,7 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
516
538
|
const goStep = (n) => {
|
|
517
539
|
STEP_IDS.forEach((id, i) => { $(id).hidden = i !== n - 1; });
|
|
518
540
|
$('err-1').textContent = ''; $('form-error').textContent = '';
|
|
519
|
-
if (n === 2) { applyShape(); previewAddr(); }
|
|
541
|
+
if (n === 2) { applyShape(); previewAddr(); previewPlace(); $('index-ask').hidden = true; }
|
|
520
542
|
if (FOCUS[n]) $(FOCUS[n]).focus();
|
|
521
543
|
};
|
|
522
544
|
const validateStep1 = () => {
|
|
@@ -526,6 +548,10 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
526
548
|
const validateStep2 = () => {
|
|
527
549
|
const hp = window.fedipodHandleProblem(f().handle.value.trim().toLowerCase());
|
|
528
550
|
if (hp) return `Fediverse handle: ${hp}`;
|
|
551
|
+
if (!moveFrom) {
|
|
552
|
+
const where = chosenRoot(signedPod, f().container.value).problem;
|
|
553
|
+
if (where) return `Where to store it: ${where}`;
|
|
554
|
+
}
|
|
529
555
|
return null;
|
|
530
556
|
};
|
|
531
557
|
$('create').addEventListener('click', showForm);
|
|
@@ -549,12 +575,12 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
549
575
|
$('run-retry').addEventListener('click', () => backToForm($('run-error').textContent));
|
|
550
576
|
$('run-back').addEventListener('click', () => { showLanding(); goStep(1); });
|
|
551
577
|
|
|
552
|
-
const LABELS = { pod: 'Checking your pod', keys: 'Making your signing key', gateway: 'Connecting your mail door'
|
|
578
|
+
const LABELS = { pod: 'Checking your pod', keys: 'Making your signing key', gateway: 'Connecting your mail door',
|
|
579
|
+
place: 'Recording where your account lives' };
|
|
553
580
|
const MOVE_LABELS = { pod: 'Reading your account on your pod', gateway: 'Taking your address here', keys: 'Moving your key and account record' };
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
const a = answers();
|
|
581
|
+
// Run the setup. `createIndex` is the person's yes to a new public type index.
|
|
582
|
+
const run = async (createIndex = false) => {
|
|
583
|
+
const a = { ...answers(), ...(createIndex ? { createIndex: true } : {}) };
|
|
558
584
|
$('pane-form').hidden = true; $('running').hidden = false;
|
|
559
585
|
$('running-title').textContent = 'Setting up…'; $('run-error').textContent = ''; $('run-actions').hidden = true;
|
|
560
586
|
const steps = $('steps'); steps.textContent = ''; const mark = {};
|
|
@@ -566,10 +592,27 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
566
592
|
location.href = '/admin/client/';
|
|
567
593
|
}
|
|
568
594
|
catch (err) {
|
|
595
|
+
// No type index: nothing has been written. Ask, on the form.
|
|
596
|
+
if (err?.code === 'needs-index') {
|
|
597
|
+
backToForm();
|
|
598
|
+
$('index-ask').hidden = false;
|
|
599
|
+
$('index-yes').focus();
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
569
602
|
$('running-title').textContent = 'Setup did not finish';
|
|
570
603
|
$('run-error').textContent = err.message || String(err);
|
|
571
604
|
$('run-actions').hidden = false; // the way out of the failure screen
|
|
572
605
|
}
|
|
606
|
+
};
|
|
607
|
+
$('form').addEventListener('submit', async (e) => {
|
|
608
|
+
e.preventDefault(); $('form-error').textContent = '';
|
|
609
|
+
const e2 = validateStep2(); if (e2) { $('form-error').textContent = e2; goStep(2); return; }
|
|
610
|
+
await run();
|
|
611
|
+
});
|
|
612
|
+
$('index-yes').addEventListener('click', () => { $('index-ask').hidden = true; run(true); });
|
|
613
|
+
$('index-no').addEventListener('click', () => {
|
|
614
|
+
$('index-ask').hidden = true;
|
|
615
|
+
$('form-error').textContent = 'Sign-up stopped. Nothing was written to your pod.';
|
|
573
616
|
});
|
|
574
617
|
|
|
575
618
|
if (pendingIdentity) await showIdentity();
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// copy-mode.mjs — the browser agent working from its account's copy at the
|
|
2
|
+
// gateway (lib/gateway/copy.mjs, lib/gateway/state-api.mjs), for an account the
|
|
3
|
+
// gateway keeps running.
|
|
4
|
+
//
|
|
5
|
+
// The state documents are read and written at the gateway instead of on the
|
|
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. The
|
|
8
|
+
// gateway writes the copy to the pod every fifteen minutes.
|
|
9
|
+
import { HttpStorage, StateApiStorage, podOnly } from '../../lib/core/storage.mjs';
|
|
10
|
+
import { Lease } from '../../lib/core/lease.mjs';
|
|
11
|
+
|
|
12
|
+
// A token this much short of its end is renewed before it is used again.
|
|
13
|
+
const RENEW_BEFORE_MS = 2 * 3600_000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ask the gateway for this account's copy, making it if there is none yet.
|
|
17
|
+
* `handle` names the account when the agent knows it. Returns the copy
|
|
18
|
+
* ({ handle, base, token, expiresAt }) or null, in which case the account
|
|
19
|
+
* works from its pod.
|
|
20
|
+
*/
|
|
21
|
+
export async function openCopy(agent, frontOrigin, { handle = null, kept = null } = {}) {
|
|
22
|
+
if (kept && kept.expiresAt - Date.now() > RENEW_BEFORE_MS) return kept;
|
|
23
|
+
if (!agent.sessionFetch || !frontOrigin) return null;
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await agent.sessionFetch(`${frontOrigin.replace(/\/$/u, '')}/api/state/open`, {
|
|
27
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(handle ? { handle } : {}),
|
|
28
|
+
});
|
|
29
|
+
} catch (e) { agent.log(`the account's copy: ${e.message}`); return null; }
|
|
30
|
+
if (res.status !== 200) {
|
|
31
|
+
// 404, 409 and 501 are answers (no copy for this account here); anything
|
|
32
|
+
// else is worth saying.
|
|
33
|
+
if (![404, 409, 501].includes(res.status)) agent.log(`the account's copy: the gateway answered ${res.status}`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const copy = await res.json().catch(() => null);
|
|
37
|
+
return copy?.base && copy?.token
|
|
38
|
+
? { handle: copy.handle, base: copy.base, token: copy.token, expiresAt: copy.expiresAt, podHome: copy.podHome || null }
|
|
39
|
+
: null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Every request to the copy carries its token, read at the time of asking so a
|
|
43
|
+
// renewed one is used at once.
|
|
44
|
+
const tokenFetch = (agent) => (u, i = {}) => fetch(u, {
|
|
45
|
+
...i, headers: { ...(i.headers || {}), authorization: `Bearer ${agent.copy.token}` },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** The store's storage, and the lease, for working from the copy. */
|
|
49
|
+
export function copyStorage(agent, podState) {
|
|
50
|
+
const s = new StateApiStorage(agent.copy.base, {
|
|
51
|
+
fetchImpl: (u, i) => fetch(u, i), token: agent.copy.token, holder: agent.holderId, pod: podState,
|
|
52
|
+
// Refused: another agent (an app at the gateway, another browser) took
|
|
53
|
+
// the lease. This one stops acting at once rather than at its next renewal.
|
|
54
|
+
onRefused: () => standDown(agent),
|
|
55
|
+
});
|
|
56
|
+
Object.defineProperty(s, 'token', { get: () => agent.copy.token, set() {} });
|
|
57
|
+
return s;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function copyLeaseOf(agent) {
|
|
61
|
+
return new Lease({ url: `${agent.copy.base}lease.json`, fetchImpl: tokenFetch(agent), log: agent.log, id: agent.holderId });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Swap the lease the agent and its drain go by.
|
|
65
|
+
function useLease(agent, lease) {
|
|
66
|
+
agent.lease.stopRenewal();
|
|
67
|
+
agent.lease = lease;
|
|
68
|
+
if (agent.intake) agent.intake.lease = lease;
|
|
69
|
+
lease.onLost = () => agent.demote();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Before this browser acts: is the copy's lease still its own? An app at the
|
|
74
|
+
* gateway may have taken it and let it go since. Free again, it is taken back
|
|
75
|
+
* and the copy read afresh, so the action starts from what the app wrote.
|
|
76
|
+
* Held by somebody else, the browser stands down and the caller takes over as
|
|
77
|
+
* from any other device. True when this browser holds it now.
|
|
78
|
+
*/
|
|
79
|
+
export async function ensureCopyLease(agent) {
|
|
80
|
+
const cur = await agent.lease.readFresh().catch(() => null);
|
|
81
|
+
if (!cur || typeof cur !== 'object') return true; // unreadable: the copy refuses if it is wrong
|
|
82
|
+
if (cur.holder === agent.lease.id && Date.now() < cur.expiresAt) return true;
|
|
83
|
+
// Held by another agent: this browser stands down, dropping what it had
|
|
84
|
+
// queued, and the caller takes over, reading the copy afresh.
|
|
85
|
+
if (Date.now() < cur.expiresAt) { agent.demote(); await agent.store.discardPending(); return false; }
|
|
86
|
+
// Free: an app acted and let it go. Writes queued here since are older than
|
|
87
|
+
// what the app wrote, so they are dropped, not sent.
|
|
88
|
+
agent._ensuring = true;
|
|
89
|
+
try {
|
|
90
|
+
await agent.store.discardPending();
|
|
91
|
+
if (!await agent.lease.acquire()) { agent.demote(); return false; }
|
|
92
|
+
await agent.store.load({ force: true });
|
|
93
|
+
agent.lease.startRenewal();
|
|
94
|
+
return true;
|
|
95
|
+
} finally { agent._ensuring = false; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Another agent took the lease: an app acting at the gateway, which gives it
|
|
100
|
+
* back as soon as it is done. This browser stops acting at once, then takes
|
|
101
|
+
* the lease back when it is free, reading the copy afresh first (goActive), so
|
|
102
|
+
* it is not left watching for the viewer poll's five minutes. Another browser
|
|
103
|
+
* that took over keeps it, and the viewer poll carries on as before.
|
|
104
|
+
*/
|
|
105
|
+
export function standDown(agent) {
|
|
106
|
+
if (agent._retaking || agent._ensuring) return;
|
|
107
|
+
if (!agent.viewer) agent.demote();
|
|
108
|
+
agent._retaking = (async () => {
|
|
109
|
+
try {
|
|
110
|
+
// What was queued before the other agent acted is older than what it wrote.
|
|
111
|
+
await agent.store.discardPending();
|
|
112
|
+
for (const wait of [3000, 20_000]) {
|
|
113
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
114
|
+
if (!agent.viewer) return;
|
|
115
|
+
const cur = await agent.lease.readFresh().catch(() => null);
|
|
116
|
+
// Held by anyone but the gateway (copy.mjs: GATEWAY_HOLDER) is another browser's.
|
|
117
|
+
if (cur && typeof cur === 'object' && cur.holder !== 'gateway' && Date.now() < cur.expiresAt) return;
|
|
118
|
+
if (await agent.lease.acquire()) {
|
|
119
|
+
clearTimeout(agent._viewerTimer); agent._viewerTimer = null;
|
|
120
|
+
// Read what the app wrote, then carry on as a restart moments later
|
|
121
|
+
// would: no republishing, one drain, the inbox watched again.
|
|
122
|
+
await agent.store.load({ force: true });
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
await agent.goActive({ warm: { hereAt: now, drainedAt: 0, wakeAt: now, resubscribe: true } });
|
|
125
|
+
agent.log('took the account back from the gateway');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} finally { agent._retaking = null; }
|
|
130
|
+
})();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Move a running agent from its pod onto the copy: everything written, the
|
|
135
|
+
* pod's lease let go, the copy made from the pod, and the agent carried over
|
|
136
|
+
* holding the copy's lease. Returns whether it moved; when it could not, the
|
|
137
|
+
* agent takes the pod's lease back and goes on as it was.
|
|
138
|
+
*/
|
|
139
|
+
export async function moveIntoCopy(agent, frontOrigin) {
|
|
140
|
+
if (agent.copy) return true;
|
|
141
|
+
await agent.store.commit();
|
|
142
|
+
await agent.lease.release();
|
|
143
|
+
const copy = await openCopy(agent, frontOrigin, { handle: agent.doorKey });
|
|
144
|
+
if (!copy) {
|
|
145
|
+
if (await agent.lease.acquire()) agent.lease.startRenewal();
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
agent.copy = copy;
|
|
149
|
+
const lease = copyLeaseOf(agent);
|
|
150
|
+
if (!await lease.acquire()) { agent.log('the account\'s copy is held by another agent; reading only'); }
|
|
151
|
+
agent.store.attach(copyStorage(agent, new HttpStorage(agent.urls.state, (u, i) => agent.remote.fetch(u, i))));
|
|
152
|
+
await agent.store.load({ force: true });
|
|
153
|
+
useLease(agent, lease);
|
|
154
|
+
if (lease.heldUntil) lease.startRenewal(); else agent.demote();
|
|
155
|
+
agent.log('working from the account\'s copy at the gateway');
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Back to the pod: everything written, the copy's lease let go, the gateway
|
|
161
|
+
* asked to write the copy to the pod and give it up, and the agent carried
|
|
162
|
+
* back onto the pod. Returns { ok } or { ok: false, why }.
|
|
163
|
+
*/
|
|
164
|
+
export async function leaveCopy(agent) {
|
|
165
|
+
if (!agent.copy) return { ok: true };
|
|
166
|
+
await agent.store.commit();
|
|
167
|
+
await agent.lease.release();
|
|
168
|
+
const res = await tokenFetch(agent)(`${agent.copy.base}leave`, { method: 'POST' }).catch((e) => ({ status: 0, e }));
|
|
169
|
+
if (res.status !== 200) {
|
|
170
|
+
if (await agent.lease.acquire()) agent.lease.startRenewal();
|
|
171
|
+
return { ok: false, why: `the gateway could not write the copy to the pod (${res.status || res.e?.message})` };
|
|
172
|
+
}
|
|
173
|
+
agent.copy = null;
|
|
174
|
+
const podFetch = (u, i) => agent.remote.fetch(u, i);
|
|
175
|
+
const lease = new Lease({ url: `${agent.urls.state}lease.json`, fetchImpl: podFetch, log: agent.log, id: agent.holderId });
|
|
176
|
+
agent.store.attach(new HttpStorage(agent.urls.state, podFetch));
|
|
177
|
+
await agent.store.load({ force: true });
|
|
178
|
+
useLease(agent, lease);
|
|
179
|
+
if (await lease.acquire()) lease.startRenewal(); else agent.demote();
|
|
180
|
+
agent.log('working from the pod again');
|
|
181
|
+
return { ok: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The gateway has a new identity, and this account's copy was kept under the
|
|
186
|
+
* old one, which the new one cannot write back for (the owner's rules name the
|
|
187
|
+
* old). The owner's own browser does it: every document in the copy written to
|
|
188
|
+
* the pod as the owner, the pod's lease taken, the gateway told to forget the
|
|
189
|
+
* copy, and the agent carried back onto the pod. Its rules then name the new
|
|
190
|
+
* identity as any first start does (setKeeper), and a new copy is made.
|
|
191
|
+
* Returns { ok } or { ok: false, why }; nothing is forgotten unless everything
|
|
192
|
+
* reached the pod.
|
|
193
|
+
*/
|
|
194
|
+
export async function handOverCopy(agent) {
|
|
195
|
+
if (!agent.copy) return { ok: true };
|
|
196
|
+
await agent.store.commit();
|
|
197
|
+
const podFetch = (u, i) => agent.remote.fetch(u, i);
|
|
198
|
+
const pod = new HttpStorage(agent.urls.state, podFetch);
|
|
199
|
+
const docs = agent.store.names().filter((n) => !podOnly(n)).map((n) => [n, agent.store.read(n, null)]).filter(([, v]) => v !== null);
|
|
200
|
+
for (const [name, value] of docs) {
|
|
201
|
+
const w = await pod.write(name, JSON.stringify(value, null, 2) + '\n', 'application/json');
|
|
202
|
+
if (!w.ok) return { ok: false, why: `${name} could not be written to the pod (${w.why})` };
|
|
203
|
+
}
|
|
204
|
+
const podLease = new Lease({ url: `${agent.urls.state}lease.json`, fetchImpl: podFetch, log: agent.log, id: agent.holderId });
|
|
205
|
+
if (!await podLease.takeover()) return { ok: false, why: 'the pod\'s lease could not be taken' };
|
|
206
|
+
const res = await tokenFetch(agent)(`${agent.copy.base}forget`, { method: 'POST', headers: { 'x-fedipod-holder': agent.holderId } })
|
|
207
|
+
.catch((e) => ({ status: 0, e }));
|
|
208
|
+
if (res.status !== 200) return { ok: false, why: `the gateway would not let the copy go (${res.status || res.e?.message})` };
|
|
209
|
+
agent.copy = null;
|
|
210
|
+
agent.store.attach(pod);
|
|
211
|
+
await agent.store.load({ force: true });
|
|
212
|
+
useLease(agent, podLease);
|
|
213
|
+
podLease.startRenewal();
|
|
214
|
+
agent.log(`handed the account's copy over: ${docs.length} documents written to the pod`);
|
|
215
|
+
return { ok: true };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** A token near its end, renewed; the hourly check-in asks. */
|
|
219
|
+
export async function renewCopyToken(agent, frontOrigin) {
|
|
220
|
+
if (!agent.copy || agent.copy.expiresAt - Date.now() > RENEW_BEFORE_MS) return;
|
|
221
|
+
const fresh = await openCopy(agent, frontOrigin, { handle: agent.copy.handle });
|
|
222
|
+
if (fresh) agent.copy = fresh;
|
|
223
|
+
}
|