fedipod-server 0.7.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 +176 -6
- 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 +101 -2
- package/lib/remote.mjs +119 -3
- package/lib/setup.mjs +56 -0
- package/lib/store.mjs +26 -1
- package/lib/wire.mjs +2 -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 +31 -0
- package/web/admin/admin.js +81 -22
- package/web/admin/index.html +37 -8
- 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.
|
|
@@ -148,6 +191,8 @@ function render() {
|
|
|
148
191
|
// A person gates followers here; a group's gate is the joins control on its
|
|
149
192
|
// kind row, so the row would be a second switch for the same thing.
|
|
150
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;
|
|
151
196
|
for (const [k, v, link] of rows) {
|
|
152
197
|
if (!v) continue;
|
|
153
198
|
const dt = document.createElement('dt');
|
|
@@ -207,27 +252,19 @@ function render() {
|
|
|
207
252
|
UPDATE_WORD.textContent = words.join('; ');
|
|
208
253
|
UPDATE_GO.hidden = !u?.available;
|
|
209
254
|
}
|
|
210
|
-
//
|
|
211
|
-
|
|
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') {
|
|
212
258
|
dd.textContent = '';
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
a.textContent = `@${config.atproto.handle}`;
|
|
217
|
-
a.target = '_blank';
|
|
218
|
-
a.rel = 'noopener';
|
|
219
|
-
a.title = ` Open this account's Bluesky page in a new tab`;
|
|
220
|
-
dd.append(a, ' ', BSKY_CTL);
|
|
221
|
-
BSKY_CTL.hidden = false;
|
|
222
|
-
BSKY_CONNECT_CTL.hidden = true;
|
|
223
|
-
BSKY_CROSSPOST.value = config.atproto.crossPost ? 'on' : 'off';
|
|
224
|
-
} else {
|
|
225
|
-
dd.append(BSKY_CONNECT_CTL);
|
|
226
|
-
BSKY_CONNECT_CTL.hidden = false;
|
|
227
|
-
BSKY_CTL.hidden = true;
|
|
228
|
-
}
|
|
259
|
+
dd.append(IDENT_CTL);
|
|
260
|
+
IDENT_CTL.hidden = false;
|
|
261
|
+
kids = identityRows();
|
|
229
262
|
}
|
|
230
263
|
facts.append(dt, dd);
|
|
264
|
+
if (kids) {
|
|
265
|
+
for (const [ckt, ckd] of kids) facts.append(ckt, ckd);
|
|
266
|
+
kids = null;
|
|
267
|
+
}
|
|
231
268
|
}
|
|
232
269
|
if (!config.address) {
|
|
233
270
|
const p = document.createElement('p');
|
|
@@ -420,9 +457,17 @@ function setRail(closed) {
|
|
|
420
457
|
$('rail-toggle').addEventListener('click', () => setRail(!$('rail').classList.contains('closed')));
|
|
421
458
|
if (matchMedia('(max-width: 47rem)').matches) setRail(true);
|
|
422
459
|
|
|
423
|
-
|
|
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', () => {
|
|
424
463
|
closePanels();
|
|
425
|
-
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');
|
|
426
471
|
});
|
|
427
472
|
|
|
428
473
|
let bskyBusy = false;
|
|
@@ -443,6 +488,20 @@ $('bsky-form').addEventListener('submit', async (ev) => {
|
|
|
443
488
|
}
|
|
444
489
|
} finally { bskyBusy = false; }
|
|
445
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
|
+
|
|
446
505
|
$('bsky-disconnect').addEventListener('click', async () => {
|
|
447
506
|
if (bskyBusy) return;
|
|
448
507
|
bskyBusy = true;
|
package/web/admin/index.html
CHANGED
|
@@ -133,6 +133,9 @@ dl { margin: .5rem 0; } /* grid only above 34rem — see the media query
|
|
|
133
133
|
gap: .6rem .9rem; align-items: baseline; }
|
|
134
134
|
}
|
|
135
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; }
|
|
136
139
|
dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
137
140
|
.rows { list-style: none; padding: 0; margin: .3rem 0; }
|
|
138
141
|
.rows li { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
|
|
@@ -216,16 +219,16 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
216
219
|
<label class="choice"><input type="radio" name="newKind" value="person" checked>
|
|
217
220
|
A <strong>person</strong> — an account you read and post from</label>
|
|
218
221
|
<label class="choice"><input type="radio" name="newKind" value="group">
|
|
219
|
-
A <strong>group</strong> of
|
|
222
|
+
A <strong>group</strong> of Fediverse users you manage</label>
|
|
220
223
|
</fieldset>
|
|
221
224
|
|
|
222
225
|
<fieldset>
|
|
223
226
|
<legend>Actor's Handle</legend>
|
|
224
227
|
<label for="new-handle">Handle</label>
|
|
225
228
|
<input type="text" id="new-handle" autocomplete="off"
|
|
226
|
-
placeholder="handle part of
|
|
229
|
+
placeholder="handle part of Fediverse address: @HANDLE@server"
|
|
227
230
|
aria-describedby="new-handle-hint">
|
|
228
|
-
<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.
|
|
229
232
|
Letters, digits, hyphens, and underscores only.</p>
|
|
230
233
|
</fieldset>
|
|
231
234
|
|
|
@@ -303,8 +306,8 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
303
306
|
<option value="auto">accepted automatically</option>
|
|
304
307
|
</select>
|
|
305
308
|
</span>
|
|
306
|
-
<span id="
|
|
307
|
-
<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>
|
|
308
311
|
</span>
|
|
309
312
|
<span id="bsky-ctl" hidden>
|
|
310
313
|
<select id="bsky-crosspost" aria-label="Bluesky crossposting" title=" Whether your public posts also go to the Bluesky account">
|
|
@@ -344,7 +347,7 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
344
347
|
on arrival. Shown only when something is actually waiting. -->
|
|
345
348
|
<div id="block-modqueue" hidden>
|
|
346
349
|
<h3>Moderation requests <span class="muted" id="modqueue-count"></span></h3>
|
|
347
|
-
<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
|
|
348
351
|
sent it, so nothing here has been acted on.</p>
|
|
349
352
|
<ul class="rows" role="list" id="modqueue"></ul>
|
|
350
353
|
</div>
|
|
@@ -423,20 +426,46 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
|
423
426
|
<button id="bsky-connect" type="submit" class="primary" title=" Sign in to the Bluesky account with the app password">Connect</button>
|
|
424
427
|
</p>
|
|
425
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>
|
|
426
455
|
<!-- Every consequence is written here rather than assembled in script: the
|
|
427
456
|
page asking "are you sure" has to say what it is sure about, and this is
|
|
428
457
|
the only record of it. Script picks which block to show, nothing else. -->
|
|
429
458
|
<!-- The gateway popups: attach proves the pod with this agent's own credential;
|
|
430
459
|
detach republishes the pod's own inbox. -->
|
|
431
460
|
<form id="gateway-attach-form" hidden>
|
|
432
|
-
<p class="hint">A gateway filters your
|
|
461
|
+
<p class="hint">A gateway filters your Fediverse mail before it reaches your pod: each
|
|
433
462
|
delivery is verified at its door, spam is dropped, and the rest lands in your pod as
|
|
434
463
|
before. Attaching proves the pod with this agent's own credential — no password
|
|
435
464
|
leaves this machine.</p>
|
|
436
465
|
<label for="gw-front">gateway</label>
|
|
437
466
|
<input type="url" id="gw-front" placeholder="https://fedipod.net" autocomplete="off">
|
|
438
467
|
<fieldset>
|
|
439
|
-
<legend>Your
|
|
468
|
+
<legend>Your Fediverse handle</legend>
|
|
440
469
|
<label class="choice"><input type="radio" name="gwShape" value="pod" checked>
|
|
441
470
|
Keep my pod-based handle: <strong id="gw-pod-preview"></strong><br>
|
|
442
471
|
<span class="hint">Your address stays tied to your own pod — you can drop the
|
|
@@ -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) {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>FediPod — accounts on this server</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: light dark; --line:#d9d4c7; --dim:#45443d; --accent:#6a8a5a; }
|
|
9
|
+
@media (prefers-color-scheme: dark) { :root { --dim:#c2c0b4; } }
|
|
10
|
+
body { font: 19px/1.6 system-ui, sans-serif; max-width: 44rem; margin: 2rem auto; padding: 0 1.1rem; }
|
|
11
|
+
h1 { font-size: 1.7rem; margin: 0 0 .2rem; }
|
|
12
|
+
p.lede { color: var(--dim); margin-top: 0; }
|
|
13
|
+
label { display: block; font-weight: 600; margin: .8rem 0 .2rem; }
|
|
14
|
+
input[type=url] { width: 100%; padding: .5rem .6rem; font: inherit;
|
|
15
|
+
border: 1px solid var(--line); border-radius: 8px; box-sizing: border-box; }
|
|
16
|
+
.hint { color: var(--dim); font-size: .95rem; }
|
|
17
|
+
.primary { background: var(--accent); color: #fff; border: none; border-radius: 9px;
|
|
18
|
+
padding: .6rem 1.1rem; font: inherit; cursor: pointer; }
|
|
19
|
+
.primary[disabled] { opacity: .5; cursor: not-allowed; }
|
|
20
|
+
button { font: inherit; cursor: pointer; }
|
|
21
|
+
[hidden] { display: none !important; }
|
|
22
|
+
table { border-collapse: collapse; width: 100%; margin: 1.2rem 0; }
|
|
23
|
+
th, td { text-align: left; padding: .5rem .7rem; border-bottom: 1px solid var(--line); }
|
|
24
|
+
th { font-weight: 600; }
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<h1>Accounts on this server</h1>
|
|
29
|
+
<p class="lede">Every name this server answers for: accounts whose identity lives here, and
|
|
30
|
+
accounts that only use it as their gateway. Sign in as the server's admin to see them.</p>
|
|
31
|
+
|
|
32
|
+
<form id="roster-form">
|
|
33
|
+
<p class="field">
|
|
34
|
+
<label for="roster-issuer">Your identity provider</label>
|
|
35
|
+
<input type="url" id="roster-issuer" autocomplete="off" value="https://solidcommunity.net">
|
|
36
|
+
</p>
|
|
37
|
+
<p class="actions">
|
|
38
|
+
<button type="submit" id="roster-signin" class="primary">Sign in & list accounts</button>
|
|
39
|
+
</p>
|
|
40
|
+
<p class="hint" id="roster-note" hidden></p>
|
|
41
|
+
</form>
|
|
42
|
+
|
|
43
|
+
<table id="roster-table" hidden>
|
|
44
|
+
<thead>
|
|
45
|
+
<tr><th>Account</th><th>Kind</th><th>Identity</th><th>Pod</th><th>Remove</th></tr>
|
|
46
|
+
</thead>
|
|
47
|
+
<tbody id="roster-rows"></tbody>
|
|
48
|
+
</table>
|
|
49
|
+
|
|
50
|
+
<script type="module">
|
|
51
|
+
const $ = (id) => document.getElementById(id);
|
|
52
|
+
const note = (text) => { const n = $('roster-note'); n.hidden = !text; n.textContent = text || ''; };
|
|
53
|
+
|
|
54
|
+
// The Solid-OIDC client for the sign-in round trip. Constructed once; the
|
|
55
|
+
// redirect state lives in this tab's sessionStorage, so the same session
|
|
56
|
+
// finishes the login on the way back.
|
|
57
|
+
let session = null;
|
|
58
|
+
try {
|
|
59
|
+
const { SessionCore } = await import('/solid-oidc-client.js');
|
|
60
|
+
session = new SessionCore({ redirect_uris: [location.origin + '/admin'], client_name: 'FediPod admin' });
|
|
61
|
+
} catch { /* leave null — the "did not load" note fires */ }
|
|
62
|
+
|
|
63
|
+
$('roster-issuer').addEventListener('input', () => {
|
|
64
|
+
$('roster-signin').disabled = !/^https?:\/\/\S+/.test($('roster-issuer').value.trim());
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
$('roster-form').addEventListener('submit', (ev) => {
|
|
68
|
+
ev.preventDefault();
|
|
69
|
+
if (!session) { note('the sign-in library did not load — reload and try again'); return; }
|
|
70
|
+
session.login($('roster-issuer').value.trim(), location.origin + '/admin')
|
|
71
|
+
.catch((e) => note('sign-in failed to start: ' + e.message));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Back from the identity provider: read the roster with the proven login.
|
|
75
|
+
(async () => {
|
|
76
|
+
if (!session) return;
|
|
77
|
+
await session.handleRedirectFromLogin().catch(() => {});
|
|
78
|
+
if (!session.isActive) return;
|
|
79
|
+
const webId = session.webId;
|
|
80
|
+
|
|
81
|
+
// Signed fetches build a DPoP proof from the URL, so it must be absolute.
|
|
82
|
+
const load = async () => {
|
|
83
|
+
let res;
|
|
84
|
+
try { res = await session.authFetch(location.origin + '/api/roster'); }
|
|
85
|
+
catch (e) { note('the roster request failed: ' + e.message); return; }
|
|
86
|
+
const d = await res.json().catch(() => ({}));
|
|
87
|
+
if (res.status === 403) { note('signed in as ' + webId + ', which is not this server’s admin'); return; }
|
|
88
|
+
if (res.status === 501) { note('this server names no admin — set FEDIPOD_ADMIN_WEBID and redeploy'); return; }
|
|
89
|
+
if (res.status !== 200) { note('roster unavailable: ' + (d.error || 'HTTP ' + res.status)); return; }
|
|
90
|
+
const rows = $('roster-rows');
|
|
91
|
+
rows.textContent = '';
|
|
92
|
+
for (const a of d.accounts || []) {
|
|
93
|
+
const tr = document.createElement('tr');
|
|
94
|
+
const cell = (child) => { const td = document.createElement('td'); td.append(child); tr.append(td); };
|
|
95
|
+
cell(a.address);
|
|
96
|
+
cell(a.kind);
|
|
97
|
+
cell(a.fronted ? 'lives here' : 'gateway only');
|
|
98
|
+
const pod = document.createElement('a');
|
|
99
|
+
pod.href = a.podHome; pod.textContent = new URL(a.podHome).host;
|
|
100
|
+
cell(pod);
|
|
101
|
+
const rm = document.createElement('button');
|
|
102
|
+
rm.type = 'button'; rm.textContent = 'Remove';
|
|
103
|
+
rm.onclick = () => revoke(a);
|
|
104
|
+
cell(rm);
|
|
105
|
+
rows.append(tr);
|
|
106
|
+
}
|
|
107
|
+
$('roster-table').hidden = false;
|
|
108
|
+
note((d.accounts || []).length + ' account(s) on ' + d.host);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const revoke = async (a) => {
|
|
112
|
+
if (!confirm('Remove ' + a.address + ' from this server? The name stops resolving here; nothing on their pod is touched.')) return;
|
|
113
|
+
let res;
|
|
114
|
+
try {
|
|
115
|
+
res = await session.authFetch(location.origin + '/api/revoke', {
|
|
116
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
117
|
+
body: JSON.stringify({ handle: a.handle }),
|
|
118
|
+
});
|
|
119
|
+
} catch (e) { note('the remove request failed: ' + e.message); return; }
|
|
120
|
+
const d = await res.json().catch(() => ({}));
|
|
121
|
+
if (res.status !== 200) { note('remove refused: ' + (d.error || 'HTTP ' + res.status)); return; }
|
|
122
|
+
if (!d.removed) { note(a.address + ' stays: ' + d.reason); return; }
|
|
123
|
+
await load();
|
|
124
|
+
note('removed ' + a.address);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
note('signed in as ' + webId + ' — reading the roster…');
|
|
128
|
+
await load();
|
|
129
|
+
})();
|
|
130
|
+
</script>
|
|
131
|
+
<i>(cc) 4.0 By, Jeff Zucker, 2026</i>
|
|
132
|
+
</body>
|
|
133
|
+
</html>
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
</style>
|
|
20
20
|
</head>
|
|
21
21
|
<body>
|
|
22
|
-
<h1>FediPod - join the
|
|
23
|
-
<p class="lede">Welcome to <b>FediPod</b>! Here, you can get a free pod and
|
|
22
|
+
<h1>FediPod - join the Fediverse from a Solid pod.</h1>
|
|
23
|
+
<p class="lede">Welcome to <b>FediPod</b>! Here, you can get a free pod and Fediverse account or link to your existing pod/account.</p>
|
|
24
24
|
|
|
25
25
|
<p class="hint" id="current-version" hidden>Current FediPod: <strong id="current-version-num"></strong><br>
|
|
26
26
|
— install or update an existing install with <code id="current-version-cmd"></code></p>
|
package/web/front/run.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
|
27
27
|
<h1>Run your identity on this server</h1>
|
|
28
|
-
<p class="lede">If your pod is hosted by this server, the server can run your
|
|
28
|
+
<p class="lede">If your pod is hosted by this server, the server can run your Fediverse identity
|
|
29
29
|
for you: it accepts follows, delivers your posts and serves your Mastodon app, with nothing for
|
|
30
30
|
you to keep running. Sign in with your pod to prove it is yours.</p>
|
|
31
31
|
|
|
@@ -45,10 +45,18 @@ you to keep running. Sign in with your pod to prove it is yours.</p>
|
|
|
45
45
|
<p class="hint" id="run-note" hidden></p>
|
|
46
46
|
</form>
|
|
47
47
|
|
|
48
|
-
<script
|
|
49
|
-
<script>
|
|
48
|
+
<script type="module">
|
|
50
49
|
const $ = (id) => document.getElementById(id);
|
|
51
50
|
|
|
51
|
+
// The Solid-OIDC client for the sign-in round trip. Constructed once; the
|
|
52
|
+
// redirect state (PKCE, csrf) lives in this tab's sessionStorage, so the same
|
|
53
|
+
// session finishes the login on the way back.
|
|
54
|
+
let session = null;
|
|
55
|
+
try {
|
|
56
|
+
const { SessionCore } = await import('/solid-oidc-client.js');
|
|
57
|
+
session = new SessionCore({ redirect_uris: [location.origin + '/run'], client_name: 'FediPod gateway' });
|
|
58
|
+
} catch { /* leave null — the "did not load" notes below fire */ }
|
|
59
|
+
|
|
52
60
|
// Offered only where this server actually runs identities. An
|
|
53
61
|
// unauthenticated probe tells the two apart: 501 = not offered here,
|
|
54
62
|
// anything else = offered (a real opt-in still needs the signed-in proof).
|
|
@@ -85,36 +93,32 @@ you to keep running. Sign in with your pod to prove it is yours.</p>
|
|
|
85
93
|
$('run-issuer').addEventListener('input', runFormCheck);
|
|
86
94
|
|
|
87
95
|
const runStart = (action) => {
|
|
88
|
-
const auth = window.solidClientAuthentication;
|
|
89
96
|
const n = $('run-note');
|
|
90
|
-
if (!
|
|
97
|
+
if (!session) { n.hidden = false; n.textContent = 'the sign-in library did not load — reload and try again'; return; }
|
|
91
98
|
const u = new URL($('run-pod-url').value.trim());
|
|
92
99
|
if (u.pathname === '') u.pathname = '/';
|
|
93
100
|
if (!u.pathname.endsWith('/')) u.pathname += '/';
|
|
94
101
|
u.search = ''; u.hash = '';
|
|
95
102
|
sessionStorage.setItem('fp-run', JSON.stringify({ podBase: u.href, action }));
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
redirectUrl: location.origin + '/run',
|
|
99
|
-
clientName: 'FediPod gateway',
|
|
100
|
-
}).catch((e) => { n.hidden = false; n.textContent = 'sign-in failed to start: ' + e.message; });
|
|
103
|
+
session.login($('run-issuer').value.trim(), location.origin + '/run')
|
|
104
|
+
.catch((e) => { n.hidden = false; n.textContent = 'sign-in failed to start: ' + e.message; });
|
|
101
105
|
};
|
|
102
106
|
$('run-continue').onclick = () => runStart('opt-in');
|
|
103
107
|
$('run-leave').onclick = () => runStart('opt-out');
|
|
104
108
|
|
|
105
109
|
// Back from the identity provider: finish the opt-in with the proven login.
|
|
106
110
|
(async () => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const info = await auth.handleIncomingRedirect().catch(() => null);
|
|
111
|
+
if (!session) return;
|
|
112
|
+
await session.handleRedirectFromLogin().catch(() => {});
|
|
110
113
|
const pending = sessionStorage.getItem('fp-run');
|
|
111
|
-
if (!
|
|
114
|
+
if (!session.isActive || !pending) return;
|
|
112
115
|
sessionStorage.removeItem('fp-run');
|
|
113
116
|
const p = JSON.parse(pending);
|
|
114
117
|
const n = $('run-note');
|
|
115
118
|
n.hidden = false;
|
|
116
|
-
n.textContent = 'signed in as ' +
|
|
117
|
-
|
|
119
|
+
n.textContent = 'signed in as ' + session.webId + ' — asking the server…';
|
|
120
|
+
// The signed fetch builds a DPoP proof from the URL, so it must be absolute.
|
|
121
|
+
const res = await session.authFetch(location.origin + '/api/agent', {
|
|
118
122
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
119
123
|
body: JSON.stringify({ action: p.action, podBase: p.podBase }),
|
|
120
124
|
}).catch(() => null);
|