fedipod 1.29.2 → 1.32.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 +1 -1
- package/device-agent.md +9 -2
- package/gateway.md +43 -4
- package/gui.md +2 -2
- package/lib/core/publisher/index.mjs +3 -0
- package/lib/core/wire.mjs +10 -1
- package/lib/device/cli/commands/setup.mjs +6 -2
- package/lib/device/gateway-move.mjs +58 -0
- package/lib/device/migrate.mjs +3 -1
- package/lib/device/setup.mjs +67 -7
- package/lib/gateway/front-core.mjs +34 -14
- package/lib/gateway/gateway-core.mjs +15 -6
- package/lib/gateway/notices.mjs +84 -0
- package/lib/gateway/quiet.mjs +184 -0
- package/package.json +1 -1
- package/run-agent.mjs +5 -0
- package/scripts/stage-site.mjs +6 -3
- package/web/admin/bar.css +44 -10
- package/web/admin/client/index.html +29 -2
- package/web/admin/gateway.js +19 -0
- package/web/admin/index.html +46 -3
- package/web/admin/notices-bar.js +90 -0
- package/web/admin/record.js +1 -0
- package/web/admin/setup/index.html +29 -2
- package/web/admin/upkeep.js +5 -1
- package/web/app/admin-facade.mjs +24 -0
- package/web/app/agent.mjs +48 -0
- package/web/app/boot.mjs +2 -0
- package/web/app/dist/boot.js +3 -1
- package/web/app/dist/boot.js.map +2 -2
- package/web/app/dist/sw.js +96 -3
- package/web/app/dist/sw.js.map +3 -3
- package/web/front/admin.html +3 -1
- package/web/front/notices.html +69 -0
- package/web/front/notices.js +107 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// notices-bar.js — the notices bell in the bar, on every page of ours.
|
|
2
|
+
//
|
|
3
|
+
// Whoever runs the site writes notices (the /notices page, admin only); every
|
|
4
|
+
// signed-in account sees the bell. This asks /api/notices once per page load,
|
|
5
|
+
// hides the bell where the site has none to offer (a DeviceAgent's own pages
|
|
6
|
+
// answer no such thing), lists the titles, and opens one notice at a time.
|
|
7
|
+
// Which notices this browser has already opened is this browser's business
|
|
8
|
+
// alone, so it lives in localStorage and nowhere else.
|
|
9
|
+
//
|
|
10
|
+
// Every element here is declared in the page's markup; this fills them.
|
|
11
|
+
(() => {
|
|
12
|
+
const bell = document.getElementById('bar-notices');
|
|
13
|
+
const list = document.getElementById('notices-list');
|
|
14
|
+
const view = document.getElementById('notice-view');
|
|
15
|
+
if (!bell || !list || !view) return;
|
|
16
|
+
const $ = (id) => document.getElementById(id);
|
|
17
|
+
const SEEN = 'fedipod-notices-seen';
|
|
18
|
+
const readSeen = () => { try { return JSON.parse(localStorage.getItem(SEEN) || '[]'); } catch { return []; } };
|
|
19
|
+
const writeSeen = (ids) => { try { localStorage.setItem(SEEN, JSON.stringify(ids.slice(-200))); } catch { /* keeps no site data */ } };
|
|
20
|
+
|
|
21
|
+
const when = (iso) => { try { return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); } catch { return ''; } };
|
|
22
|
+
|
|
23
|
+
// A notice's body is plain text: paragraphs at blank lines, and a bare
|
|
24
|
+
// https link becomes a link. Nothing in it is ever read as markup.
|
|
25
|
+
const renderBody = (el, text) => {
|
|
26
|
+
el.textContent = '';
|
|
27
|
+
for (const para of String(text || '').split(/\n\s*\n/u)) {
|
|
28
|
+
const p = document.createElement('p');
|
|
29
|
+
for (const part of para.split(/(https?:\/\/[^\s<>"']+)/u)) {
|
|
30
|
+
if (/^https?:\/\//u.test(part)) {
|
|
31
|
+
const a = document.createElement('a');
|
|
32
|
+
a.href = part; a.textContent = part; a.target = '_blank'; a.rel = 'noopener';
|
|
33
|
+
p.append(a);
|
|
34
|
+
} else p.append(part);
|
|
35
|
+
}
|
|
36
|
+
el.append(p);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let notices = [];
|
|
41
|
+
const render = () => {
|
|
42
|
+
const seen = new Set(readSeen());
|
|
43
|
+
const fresh = notices.filter((n) => !seen.has(n.id)).length;
|
|
44
|
+
$('bar-notices-count').textContent = fresh ? String(fresh) : '';
|
|
45
|
+
bell.dataset.new = String(fresh);
|
|
46
|
+
bell.setAttribute('aria-label', fresh ? `Notices, ${fresh} new` : 'Notices');
|
|
47
|
+
const ul = $('notices-items');
|
|
48
|
+
ul.textContent = '';
|
|
49
|
+
if (!notices.length) {
|
|
50
|
+
const p = document.createElement('p'); p.className = 'none'; p.textContent = 'No notices.';
|
|
51
|
+
ul.append(p);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const n of notices) {
|
|
55
|
+
const li = document.createElement('li');
|
|
56
|
+
if (!seen.has(n.id)) li.classList.add('new');
|
|
57
|
+
const b = document.createElement('button');
|
|
58
|
+
b.type = 'button'; b.className = 'notice-open'; b.textContent = n.title;
|
|
59
|
+
b.addEventListener('click', () => open(n));
|
|
60
|
+
const w = document.createElement('span'); w.className = 'when'; w.textContent = when(n.at);
|
|
61
|
+
li.append(b, w);
|
|
62
|
+
ul.append(li);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const open = (n) => {
|
|
67
|
+
$('notice-view-title').textContent = n.title;
|
|
68
|
+
$('notice-view-when').textContent = when(n.at);
|
|
69
|
+
renderBody($('notice-view-body'), n.body);
|
|
70
|
+
const seen = readSeen();
|
|
71
|
+
if (!seen.includes(n.id)) { seen.push(n.id); writeSeen(seen); }
|
|
72
|
+
render();
|
|
73
|
+
list.close();
|
|
74
|
+
view.showModal();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
bell.addEventListener('click', () => { render(); list.showModal(); });
|
|
78
|
+
$('notices-close').addEventListener('click', () => list.close());
|
|
79
|
+
$('notice-view-close').addEventListener('click', () => { view.close(); list.showModal(); });
|
|
80
|
+
|
|
81
|
+
fetch(location.origin + '/api/notices', { headers: { accept: 'application/json' } })
|
|
82
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
83
|
+
.then((j) => {
|
|
84
|
+
if (!j || !Array.isArray(j.notices)) return;
|
|
85
|
+
notices = j.notices;
|
|
86
|
+
render();
|
|
87
|
+
bell.hidden = false;
|
|
88
|
+
})
|
|
89
|
+
.catch(() => { /* no site notices here; the bell stays hidden */ });
|
|
90
|
+
})();
|
package/web/admin/record.js
CHANGED
|
@@ -94,6 +94,7 @@ 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 = '';
|
|
97
98
|
$('move-target').value = '';
|
|
98
99
|
}
|
|
99
100
|
function closePanels(keep = null) {
|
|
@@ -59,8 +59,16 @@ button:disabled { opacity: .5; cursor: default; }
|
|
|
59
59
|
<nav id="bar" aria-label="Site">
|
|
60
60
|
<h1 class="name">FediPod</h1>
|
|
61
61
|
<span id="bar-handle"></span>
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
<!-- The account group, styled like the client group beside it: one label,
|
|
63
|
+
then the places to go. The browser build adds "sign out" to it. -->
|
|
64
|
+
<span id="account-pick"><span id="account-now">account:</span>
|
|
65
|
+
<a id="bar-fediverse" href="../client/" title=" Open the client and read your timeline">visit</a>
|
|
66
|
+
<a id="bar-manage" href="../" title=" The record: what this actor is, and what you can do to it">manage</a>
|
|
67
|
+
</span>
|
|
68
|
+
<!-- Notices from whoever runs this site. Shown only when the site has any
|
|
69
|
+
(notices-bar.js asks /api/notices); the count is how many are new here. -->
|
|
70
|
+
<button type="button" id="bar-notices" hidden aria-haspopup="dialog" aria-controls="notices-list"
|
|
71
|
+
title=" Notices from the operator of this site">🔔<span id="bar-notices-count" aria-live="polite"></span></button>
|
|
64
72
|
<a id="bar-add" href="../?new=1" title=" Set up another actor on this machine, with its own pod" aria-current="page">add new account</a>
|
|
65
73
|
</nav>
|
|
66
74
|
<main id="page">
|
|
@@ -210,5 +218,24 @@ button:disabled { opacity: .5; cursor: default; }
|
|
|
210
218
|
<script src="../bar.js"></script>
|
|
211
219
|
<script src="../common.js"></script>
|
|
212
220
|
<script src="setup.js"></script>
|
|
221
|
+
|
|
222
|
+
<!-- The notices: the list, and one notice open. Declared here and filled by
|
|
223
|
+
notices-bar.js from /api/notices; nothing in them is markup from a notice. -->
|
|
224
|
+
<dialog id="notices-list" aria-labelledby="notices-title">
|
|
225
|
+
<section>
|
|
226
|
+
<h2 id="notices-title">Notices</h2>
|
|
227
|
+
<ul id="notices-items"></ul>
|
|
228
|
+
<p class="row"><button type="button" id="notices-close" class="primary" title=" Close the list of notices">Close</button></p>
|
|
229
|
+
</section>
|
|
230
|
+
</dialog>
|
|
231
|
+
<dialog id="notice-view" aria-labelledby="notice-view-title">
|
|
232
|
+
<section>
|
|
233
|
+
<h2 id="notice-view-title"></h2>
|
|
234
|
+
<p class="when" id="notice-view-when"></p>
|
|
235
|
+
<div id="notice-view-body"></div>
|
|
236
|
+
<p class="row"><button type="button" id="notice-view-close" class="primary" title=" Back to the list of notices">Close</button></p>
|
|
237
|
+
</section>
|
|
238
|
+
</dialog>
|
|
239
|
+
<script src="../notices-bar.js"></script>
|
|
213
240
|
</body>
|
|
214
241
|
</html>
|
package/web/admin/upkeep.js
CHANGED
|
@@ -76,6 +76,9 @@ 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') },
|
|
79
82
|
};
|
|
80
83
|
let pending = null;
|
|
81
84
|
|
|
@@ -119,7 +122,8 @@ $('confirm-form').addEventListener('submit', async (ev) => {
|
|
|
119
122
|
const what = pending;
|
|
120
123
|
const body = what === 'retire' ? { confirm: $('confirm-handle').value.trim() }
|
|
121
124
|
: what === 'move' ? { target: $('move-target').value.trim(), confirm: $('confirm-handle-move').value.trim() }
|
|
122
|
-
: {}
|
|
125
|
+
: what === 'close-address' ? { confirm: $('confirm-handle-close').value.trim() }
|
|
126
|
+
: {};
|
|
123
127
|
if (what === 'move' && !body.target) { say('name the account to transfer to', 'err'); return; }
|
|
124
128
|
$('confirm-go').disabled = true;
|
|
125
129
|
say(`${what} — this talks to the pod and to other servers, so it takes a moment`);
|
package/web/app/admin-facade.mjs
CHANGED
|
@@ -33,6 +33,7 @@ export const ADMIN_PATHS = new Set([
|
|
|
33
33
|
'/deadletter', '/blocks', '/profiles', '/modqueue', '/log', '/fediacct', '/describe', '/follow',
|
|
34
34
|
'/atproto', '/atproto/connect', '/atproto/disconnect',
|
|
35
35
|
'/rebuild', '/move', '/retire', '/inbox/prune', '/park', '/revive', '/takeover',
|
|
36
|
+
'/gateway/pause', '/gateway/close',
|
|
36
37
|
'/fediacct/connect', '/fediacct/disconnect', '/fediacct/callback',
|
|
37
38
|
]);
|
|
38
39
|
|
|
@@ -138,6 +139,10 @@ export class AdminFacade {
|
|
|
138
139
|
configured: !!(g && g.url), url: g?.url || null, webId: g?.webId || null,
|
|
139
140
|
frontActor: g?.frontActor || null, mode: g?.mode || 'off', hasSecret: !!g?.hmacSecret,
|
|
140
141
|
stats: a.store.read('gateway-stats.json', { verified: 0, unverified: 0, lastAt: null }),
|
|
142
|
+
// The account's standing at the gateway — paused, closed — as the
|
|
143
|
+
// gateway last said it (agent.mjs tellGateway). The record page
|
|
144
|
+
// shows its pause and close controls only when this is here.
|
|
145
|
+
standing: a.gatewayStanding || null,
|
|
141
146
|
});
|
|
142
147
|
}
|
|
143
148
|
case '/deadletter': return json(200, { items: a.store.getDeadLetters() });
|
|
@@ -264,6 +269,25 @@ export class AdminFacade {
|
|
|
264
269
|
return json(200, { ok: true, mode: a.status().mode });
|
|
265
270
|
}
|
|
266
271
|
|
|
272
|
+
// The account's standing at its gateway: paused by its owner, or closed
|
|
273
|
+
// for good (front-core: accounts that go quiet). Both go to the gateway
|
|
274
|
+
// with the pod session as proof, and its answer is the new standing.
|
|
275
|
+
case '/gateway/pause': {
|
|
276
|
+
if (typeof body.paused !== 'boolean') return json(400, { error: 'paused must be true or false' });
|
|
277
|
+
if (!a.pauseAtGateway || !a.gatewayApi) return json(501, { error: 'this account is not at a gateway' });
|
|
278
|
+
const r = await a.pauseAtGateway(body.paused);
|
|
279
|
+
if (!r) return json(502, { error: 'the gateway could not be reached' });
|
|
280
|
+
return json(r.status === 200 ? 200 : r.status, r);
|
|
281
|
+
}
|
|
282
|
+
case '/gateway/close': {
|
|
283
|
+
const handle = String(a.store.getConfig()?.handle || '').toLowerCase();
|
|
284
|
+
if (!handle || String(body.confirm || '').toLowerCase() !== handle) return json(400, { error: 'type the handle to confirm' });
|
|
285
|
+
if (!a.closeAtGateway || !a.gatewayApi) return json(501, { error: 'this account is not at a gateway' });
|
|
286
|
+
const r = await a.closeAtGateway();
|
|
287
|
+
if (!r) return json(502, { error: 'the gateway could not be reached' });
|
|
288
|
+
return json(r.status === 200 ? 200 : r.status, r);
|
|
289
|
+
}
|
|
290
|
+
|
|
267
291
|
// Going quiet, and coming back. The record page's active/parked select.
|
|
268
292
|
case '/park': {
|
|
269
293
|
if (!await a.requestTakeover?.()) return json(503, { error: 'another device is active for this pod — park from there' });
|
package/web/app/agent.mjs
CHANGED
|
@@ -137,10 +137,35 @@ export class BrowserAgent {
|
|
|
137
137
|
return true;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// One call to the gateway's owner API, proved with the pod session. What
|
|
141
|
+
// comes back on 200 or 410 is the account's standing there, kept for the
|
|
142
|
+
// manage page; anything else is logged and forgotten.
|
|
143
|
+
async tellGateway(what, body) {
|
|
144
|
+
if (!this.gatewayApi) return null;
|
|
145
|
+
let res;
|
|
146
|
+
try {
|
|
147
|
+
res = await this.sessionFetch(`${this.gatewayApi}/${what}`, {
|
|
148
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
|
|
149
|
+
});
|
|
150
|
+
} catch (e) { this.log(`gateway ${what}: ${e.message}`); return null; }
|
|
151
|
+
const json = await res.json().catch(() => ({}));
|
|
152
|
+
const out = { status: res.status, ...json };
|
|
153
|
+
if (res.status === 200 || res.status === 410) this.gatewayStanding = out;
|
|
154
|
+
else this.log(`gateway ${what}: ${res.status} ${json.error || ''}`);
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
openAtGateway() { return this.tellGateway('open', { handle: this.doorKey }); }
|
|
158
|
+
pauseAtGateway(paused) { return this.tellGateway('pause', { handle: this.doorKey, paused: !!paused }); }
|
|
159
|
+
closeAtGateway() { return this.tellGateway('close', { handle: this.doorKey, confirm: true }); }
|
|
160
|
+
static OPEN_EVERY_MS = 60 * 60_000;
|
|
161
|
+
|
|
140
162
|
// Become the active agent: renew the lease, then start what a viewer skips —
|
|
141
163
|
// publish the face, drain the inbox, run the mirrors.
|
|
142
164
|
async goActive() {
|
|
143
165
|
this.viewer = false;
|
|
166
|
+
// Reading here is being here: the gateway hears so once an hour.
|
|
167
|
+
clearInterval(this._openTimer);
|
|
168
|
+
this._openTimer = setInterval(() => { this.openAtGateway(); }, BrowserAgent.OPEN_EVERY_MS);
|
|
144
169
|
this.lease.onLost = () => this.demote();
|
|
145
170
|
this.lease.startRenewal();
|
|
146
171
|
try {
|
|
@@ -179,6 +204,7 @@ export class BrowserAgent {
|
|
|
179
204
|
this.viewer = true;
|
|
180
205
|
this.log('another device took over — read-only here');
|
|
181
206
|
this.lease.stopRenewal();
|
|
207
|
+
clearInterval(this._openTimer); this._openTimer = null;
|
|
182
208
|
this.intake?.stop?.();
|
|
183
209
|
// The delivery queue as well. Its timer starts in the Deliverer's
|
|
184
210
|
// constructor and nothing here ever switched it off, so a demoted device
|
|
@@ -322,6 +348,28 @@ export class BrowserAgent {
|
|
|
322
348
|
onGone: () => this.publisher.publishCollections({ followers: true }),
|
|
323
349
|
});
|
|
324
350
|
|
|
351
|
+
// The gateway this account's mail comes through, and the account's own
|
|
352
|
+
// standing there — paused, closed, how much arrived unread (front-core:
|
|
353
|
+
// accounts that go quiet). The gateway is told on every sign-in and
|
|
354
|
+
// hourly while this device is active: that is what keeps an account
|
|
355
|
+
// somebody reads from counting as a quiet one. Only a closed address
|
|
356
|
+
// stops the boot; a gateway that cannot be reached is no reason to
|
|
357
|
+
// refuse a sign-in.
|
|
358
|
+
this.gatewayApi = null;
|
|
359
|
+
try { if (config.gateway?.url) this.gatewayApi = `${new URL(config.gateway.url).origin}/api`; } catch { this.gatewayApi = null; }
|
|
360
|
+
this.doorKey = doorKeyOf(config.gateway?.url) || config.handle;
|
|
361
|
+
this.gatewayStanding = null;
|
|
362
|
+
const standing = await this.openAtGateway();
|
|
363
|
+
if (standing?.status === 410) {
|
|
364
|
+
const host = (() => { try { return new URL(this.gatewayApi).host; } catch { return 'the gateway'; } })();
|
|
365
|
+
const address = this.doorKey.includes('@') ? `@${this.doorKey}` : `@${this.doorKey}@${host}`;
|
|
366
|
+
const why = standing.closedBy === 'owner' ? 'you closed it' : 'nothing opened it for six months';
|
|
367
|
+
const e = new Error(`${address} is closed at ${host}: ${why}, and a closed address does not come back.`
|
|
368
|
+
+ ` Everything on your pod is untouched. The sign-in used here was ${webId}.`);
|
|
369
|
+
e.code = 'address-closed';
|
|
370
|
+
throw e;
|
|
371
|
+
}
|
|
372
|
+
|
|
325
373
|
this.publisher = new Publisher({
|
|
326
374
|
config: this.store.getConfig(), remote: this.remote, store: this.store,
|
|
327
375
|
deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem, assertionKey: null, log: this.log,
|
package/web/app/boot.mjs
CHANGED
|
@@ -179,6 +179,7 @@ async function issuerForPod(pod) {
|
|
|
179
179
|
async function podForFrontedAddress(handle) {
|
|
180
180
|
const res = await fetch(`/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${location.host}`)}`,
|
|
181
181
|
{ headers: { accept: 'application/jrd+json, application/json' } }).catch(() => null);
|
|
182
|
+
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.`);
|
|
182
183
|
if (!res || res.status >= 400) throw new Error(`nobody at this site is called @${handle}@${location.host}`);
|
|
183
184
|
const doc = await res.json().catch(() => ({}));
|
|
184
185
|
const podActorId = (doc.aliases || []).find((a) => /\/ap\/actor$/u.test(String(a)));
|
|
@@ -352,6 +353,7 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
352
353
|
'no-account-here': { title: 'No FediPod account in that pod', retry: 'Create one on this pod', go: () => showIdentity() },
|
|
353
354
|
'no-account': { title: 'No FediPod account in that pod', retry: 'Create one on this pod', go: () => showIdentity() },
|
|
354
355
|
'device-account': { title: 'This account is run from a device', retry: 'Use another pod', go: signIn },
|
|
356
|
+
'address-closed': { title: 'This address is closed', retry: 'Sign in with another account', go: signIn },
|
|
355
357
|
}[e.code];
|
|
356
358
|
|
|
357
359
|
// A sign-in the pod would not take: renew it here, and if the pod will
|
package/web/app/dist/boot.js
CHANGED
|
@@ -33662,6 +33662,7 @@ async function podForFrontedAddress(handle) {
|
|
|
33662
33662
|
`/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${location.host}`)}`,
|
|
33663
33663
|
{ headers: { accept: "application/jrd+json, application/json" } }
|
|
33664
33664
|
).catch(() => null);
|
|
33665
|
+
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.`);
|
|
33665
33666
|
if (!res || res.status >= 400) throw new Error(`nobody at this site is called @${handle}@${location.host}`);
|
|
33666
33667
|
const doc = await res.json().catch(() => ({}));
|
|
33667
33668
|
const podActorId = (doc.aliases || []).find((a) => /\/ap\/actor$/u.test(String(a)));
|
|
@@ -33809,7 +33810,8 @@ if (typeof document !== "undefined") (async () => {
|
|
|
33809
33810
|
// login and the identity screen lands here.
|
|
33810
33811
|
"no-account-here": { title: "No FediPod account in that pod", retry: "Create one on this pod", go: () => showIdentity() },
|
|
33811
33812
|
"no-account": { title: "No FediPod account in that pod", retry: "Create one on this pod", go: () => showIdentity() },
|
|
33812
|
-
"device-account": { title: "This account is run from a device", retry: "Use another pod", go: signIn }
|
|
33813
|
+
"device-account": { title: "This account is run from a device", retry: "Use another pod", go: signIn },
|
|
33814
|
+
"address-closed": { title: "This address is closed", retry: "Sign in with another account", go: signIn }
|
|
33813
33815
|
}[e.code];
|
|
33814
33816
|
if (e.code === "sign-in-refused" && once("fedipod-renewing")) {
|
|
33815
33817
|
$("loading").hidden = true;
|