fedipod-server 0.6.0 → 0.8.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 +28 -8
- package/config/server.json +11 -0
- package/dist/claims.js +5 -4
- package/dist/components/context.jsonld +6 -0
- package/dist/directory.d.ts +11 -0
- package/dist/directory.js +19 -0
- package/dist/handler.d.ts +18 -2
- package/dist/handler.js +63 -27
- package/dist/handler.jsonld +27 -0
- package/lib/acctfeed.mjs +257 -0
- package/lib/admin.mjs +247 -19
- package/lib/bskygroup.mjs +1 -1
- package/lib/embed.mjs +6 -1
- package/lib/fediacct.mjs +257 -0
- package/lib/front-core.mjs +74 -4
- package/lib/intake.mjs +21 -2
- package/lib/links.mjs +35 -0
- package/lib/mastoapi.mjs +104 -5
- package/lib/publisher.mjs +14 -8
- package/lib/remote.mjs +119 -3
- package/lib/setup.mjs +56 -0
- package/lib/store.mjs +26 -1
- package/lib/wire.mjs +8 -2
- package/package.json +1 -1
- package/phanpy/dist/assets/main-BdqNbG-a.js +1 -1
- package/phanpy/dist/compose/index.html +1 -1
- package/phanpy/dist/index.html +1 -1
- package/phanpy/dist/sw.js +2 -25
- package/phanpy/dist/sw.js.map +1 -0
- package/run-agent.mjs +35 -2
- package/web/admin/admin.js +150 -53
- package/web/admin/index.html +85 -39
- package/web/admin/setup/index.html +20 -4
- package/web/admin/setup/setup.js +50 -10
- package/web/front/admin.html +133 -0
- package/web/front/new-account.html +2 -2
- package/web/front/run.html +20 -16
- package/web/front/solid-oidc-client.js +6 -0
- package/web/front/solid-client-authn.bundle.js +0 -2
package/web/admin/admin.js
CHANGED
|
@@ -28,7 +28,7 @@ const MODERATION = $('moderation');
|
|
|
28
28
|
const STATUS = $('status-ctl');
|
|
29
29
|
const STATUS_PICK = $('status-pick'); // held: getElementById can't see it mid-render
|
|
30
30
|
const BSKY_CTL = $('bsky-ctl');
|
|
31
|
-
const
|
|
31
|
+
const IDENT_CTL = $('ident-ctl');
|
|
32
32
|
const FOLLOWS_CTL = $('follows-ctl');
|
|
33
33
|
const FOLLOWS_PICK = $('follows-pick');
|
|
34
34
|
const UPDATE_CTL = $('update-ctl');
|
|
@@ -113,6 +113,49 @@ document.getElementById('win').addEventListener('win:closed', () => {
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
// One row per identity connected elsewhere, indented under the row that adds
|
|
117
|
+
// them. Each carries its own way out: disconnecting one has nothing to do with
|
|
118
|
+
// the others.
|
|
119
|
+
function identityRows() {
|
|
120
|
+
const out = [];
|
|
121
|
+
const row = (label, ...nodes) => {
|
|
122
|
+
const dt = document.createElement('dt');
|
|
123
|
+
dt.className = 'under';
|
|
124
|
+
dt.textContent = label;
|
|
125
|
+
const dd = document.createElement('dd');
|
|
126
|
+
dd.append(...nodes);
|
|
127
|
+
out.push([dt, dd]);
|
|
128
|
+
};
|
|
129
|
+
if (config.atproto?.connected) {
|
|
130
|
+
const a = document.createElement('a');
|
|
131
|
+
a.href = `https://bsky.app/profile/${config.atproto.handle}`;
|
|
132
|
+
a.textContent = `@${config.atproto.handle}`;
|
|
133
|
+
a.target = '_blank';
|
|
134
|
+
a.rel = 'noopener';
|
|
135
|
+
a.title = ` Open this account's Bluesky page in a new tab`;
|
|
136
|
+
BSKY_CTL.hidden = false;
|
|
137
|
+
BSKY_CROSSPOST.value = config.atproto.crossPost ? 'on' : 'off';
|
|
138
|
+
row('Bluesky', a, ' ', BSKY_CTL);
|
|
139
|
+
} else {
|
|
140
|
+
BSKY_CTL.hidden = true;
|
|
141
|
+
}
|
|
142
|
+
for (const acct of config.fediAccounts || []) {
|
|
143
|
+
const name = document.createElement('span');
|
|
144
|
+
name.textContent = acct.needsReconnect ? `${acct.handle} — sign in again` : acct.handle;
|
|
145
|
+
if (acct.needsReconnect) name.className = 'warn';
|
|
146
|
+
const off = document.createElement('button');
|
|
147
|
+
off.type = 'button';
|
|
148
|
+
off.className = 'inline danger';
|
|
149
|
+
off.textContent = 'Disconnect';
|
|
150
|
+
off.title = ` Stop reading ${acct.handle} and remove its stored token`;
|
|
151
|
+
off.addEventListener('click', async () => {
|
|
152
|
+
if (await write('/fediacct/disconnect', { id: acct.id }, `${acct.handle} disconnected`)) await load();
|
|
153
|
+
});
|
|
154
|
+
row(acct.host, name, ' ', off);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
116
159
|
function render() {
|
|
117
160
|
// The bar names the actor, the same way on every page. Only the tab title is
|
|
118
161
|
// this page's own business.
|
|
@@ -134,7 +177,7 @@ function render() {
|
|
|
134
177
|
['Fediverse identity', config.address || `@${config.handle} — no resolvable address`,
|
|
135
178
|
config.accountId && config.address ? { href: `/admin/client/#/a/${config.accountId}` } : null],
|
|
136
179
|
['Solid identity', config.webId, config.remotePod ? { href: config.remotePod, blank: true } : null],
|
|
137
|
-
['
|
|
180
|
+
['Other identities', 'ctl'],
|
|
138
181
|
['local store', config.home],
|
|
139
182
|
// The address you actually open, not the bare number — the named origin when
|
|
140
183
|
// there is one, since that is what the client and the OAuth redirect use.
|
|
@@ -142,11 +185,14 @@ function render() {
|
|
|
142
185
|
.replace(/\/$/, '')],
|
|
143
186
|
];
|
|
144
187
|
if (config.version || config.update || config.pendingUpgrade?.length) rows.push(['software', 'ctl']);
|
|
188
|
+
rows.push(['gateway', 'ctl']);
|
|
145
189
|
if (config.quiescedAt) rows.push(['parked since', config.quiescedAt]);
|
|
146
190
|
if (config.movedTo) rows.push(['moved to', config.movedTo]);
|
|
147
191
|
// A person gates followers here; a group's gate is the joins control on its
|
|
148
192
|
// kind row, so the row would be a second switch for the same thing.
|
|
149
193
|
if (config.kind !== 'group') rows.splice(2, 0, ['new followers', 'ctl']);
|
|
194
|
+
// Rows that belong under the one being written, appended after it.
|
|
195
|
+
let kids = null;
|
|
150
196
|
for (const [k, v, link] of rows) {
|
|
151
197
|
if (!v) continue;
|
|
152
198
|
const dt = document.createElement('dt');
|
|
@@ -185,6 +231,11 @@ function render() {
|
|
|
185
231
|
FOLLOWS_CTL.hidden = false;
|
|
186
232
|
FOLLOWS_PICK.value = config.autoAcceptFollows ? 'auto' : 'approve';
|
|
187
233
|
}
|
|
234
|
+
if (k === 'gateway') {
|
|
235
|
+
dd.textContent = '';
|
|
236
|
+
dd.append(GATEWAY_CTL);
|
|
237
|
+
refreshGateway();
|
|
238
|
+
}
|
|
188
239
|
if (k === 'software') {
|
|
189
240
|
dd.textContent = '';
|
|
190
241
|
dd.append(UPDATE_CTL);
|
|
@@ -201,27 +252,19 @@ function render() {
|
|
|
201
252
|
UPDATE_WORD.textContent = words.join('; ');
|
|
202
253
|
UPDATE_GO.hidden = !u?.available;
|
|
203
254
|
}
|
|
204
|
-
//
|
|
205
|
-
|
|
255
|
+
// The way to add one. Each account already connected is a row of its own
|
|
256
|
+
// underneath, so this row stays the action and never becomes a list.
|
|
257
|
+
if (k === 'Other identities') {
|
|
206
258
|
dd.textContent = '';
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
a.textContent = `@${config.atproto.handle}`;
|
|
211
|
-
a.target = '_blank';
|
|
212
|
-
a.rel = 'noopener';
|
|
213
|
-
a.title = ` Open this account's Bluesky page in a new tab`;
|
|
214
|
-
dd.append(a, ' ', BSKY_CTL);
|
|
215
|
-
BSKY_CTL.hidden = false;
|
|
216
|
-
BSKY_CONNECT_CTL.hidden = true;
|
|
217
|
-
BSKY_CROSSPOST.value = config.atproto.crossPost ? 'on' : 'off';
|
|
218
|
-
} else {
|
|
219
|
-
dd.append(BSKY_CONNECT_CTL);
|
|
220
|
-
BSKY_CONNECT_CTL.hidden = false;
|
|
221
|
-
BSKY_CTL.hidden = true;
|
|
222
|
-
}
|
|
259
|
+
dd.append(IDENT_CTL);
|
|
260
|
+
IDENT_CTL.hidden = false;
|
|
261
|
+
kids = identityRows();
|
|
223
262
|
}
|
|
224
263
|
facts.append(dt, dd);
|
|
264
|
+
if (kids) {
|
|
265
|
+
for (const [ckt, ckd] of kids) facts.append(ckt, ckd);
|
|
266
|
+
kids = null;
|
|
267
|
+
}
|
|
225
268
|
}
|
|
226
269
|
if (!config.address) {
|
|
227
270
|
const p = document.createElement('p');
|
|
@@ -237,7 +280,6 @@ function render() {
|
|
|
237
280
|
renderAliases();
|
|
238
281
|
renderOthers();
|
|
239
282
|
renderInbox();
|
|
240
|
-
renderGateway();
|
|
241
283
|
if (config.kind === 'group') {
|
|
242
284
|
// Its lists have no bound, so this page scrolls — see body.group in the CSS.
|
|
243
285
|
document.body.classList.add('group');
|
|
@@ -415,9 +457,17 @@ function setRail(closed) {
|
|
|
415
457
|
$('rail-toggle').addEventListener('click', () => setRail(!$('rail').classList.contains('closed')));
|
|
416
458
|
if (matchMedia('(max-width: 47rem)').matches) setRail(true);
|
|
417
459
|
|
|
418
|
-
|
|
460
|
+
// One way in for both networks, because from the record they are the same
|
|
461
|
+
// thing: an account elsewhere whose timeline joins the one you read here.
|
|
462
|
+
$('ident-open').addEventListener('click', () => {
|
|
419
463
|
closePanels();
|
|
420
|
-
solWindow.show('
|
|
464
|
+
solWindow.show('ident-form', 'Connect an identity');
|
|
465
|
+
});
|
|
466
|
+
$('ident-pick-fedi').addEventListener('click', () => {
|
|
467
|
+
solWindow.show('fediacct-form', 'Connect a Fediverse account');
|
|
468
|
+
});
|
|
469
|
+
$('ident-pick-bsky').addEventListener('click', () => {
|
|
470
|
+
solWindow.show('bsky-form', 'Connect a Bluesky account');
|
|
421
471
|
});
|
|
422
472
|
|
|
423
473
|
let bskyBusy = false;
|
|
@@ -438,6 +488,20 @@ $('bsky-form').addEventListener('submit', async (ev) => {
|
|
|
438
488
|
}
|
|
439
489
|
} finally { bskyBusy = false; }
|
|
440
490
|
});
|
|
491
|
+
// The sign-in happens at the other server, so this hands the browser over
|
|
492
|
+
// rather than taking a password. What comes back lands on /fediacct/callback,
|
|
493
|
+
// which this machine answers and nowhere else does.
|
|
494
|
+
let fediBusy = false;
|
|
495
|
+
$('fediacct-form').addEventListener('submit', async (ev) => {
|
|
496
|
+
ev.preventDefault();
|
|
497
|
+
if (fediBusy) return;
|
|
498
|
+
fediBusy = true;
|
|
499
|
+
try {
|
|
500
|
+
const r = await write('/fediacct/connect', { host: $('fediacct-host').value.trim() }, '');
|
|
501
|
+
if (r?.authorize) location.href = r.authorize;
|
|
502
|
+
} finally { fediBusy = false; }
|
|
503
|
+
});
|
|
504
|
+
|
|
441
505
|
$('bsky-disconnect').addEventListener('click', async () => {
|
|
442
506
|
if (bskyBusy) return;
|
|
443
507
|
bskyBusy = true;
|
|
@@ -937,35 +1001,62 @@ async function renderInbox() {
|
|
|
937
1001
|
panel.hidden = false;
|
|
938
1002
|
}
|
|
939
1003
|
|
|
940
|
-
// The gateway
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
1004
|
+
// The gateway, as a facts row under software: attach through a multi-user
|
|
1005
|
+
// front with this agent's own credential, detach back to the pod inbox. The
|
|
1006
|
+
// forms live in the floating window, like every other disclosure.
|
|
1007
|
+
const GATEWAY_CTL = $('gateway-ctl'); // held: it rides into a generated row
|
|
1008
|
+
const GATEWAY_WORD = $('gateway-word');
|
|
1009
|
+
const GW_OPEN_ATTACH = $('gateway-open-attach');
|
|
1010
|
+
const GW_OPEN_DETACH = $('gateway-open-detach');
|
|
1011
|
+
let gwState = null;
|
|
1012
|
+
async function refreshGateway() {
|
|
944
1013
|
const { status, json: g } = await api('/gateway');
|
|
945
|
-
if (status !== 200 || !g) return;
|
|
946
|
-
|
|
1014
|
+
if (status !== 200 || !g) return;
|
|
1015
|
+
gwState = g;
|
|
1016
|
+
GATEWAY_CTL.hidden = false;
|
|
947
1017
|
if (g.configured) {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
$('gateway-attached').hidden = false;
|
|
1018
|
+
let host = g.url;
|
|
1019
|
+
try { host = new URL(g.url).host; } catch { /* show it as-is */ }
|
|
1020
|
+
const frontName = g.frontActor?.match(/\/u\/([^/]+)\/ap\/actor\/?$/)?.[1];
|
|
1021
|
+
GATEWAY_WORD.textContent = frontName ? `${host} — publishing as @${frontName}@${host}` : host;
|
|
1022
|
+
GW_OPEN_ATTACH.hidden = true;
|
|
1023
|
+
GW_OPEN_DETACH.hidden = false;
|
|
955
1024
|
} else {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
if (!$('gw-name').value && config?.handle) $('gw-name').value = config.handle;
|
|
1025
|
+
GATEWAY_WORD.textContent = '';
|
|
1026
|
+
GW_OPEN_ATTACH.hidden = false;
|
|
1027
|
+
GW_OPEN_DETACH.hidden = true;
|
|
960
1028
|
}
|
|
961
1029
|
}
|
|
1030
|
+
const gwShape = () => document.querySelector('input[name=gwShape]:checked')?.value || 'pod';
|
|
1031
|
+
function gwPreviews() {
|
|
1032
|
+
$('gw-pod-preview').textContent = config?.address || `@${config?.handle || 'you'}@your.pod`;
|
|
1033
|
+
try { $('gw-front-host').textContent = new URL($('gw-front').value.trim()).host; }
|
|
1034
|
+
catch { /* not a URL yet — the placeholder host stands */ }
|
|
1035
|
+
}
|
|
1036
|
+
function gwShapeChanged() { gwPreviews(); gwCheck(); }
|
|
1037
|
+
for (const r of document.querySelectorAll('input[name=gwShape]')) r.addEventListener('change', gwShapeChanged);
|
|
1038
|
+
// Typing in the blank IS choosing that shape.
|
|
1039
|
+
$('gw-name').addEventListener('focus', () => {
|
|
1040
|
+
const r = document.querySelector('input[name=gwShape][value=front]');
|
|
1041
|
+
if (!r.checked) { r.checked = true; gwShapeChanged(); }
|
|
1042
|
+
});
|
|
1043
|
+
GW_OPEN_ATTACH.addEventListener('click', () => {
|
|
1044
|
+
gwShapeChanged();
|
|
1045
|
+
solWindow.show('gateway-attach-form', 'Attach to a gateway');
|
|
1046
|
+
});
|
|
1047
|
+
GW_OPEN_DETACH.addEventListener('click', () => {
|
|
1048
|
+
$('gw-detach-fronted').hidden = !gwState?.frontActor;
|
|
1049
|
+
$('gw-detach-plain').hidden = !!gwState?.frontActor;
|
|
1050
|
+
solWindow.show('gateway-detach-form', 'Detach from gateway');
|
|
1051
|
+
});
|
|
962
1052
|
// Live availability, asked through the agent (the front answers it without CORS).
|
|
1053
|
+
let gwTimer = null;
|
|
963
1054
|
function gwCheck() {
|
|
964
1055
|
clearTimeout(gwTimer);
|
|
965
1056
|
const front = $('gw-front').value.trim().replace(/\/+$/, '');
|
|
966
1057
|
const name = $('gw-name').value.trim().toLowerCase();
|
|
967
1058
|
$('gw-name-msg').textContent = ''; $('gw-name-msg').className = 'hint';
|
|
968
|
-
if (!front || !name) return;
|
|
1059
|
+
if (gwShape() !== 'front' || !front || !name) return;
|
|
969
1060
|
gwTimer = setTimeout(async () => {
|
|
970
1061
|
const { status, json } = await postJson('/gateway', { action: 'check', front, handle: name });
|
|
971
1062
|
if (status !== 200 || !json) return;
|
|
@@ -975,24 +1066,30 @@ function gwCheck() {
|
|
|
975
1066
|
$('gw-name-msg').className = json.available ? 'hint' : 'warn';
|
|
976
1067
|
}, 300);
|
|
977
1068
|
}
|
|
978
|
-
$('gw-front').addEventListener('input', gwCheck);
|
|
979
|
-
$('gw-name').addEventListener('input', gwCheck);
|
|
980
|
-
$('gw-attach').
|
|
1069
|
+
$('gw-front').addEventListener('input', () => { gwPreviews(); gwCheck(); });
|
|
1070
|
+
$('gw-name').addEventListener('input', () => { gwPreviews(); gwCheck(); });
|
|
1071
|
+
$('gw-attach').addEventListener('click', async () => {
|
|
981
1072
|
const front = $('gw-front').value.trim().replace(/\/+$/, '');
|
|
982
|
-
const fronted =
|
|
1073
|
+
const fronted = gwShape() === 'front';
|
|
1074
|
+
const name = $('gw-name').value.trim().toLowerCase();
|
|
1075
|
+
if (fronted && !name) { say('give the handle you want at the gateway', 'err'); return; }
|
|
983
1076
|
$('gw-attach').disabled = true;
|
|
1077
|
+
// Pod-based sends no name: the door's label is the agent's business, not
|
|
1078
|
+
// the user's, and the agent picks a free variant by itself.
|
|
984
1079
|
const r = await write('/gateway',
|
|
985
|
-
{ action: 'attach', front, handle:
|
|
986
|
-
fronted ? 'attached —
|
|
1080
|
+
{ action: 'attach', front, ...(fronted ? { handle: name, fronted: true } : {}) },
|
|
1081
|
+
fronted ? 'attached — the agent is restarting to publish under the gateway handle; reload in a moment'
|
|
987
1082
|
: 'attached — your mail now arrives through the gateway, filtered');
|
|
988
1083
|
$('gw-attach').disabled = false;
|
|
989
|
-
if (r)
|
|
990
|
-
};
|
|
991
|
-
$('gw-detach').
|
|
1084
|
+
if (r) { closePanels(); refreshGateway(); }
|
|
1085
|
+
});
|
|
1086
|
+
$('gw-detach').addEventListener('click', async () => {
|
|
1087
|
+
const fronted = !!gwState?.frontActor;
|
|
992
1088
|
const r = await write('/gateway', { action: 'forget' },
|
|
993
|
-
'detached — the
|
|
994
|
-
|
|
995
|
-
}
|
|
1089
|
+
fronted ? 'detached — the agent is restarting under your pod\'s own name; reload in a moment'
|
|
1090
|
+
: 'detached — the actor was republished advertising your pod\'s own inbox');
|
|
1091
|
+
if (r) { closePanels(); refreshGateway(); }
|
|
1092
|
+
});
|
|
996
1093
|
|
|
997
1094
|
$('inbox-keep').addEventListener('click', () => {
|
|
998
1095
|
dismissed = true;
|
package/web/admin/index.html
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
:root { color-scheme: light dark; font-size: 112.5%;
|
|
16
16
|
--bar-col: 1280px; /* the bar centres on the same column */
|
|
17
17
|
--link: #1a4f8a; --heading: #5b3a8e;
|
|
18
|
-
--ink: #1b1c1e; --muted: #
|
|
18
|
+
--ink: #1b1c1e; --muted: #4a4e53;
|
|
19
19
|
--btn-bg: #f4efe6; --btn-bg-hover: #ece3d2; --btn-bg-active: #e2d5bd; --btn-edge: #8a7a61;
|
|
20
20
|
--field-bg: #ffffff; --field-bg-hover: #f7f4ee; --field-edge: #767676;
|
|
21
21
|
--danger: #b00020; --ring: var(--link);
|
|
22
22
|
--chevron: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6'><path d='M1 1l4 4 4-4' fill='none' stroke='%23444444' stroke-width='1.6' stroke-linecap='round'/></svg>"); }
|
|
23
23
|
@media (prefers-color-scheme: dark) {
|
|
24
24
|
:root { --link: #7fb3e8; --heading: #b9a3e3;
|
|
25
|
-
--ink: #e8eaed; --muted: #
|
|
25
|
+
--ink: #e8eaed; --muted: #b9bdc2;
|
|
26
26
|
--btn-bg: #35302a; --btn-bg-hover: #423b31; --btn-bg-active: #4d4436; --btn-edge: #9a8d7a;
|
|
27
27
|
--field-bg: #383d44; --field-bg-hover: #40464e; --field-edge: #9aa0a6;
|
|
28
28
|
--danger: #ff8a8a;
|
|
@@ -98,7 +98,8 @@ p { margin: .5rem 0; }
|
|
|
98
98
|
fieldset { border: 1px solid #bbb; border-radius: .4rem; margin: 1rem 0; padding: .75rem 1rem 1rem; }
|
|
99
99
|
legend { padding: 0 .3rem; font-weight: 600; color: var(--heading); }
|
|
100
100
|
label { display: block; margin: .75rem 0 .2rem; }
|
|
101
|
-
.hint { color: var(--muted); font-size:
|
|
101
|
+
.hint { color: var(--muted); font-size: 1rem; margin: .15rem 0 0; }
|
|
102
|
+
.choice input[type=text] { width: 9ch; display: inline-block; padding: .15rem .3rem; }
|
|
102
103
|
/* SETTINGS: light wells, unmistakably fields, in both schemes. */
|
|
103
104
|
input[type=text], input[type=email], input[type=url], input[type=password], textarea {
|
|
104
105
|
font: inherit; width: 100%; padding: .5rem; box-sizing: border-box;
|
|
@@ -132,6 +133,9 @@ dl { margin: .5rem 0; } /* grid only above 34rem — see the media query
|
|
|
132
133
|
gap: .6rem .9rem; align-items: baseline; }
|
|
133
134
|
}
|
|
134
135
|
dt { color: var(--muted); }
|
|
136
|
+
/* An identity connected under "Other identities" belongs to the row above it,
|
|
137
|
+
and the indent is what says so. */
|
|
138
|
+
dt.under { padding-left: 1.25rem; }
|
|
135
139
|
dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
136
140
|
.rows { list-style: none; padding: 0; margin: .3rem 0; }
|
|
137
141
|
.rows li { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
|
|
@@ -215,16 +219,16 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
215
219
|
<label class="choice"><input type="radio" name="newKind" value="person" checked>
|
|
216
220
|
A <strong>person</strong> — an account you read and post from</label>
|
|
217
221
|
<label class="choice"><input type="radio" name="newKind" value="group">
|
|
218
|
-
A <strong>group</strong> of
|
|
222
|
+
A <strong>group</strong> of Fediverse users you manage</label>
|
|
219
223
|
</fieldset>
|
|
220
224
|
|
|
221
225
|
<fieldset>
|
|
222
226
|
<legend>Actor's Handle</legend>
|
|
223
227
|
<label for="new-handle">Handle</label>
|
|
224
228
|
<input type="text" id="new-handle" autocomplete="off"
|
|
225
|
-
placeholder="handle part of
|
|
229
|
+
placeholder="handle part of Fediverse address: @HANDLE@server"
|
|
226
230
|
aria-describedby="new-handle-hint">
|
|
227
|
-
<p class="hint" id="new-handle-hint">The name in your
|
|
231
|
+
<p class="hint" id="new-handle-hint">The name in your Fediverse address. This is permanent.
|
|
228
232
|
Letters, digits, hyphens, and underscores only.</p>
|
|
229
233
|
</fieldset>
|
|
230
234
|
|
|
@@ -302,8 +306,8 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
302
306
|
<option value="auto">accepted automatically</option>
|
|
303
307
|
</select>
|
|
304
308
|
</span>
|
|
305
|
-
<span id="
|
|
306
|
-
<button type="button" id="
|
|
309
|
+
<span id="ident-ctl" hidden>
|
|
310
|
+
<button type="button" id="ident-open" class="inline" title=" Connect an account you hold on another network, so its timeline joins the one you read here">Connect an identity</button>
|
|
307
311
|
</span>
|
|
308
312
|
<span id="bsky-ctl" hidden>
|
|
309
313
|
<select id="bsky-crosspost" aria-label="Bluesky crossposting" title=" Whether your public posts also go to the Bluesky account">
|
|
@@ -314,36 +318,13 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
314
318
|
</span>
|
|
315
319
|
</section>
|
|
316
320
|
|
|
317
|
-
|
|
318
|
-
<
|
|
319
|
-
<
|
|
320
|
-
|
|
321
|
-
<
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
spam is dropped, and the rest lands in your pod as before. Attaching proves the pod
|
|
325
|
-
with this agent's own credential — no password leaves this machine.</p>
|
|
326
|
-
<p>
|
|
327
|
-
<label for="gw-front">Gateway</label>
|
|
328
|
-
<input type="url" id="gw-front" placeholder="https://fedipod.net" autocomplete="off">
|
|
329
|
-
<label for="gw-name">Your name there</label>
|
|
330
|
-
<input type="text" id="gw-name" autocomplete="off" autocapitalize="off" spellcheck="false">
|
|
331
|
-
</p>
|
|
332
|
-
<p id="gw-name-msg" class="hint"></p>
|
|
333
|
-
<p>
|
|
334
|
-
<select id="gw-shape" aria-label="Name shape" title=" Which address the fediverse sees — the pod-based name keeps everything on your pod; a gateway-based name survives changing pods">
|
|
335
|
-
<option value="pod">keep my pod-based name</option>
|
|
336
|
-
<option value="front">use a gateway-based name</option>
|
|
337
|
-
</select>
|
|
338
|
-
<button id="gw-attach" class="primary" title=" Create the gateway account and point your mail through its door">Attach</button>
|
|
339
|
-
</p>
|
|
340
|
-
</div>
|
|
341
|
-
<div id="gateway-attached" hidden>
|
|
342
|
-
<p>
|
|
343
|
-
<button id="gw-detach" class="inline danger" title=" Republish the actor with your pod's own inbox and forget the gateway">Detach</button>
|
|
344
|
-
</p>
|
|
345
|
-
</div>
|
|
346
|
-
</section>
|
|
321
|
+
<span id="gateway-ctl" hidden>
|
|
322
|
+
<span id="gateway-word"></span>
|
|
323
|
+
<button type="button" id="gateway-open-attach" class="inline" hidden
|
|
324
|
+
title=" Attach this account to a mail-filtering gateway">Attach to a gateway</button>
|
|
325
|
+
<button type="button" id="gateway-open-detach" class="inline danger" hidden
|
|
326
|
+
title=" Go back to your pod's own inbox">Detach from gateway</button>
|
|
327
|
+
</span>
|
|
347
328
|
|
|
348
329
|
<section id="pane-group" hidden>
|
|
349
330
|
<!-- Both queues are consequences of a moderation setting: with the setting off
|
|
@@ -366,7 +347,7 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
366
347
|
on arrival. Shown only when something is actually waiting. -->
|
|
367
348
|
<div id="block-modqueue" hidden>
|
|
368
349
|
<h3>Moderation requests <span class="muted" id="modqueue-count"></span></h3>
|
|
369
|
-
<p class="hint">Asked for by a moderator over the
|
|
350
|
+
<p class="hint">Asked for by a moderator over the Fediverse. A delivery cannot prove who
|
|
370
351
|
sent it, so nothing here has been acted on.</p>
|
|
371
352
|
<ul class="rows" role="list" id="modqueue"></ul>
|
|
372
353
|
</div>
|
|
@@ -445,9 +426,74 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
445
426
|
<button id="bsky-connect" type="submit" class="primary" title=" Sign in to the Bluesky account with the app password">Connect</button>
|
|
446
427
|
</p>
|
|
447
428
|
</form>
|
|
429
|
+
<form id="ident-form" hidden>
|
|
430
|
+
<p class="hint">Either kind brings that account's timeline and notifications
|
|
431
|
+
into the feed you read here, and acts as that account when you reply to,
|
|
432
|
+
favourite or boost one of its posts.</p>
|
|
433
|
+
<p>
|
|
434
|
+
<button type="button" id="ident-pick-fedi" class="primary" title=" Sign in at a Mastodon-API server: Mastodon, GoToSocial, Pleroma, Akkoma, Pixelfed or Friendica">A Fediverse account</button>
|
|
435
|
+
<button type="button" id="ident-pick-bsky" title=" Sign in to Bluesky or another ATProto server with an app password">A Bluesky account</button>
|
|
436
|
+
</p>
|
|
437
|
+
</form>
|
|
438
|
+
<form id="fediacct-form" hidden>
|
|
439
|
+
<ol class="hint">
|
|
440
|
+
<li>Name the server your other account is on.</li>
|
|
441
|
+
<li>That server asks you to sign in and to allow FediPod. It is the only
|
|
442
|
+
place your password is typed; this agent never sees or stores one.</li>
|
|
443
|
+
<li>Its timeline and notifications then join the ones you already read here,
|
|
444
|
+
and a like or a reply on one of its posts goes out as that account.</li>
|
|
445
|
+
<li>What arrives is held on this machine for reading. None of it is written
|
|
446
|
+
to your pod.</li>
|
|
447
|
+
</ol>
|
|
448
|
+
<label for="fediacct-host">server</label>
|
|
449
|
+
<input type="text" id="fediacct-host" placeholder="mastodon.social" autocomplete="off"
|
|
450
|
+
title=" The server your other account lives on — Mastodon, GoToSocial, Pleroma, Akkoma, Pixelfed and Friendica all speak the API this needs">
|
|
451
|
+
<p>
|
|
452
|
+
<button id="fediacct-connect" type="submit" class="primary" title=" Go to that server to sign in and allow FediPod">Continue to that server</button>
|
|
453
|
+
</p>
|
|
454
|
+
</form>
|
|
448
455
|
<!-- Every consequence is written here rather than assembled in script: the
|
|
449
456
|
page asking "are you sure" has to say what it is sure about, and this is
|
|
450
457
|
the only record of it. Script picks which block to show, nothing else. -->
|
|
458
|
+
<!-- The gateway popups: attach proves the pod with this agent's own credential;
|
|
459
|
+
detach republishes the pod's own inbox. -->
|
|
460
|
+
<form id="gateway-attach-form" hidden>
|
|
461
|
+
<p class="hint">A gateway filters your Fediverse mail before it reaches your pod: each
|
|
462
|
+
delivery is verified at its door, spam is dropped, and the rest lands in your pod as
|
|
463
|
+
before. Attaching proves the pod with this agent's own credential — no password
|
|
464
|
+
leaves this machine.</p>
|
|
465
|
+
<label for="gw-front">gateway</label>
|
|
466
|
+
<input type="url" id="gw-front" placeholder="https://fedipod.net" autocomplete="off">
|
|
467
|
+
<fieldset>
|
|
468
|
+
<legend>Your Fediverse handle</legend>
|
|
469
|
+
<label class="choice"><input type="radio" name="gwShape" value="pod" checked>
|
|
470
|
+
Keep my pod-based handle: <strong id="gw-pod-preview"></strong><br>
|
|
471
|
+
<span class="hint">Your address stays tied to your own pod — you can drop the
|
|
472
|
+
gateway any time and keep everything.</span></label>
|
|
473
|
+
<label class="choice"><input type="radio" name="gwShape" value="front">
|
|
474
|
+
Create a handle at the gateway:
|
|
475
|
+
@<input type="text" id="gw-name" placeholder="name" autocomplete="off"
|
|
476
|
+
autocapitalize="off" spellcheck="false">@<span id="gw-front-host">fedipod.net</span><br>
|
|
477
|
+
<span class="hint">Your address lives on the gateway's domain, but not your data
|
|
478
|
+
or key; you can change pods later and keep the handle.</span>
|
|
479
|
+
<span id="gw-name-msg" class="hint"></span></label>
|
|
480
|
+
</fieldset>
|
|
481
|
+
<p>
|
|
482
|
+
<button id="gw-attach" type="button" class="primary"
|
|
483
|
+
title=" Create the gateway account and point your mail through its door">Attach</button>
|
|
484
|
+
</p>
|
|
485
|
+
</form>
|
|
486
|
+
<form id="gateway-detach-form" hidden>
|
|
487
|
+
<p class="hint" id="gw-detach-plain">Deliveries go back to your pod's own inbox — the
|
|
488
|
+
actor is republished advertising it. Your name and data never moved.</p>
|
|
489
|
+
<p class="warn" id="gw-detach-fronted" hidden>This identity publishes under the gateway's
|
|
490
|
+
name. Detaching moves every published id back to the pod — a rename other servers
|
|
491
|
+
see, not just a mail change.</p>
|
|
492
|
+
<p>
|
|
493
|
+
<button id="gw-detach" type="button" class="primary"
|
|
494
|
+
title=" Forget the gateway and republish the actor with the pod inbox">Detach</button>
|
|
495
|
+
</p>
|
|
496
|
+
</form>
|
|
451
497
|
<form id="confirm-form" hidden>
|
|
452
498
|
<div id="warn-rotate-key" class="warn" hidden>
|
|
453
499
|
<p>A new keypair replaces the one in this home and the actor is republished so other
|
|
@@ -20,7 +20,7 @@ fieldset { border: 1px solid #bbb; border-radius: .4rem; margin: 1rem 0; padding
|
|
|
20
20
|
legend { padding: 0 .3rem; font-weight: 600; }
|
|
21
21
|
label { display: block; margin: .75rem 0 .2rem; }
|
|
22
22
|
.hint { color: #666; font-size: .9rem; margin: .15rem 0 0; }
|
|
23
|
-
input[type=text], input[type=email], input[type=url], input[type=password], textarea {
|
|
23
|
+
input[type=text], input[type=email], input[type=url], input[type=password], select, textarea {
|
|
24
24
|
font: inherit; width: 100%; padding: .5rem; box-sizing: border-box;
|
|
25
25
|
border: 1px solid #767676; border-radius: .3rem; background: Field; color: FieldText;
|
|
26
26
|
}
|
|
@@ -94,7 +94,8 @@ button:disabled { opacity: .5; cursor: default; }
|
|
|
94
94
|
<ul class="steps" id="run-steps"></ul>
|
|
95
95
|
<p class="sr-only" id="run-say" role="status"></p>
|
|
96
96
|
<p class="err" id="run-error" role="alert"></p>
|
|
97
|
-
<p><button id="run-again" title=" Try the step that failed again" hidden>Try again</button
|
|
97
|
+
<p><button id="run-again" title=" Try the step that failed again" hidden>Try again</button>
|
|
98
|
+
<button id="run-reenter" title=" Discard the credential and enter the account and pod again" hidden>Re-enter credentials</button></p>
|
|
98
99
|
</section>
|
|
99
100
|
|
|
100
101
|
<section id="pane-form" hidden>
|
|
@@ -124,8 +125,19 @@ button:disabled { opacity: .5; cursor: default; }
|
|
|
124
125
|
<label class="choice"><input type="radio" name="mode" value="existing">
|
|
125
126
|
Use a pod I already have</label>
|
|
126
127
|
|
|
127
|
-
<
|
|
128
|
-
|
|
128
|
+
<div id="row-issuer-new">
|
|
129
|
+
<label for="issuer-new">Solid pod provider</label>
|
|
130
|
+
<select id="issuer-new" name="issuerNew" aria-describedby="issuer-new-hint">
|
|
131
|
+
<option value="https://solidcommunity.net">solidcommunity.net</option>
|
|
132
|
+
</select>
|
|
133
|
+
<p class="hint" id="issuer-new-hint">Providers that give each pod its own subdomain, so the
|
|
134
|
+
Fediverse address works everywhere.</p>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div id="row-issuer-existing" hidden>
|
|
138
|
+
<label for="issuer">Solid identity provider</label>
|
|
139
|
+
<input type="url" id="issuer" name="issuer" value="https://solidcommunity.net" autocomplete="url">
|
|
140
|
+
</div>
|
|
129
141
|
|
|
130
142
|
<label for="email">Solid Account email/username</label>
|
|
131
143
|
<input type="email" id="email" name="email" autocomplete="email" placeholder="you@example.org">
|
|
@@ -153,6 +165,10 @@ button:disabled { opacity: .5; cursor: default; }
|
|
|
153
165
|
</div>
|
|
154
166
|
</fieldset>
|
|
155
167
|
|
|
168
|
+
<p id="row-reenter" class="warn" hidden>Finishing a setup that already has an account credential.
|
|
169
|
+
Wrong account or pod?
|
|
170
|
+
<button type="button" id="form-reenter" title=" Discard the credential and enter the account and pod again">Re-enter credentials</button></p>
|
|
171
|
+
|
|
156
172
|
<h2>You will be</h2>
|
|
157
173
|
<p class="address" id="preview">…</p>
|
|
158
174
|
<div id="preview-notes"></div>
|
package/web/admin/setup/setup.js
CHANGED
|
@@ -60,20 +60,34 @@ function paneForm() {
|
|
|
60
60
|
if (fr.kind) $('form').elements.kind.value = fr.kind;
|
|
61
61
|
if (fr.handle) $('handle').value = fr.handle;
|
|
62
62
|
if (fr.pod) { $('form').elements.mode.value = 'existing'; $('pod').value = fr.pod; }
|
|
63
|
-
|
|
63
|
+
// A gateway-arranged signup names its own provider; give the dropdown that
|
|
64
|
+
// choice too, so the carried-over answer survives the narrower control.
|
|
65
|
+
if (fr.issuer) {
|
|
66
|
+
$('issuer').value = fr.issuer;
|
|
67
|
+
const sel = $('issuer-new');
|
|
68
|
+
if (![...sel.options].some(o => o.value === fr.issuer)) {
|
|
69
|
+
const o = document.createElement('option');
|
|
70
|
+
o.value = fr.issuer;
|
|
71
|
+
o.textContent = host(fr.issuer);
|
|
72
|
+
sel.appendChild(o);
|
|
73
|
+
}
|
|
74
|
+
sel.value = fr.issuer;
|
|
75
|
+
}
|
|
64
76
|
if (fr.gatewayHost) strap(`finishing your ${fr.gatewayHost} signup — enter your Solid account details`);
|
|
65
77
|
}
|
|
66
78
|
// Resuming: the account exists and the credential is minted. Asking for a
|
|
67
79
|
// password again would mint a second one and orphan the first, which cannot
|
|
68
80
|
// be recovered.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
// Set both ways, not just hidden-when-resuming: re-entering credentials
|
|
82
|
+
// re-renders this in the other mode, and a one-way hide would strand the
|
|
83
|
+
// account and pod fields off-screen. Resuming settles the account and the
|
|
84
|
+
// credential, so those fields go; a fresh run shows them.
|
|
85
|
+
$('row-reenter').hidden = !state.resumable;
|
|
86
|
+
$('fs-pod').hidden = state.resumable;
|
|
87
|
+
// The password field also goes when AP_PASSWORD supplied it in the environment.
|
|
88
|
+
$('row-password').hidden = state.resumable || state.passwordSupplied;
|
|
89
|
+
$('submit').textContent = state.resumable ? 'Finish setting up' : 'Create it';
|
|
90
|
+
if (state.resumable) $('form-reenter').onclick = reEnter; // the escape from a wrong credential
|
|
77
91
|
show('pane-form');
|
|
78
92
|
// The markup's autofocus was set while this pane was still hidden, so it
|
|
79
93
|
// never fired — move focus to the first field now that the pane is showing.
|
|
@@ -94,7 +108,7 @@ function answers() {
|
|
|
94
108
|
kind,
|
|
95
109
|
mode,
|
|
96
110
|
handle: f.handle.value.trim(),
|
|
97
|
-
issuer: f.issuer.value.trim(),
|
|
111
|
+
issuer: (mode === 'new' ? f.issuerNew.value : f.issuer.value).trim(),
|
|
98
112
|
email: f.email.value.trim(),
|
|
99
113
|
};
|
|
100
114
|
if (mode === 'new') a.podName = f.podName.value.trim() || a.handle;
|
|
@@ -123,6 +137,8 @@ function onEdit() {
|
|
|
123
137
|
const mode = state.resumable ? 'existing' : $('form').elements.mode.value;
|
|
124
138
|
$('row-podname').hidden = mode !== 'new';
|
|
125
139
|
$('row-pod').hidden = mode === 'new';
|
|
140
|
+
$('row-issuer-new').hidden = mode !== 'new';
|
|
141
|
+
$('row-issuer-existing').hidden = mode === 'new';
|
|
126
142
|
clearTimeout(editTimer);
|
|
127
143
|
editTimer = setTimeout(preview, 150);
|
|
128
144
|
}
|
|
@@ -194,6 +210,30 @@ async function watchRun() {
|
|
|
194
210
|
state = json || state;
|
|
195
211
|
paneForm();
|
|
196
212
|
};
|
|
213
|
+
// A failure past the credential (a wrong pod answering 401 to the first
|
|
214
|
+
// write) leaves a credential that "Try again" can only re-use. Offer to
|
|
215
|
+
// discard it and re-enter the account and pod — only when there is one.
|
|
216
|
+
const { json: st } = await api('/setup/state');
|
|
217
|
+
const reenter = $('run-reenter');
|
|
218
|
+
reenter.hidden = !st?.hasCredential;
|
|
219
|
+
reenter.onclick = reEnter;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Discard the saved credential (server confirms, then the full form returns).
|
|
223
|
+
// Destructive and unrecoverable — a minted credential is shown once — so it
|
|
224
|
+
// asks first.
|
|
225
|
+
async function reEnter() {
|
|
226
|
+
if (!confirm('Discard the saved account credential and enter the account and pod again?\n\n'
|
|
227
|
+
+ 'The old credential is left on your account — revoke it from the account dashboard if you want it gone.')) return;
|
|
228
|
+
const { status, json } = await postJson('/setup/reset', {});
|
|
229
|
+
if (status !== 200) {
|
|
230
|
+
const box = $('pane-form').hidden ? $('run-error') : $('form-error');
|
|
231
|
+
box.textContent = json?.error || `could not reset (HTTP ${status})`;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const { json: st } = await api('/setup/state');
|
|
235
|
+
state = st || {};
|
|
236
|
+
paneForm();
|
|
197
237
|
}
|
|
198
238
|
|
|
199
239
|
function renderRun(run) {
|