fedipod 1.26.1 → 1.27.3

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.
@@ -19,8 +19,8 @@ main { flex: 1 1 auto; min-height: 0; display: flex; }
19
19
  </style>
20
20
  <link rel="stylesheet" href="../bar.css">
21
21
  <style>#actor-pick{display:none!important}</style>
22
- <meta name="fedipod-version" content="1.26.1">
23
- <meta name="fedipod-build" content="a59138536e01">
22
+ <meta name="fedipod-version" content="1.27.3">
23
+ <meta name="fedipod-build" content="940565ccb8f5">
24
24
  <script src="/update.js" defer></script>
25
25
  <script src="/admin/client-pick.js"></script>
26
26
  </head>
@@ -19,8 +19,8 @@ main { flex: 1 1 auto; min-height: 0; display: flex; }
19
19
  </style>
20
20
  <link rel="stylesheet" href="../bar.css">
21
21
  <style>#actor-pick{display:none!important}</style>
22
- <meta name="fedipod-version" content="1.26.1">
23
- <meta name="fedipod-build" content="a59138536e01">
22
+ <meta name="fedipod-version" content="1.27.3">
23
+ <meta name="fedipod-build" content="940565ccb8f5">
24
24
  <script src="/update.js" defer></script>
25
25
  <script src="/admin/client-pick.js"></script>
26
26
  </head>
@@ -168,8 +168,8 @@ pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
168
168
  <link rel="stylesheet" href="bar.css?v=3">
169
169
  <link rel="stylesheet" href="window.css">
170
170
  <style>#gw-shape-front,#do-drain,#do-log,#actor-pick{display:none!important}</style>
171
- <meta name="fedipod-version" content="1.26.1">
172
- <meta name="fedipod-build" content="a59138536e01">
171
+ <meta name="fedipod-version" content="1.27.3">
172
+ <meta name="fedipod-build" content="940565ccb8f5">
173
173
  <script src="/update.js" defer></script>
174
174
  <script src="/admin/client-pick.js"></script>
175
175
  </head>
@@ -0,0 +1,37 @@
1
+ // oauth-signin.mjs — a client asked this account to sign its owner in. The
2
+ // owner signs in at their own pod, with the fediverse-account library, and the
3
+ // page proves that sign-in to the account with one signed request; the account
4
+ // then hands the client its code. No password of ours anywhere.
5
+ import { fediLogin } from './fedi-login.mjs';
6
+
7
+ const $ = (id) => document.getElementById(id);
8
+ const say = (text, error = false) => { const s = $('signin-status'); s.textContent = text; s.setAttribute('role', error ? 'alert' : 'status'); };
9
+ const webId = $('webid').href;
10
+ const here = location.origin + location.pathname;
11
+ const login = fediLogin({ dbName: 'fedipod-signin', clientName: 'FediPod sign-in', redirectUri: here });
12
+
13
+ // Back from the pod: this finishes the sign-in and returns to the client's
14
+ // request, which carries the client's own parameters.
15
+ try { await login.resume(); } catch (e) { say(e.message, true); }
16
+
17
+ async function prove() {
18
+ const s = await login.getSession();
19
+ if (!s) return false;
20
+ if (s.webId !== webId) { say(`You signed in as ${s.webId}, but this account belongs to ${webId}. Sign out there and sign in as the owner.`, true); return true; }
21
+ say('Signing you in…');
22
+ const res = await s.fetch(here, {
23
+ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' },
24
+ body: location.search.replace(/^\?/u, ''),
25
+ });
26
+ const d = await res.json().catch(() => ({}));
27
+ if (res.ok && d.redirect) { location.href = d.redirect; return true; }
28
+ if (res.ok && d.code) { say(`Your authorization code: ${d.code}`); return true; }
29
+ say(d.error || `The account refused the sign-in (HTTP ${res.status}).`, true);
30
+ return true;
31
+ }
32
+
33
+ if (location.search.includes('client_id=') && !(await prove())) $('signin').hidden = false;
34
+ $('signin').addEventListener('click', async () => {
35
+ say('Taking you to your pod…');
36
+ try { await login.login(webId, { returnTo: location.href, redirectUri: here }); } catch (e) { say(e.message, true); }
37
+ });
@@ -54,8 +54,8 @@ button:disabled { opacity: .5; cursor: default; }
54
54
  </style>
55
55
  <link rel="stylesheet" href="../bar.css">
56
56
  <style>#actor-pick{display:none!important}</style>
57
- <meta name="fedipod-version" content="1.26.1">
58
- <meta name="fedipod-build" content="a59138536e01">
57
+ <meta name="fedipod-version" content="1.27.3">
58
+ <meta name="fedipod-build" content="940565ccb8f5">
59
59
  <script src="/update.js" defer></script>
60
60
  </head>
61
61
  <body>
@@ -12,8 +12,8 @@
12
12
  <link rel="modulepreload" crossorigin href="../assets/chunk-aKtaBQYM.js">
13
13
  <link rel="modulepreload" crossorigin href="../assets/pwa-viewport-ogKbF51K.js">
14
14
 
15
- <link rel="manifest" href="./manifest.webmanifest"><meta name="fedipod-version" content="1.26.1">
16
- <meta name="fedipod-build" content="a59138536e01">
15
+ <link rel="manifest" href="./manifest.webmanifest"><meta name="fedipod-version" content="1.27.3">
16
+ <meta name="fedipod-build" content="940565ccb8f5">
17
17
  <script src="/update.js" defer></script>
18
18
  </head>
19
19
  <body>
@@ -65,8 +65,8 @@
65
65
  <link rel="modulepreload" crossorigin href="./assets/ICONS-DIPJeU0C.js">
66
66
  <link rel="modulepreload" crossorigin href="./assets/ScheduledAtField-D-Abfo4J.js">
67
67
 
68
- <link rel="manifest" href="./manifest.webmanifest"><meta name="fedipod-version" content="1.26.1">
69
- <meta name="fedipod-build" content="a59138536e01">
68
+ <link rel="manifest" href="./manifest.webmanifest"><meta name="fedipod-version" content="1.27.3">
69
+ <meta name="fedipod-build" content="940565ccb8f5">
70
70
  <script src="/update.js" defer></script>
71
71
  </head>
72
72
  <body>
@@ -35,8 +35,8 @@
35
35
  .hint { color: var(--sub); }
36
36
  </style>
37
37
  <script src="/authorize_interaction.js" defer></script>
38
- <meta name="fedipod-version" content="1.26.1">
39
- <meta name="fedipod-build" content="a59138536e01">
38
+ <meta name="fedipod-version" content="1.27.3">
39
+ <meta name="fedipod-build" content="940565ccb8f5">
40
40
  <script src="/update.js" defer></script>
41
41
  </head>
42
42
  <body>
@@ -211,8 +211,8 @@
211
211
  /* At the foot of the page, centred, clear of whatever is above it. */
212
212
  footer.foot { margin: 1rem auto 0; color: var(--sub); font-size: 16px; text-align: center; }
213
213
  </style>
214
- <meta name="fedipod-version" content="1.26.1">
215
- <meta name="fedipod-build" content="a59138536e01">
214
+ <meta name="fedipod-version" content="1.27.3">
215
+ <meta name="fedipod-build" content="940565ccb8f5">
216
216
  <script src="/update.js" defer></script>
217
217
  </head>
218
218
  <body>
@@ -1 +1 @@
1
- {"build":"a59138536e01","version":"1.26.1","at":"2026-09-20T18:26:18.671Z"}
1
+ {"build":"940565ccb8f5","version":"1.27.3","at":"2026-09-22T03:52:57.316Z"}
@@ -72,8 +72,8 @@ button:disabled { opacity: .55; cursor: default; }
72
72
  a { color: var(--accent); }
73
73
  [hidden] { display: none !important; }
74
74
  </style>
75
- <meta name="fedipod-version" content="1.26.1">
76
- <meta name="fedipod-build" content="a59138536e01">
75
+ <meta name="fedipod-version" content="1.27.3">
76
+ <meta name="fedipod-build" content="940565ccb8f5">
77
77
  <script src="/update.js" defer></script>
78
78
  </head>
79
79
  <body>
@@ -45643,7 +45643,8 @@ var ALL_COLLECTIONS = {
45643
45643
  outbox: true,
45644
45644
  acls: true,
45645
45645
  pending: true,
45646
- blocked: true
45646
+ blocked: true,
45647
+ featured: true
45647
45648
  };
45648
45649
  async function publishOutbox(publisher, outbox, { acls = false, force = false } = {}) {
45649
45650
  const { urls } = publisher;
@@ -45742,6 +45743,7 @@ async function publishCollections(publisher, which = ALL_COLLECTIONS) {
45742
45743
  }
45743
45744
  if (which.pending) await publisher.publishPending();
45744
45745
  if (which.blocked) await publisher.publishBlocked();
45746
+ if (which.featured) await publisher.publishFeatured();
45745
45747
  if (which.outbox) {
45746
45748
  const outbox = publisher.store.read("outbox.json", []);
45747
45749
  const known2 = publisher.store.read("published.json", {}).outboxIndex;
@@ -57345,6 +57347,7 @@ var Publisher = class {
57345
57347
  ["notes", urls.notes],
57346
57348
  ["followers", urls.followers],
57347
57349
  ["following", urls.following],
57350
+ ["featured", urls.featured],
57348
57351
  ["outbox", urls.outbox]
57349
57352
  ];
57350
57353
  const unreachable = [];
@@ -65852,6 +65855,39 @@ function checkPassword(rec, password) {
65852
65855
  }
65853
65856
  var escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
65854
65857
  var parseRedirects = (v) => (Array.isArray(v) ? v : String(v || "").split(/\s+/)).map((s) => s.trim()).filter(Boolean);
65858
+ function sendPodSigninPage(res, client, webId, mount = "") {
65859
+ let asking = "";
65860
+ if (client && (client.name || client.redirect)) {
65861
+ let where = "";
65862
+ try {
65863
+ where = client.redirect ? new URL(client.redirect).host : "";
65864
+ } catch {
65865
+ }
65866
+ const who = client.name ? escapeHtml(client.name) : where ? escapeHtml(where) : "A client";
65867
+ asking = `<p><strong>${who}</strong> is asking to use your account${where ? `, and will be sent back to <code>${escapeHtml(where)}</code>` : ""}.</p>`;
65868
+ }
65869
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" });
65870
+ res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
65871
+ <title>Sign in with your pod</title>
65872
+ <style>:root{color-scheme:light dark;--heading:#1a4f8a;--btn:#3d6b35;--line:#9a9a9a}
65873
+ body{font:20px Arial,Helvetica,sans-serif;max-width:28rem;margin:12vh auto;padding:0 1rem}
65874
+ h1{color:var(--heading);font-size:1.5rem}
65875
+ code{word-break:break-all;background:rgba(120,120,120,.12);padding:.05em .35em;border-radius:4px}
65876
+ button{font:inherit;padding:.6rem 1.1rem;background:var(--btn);color:#fff;border:none;border-radius:9px;cursor:pointer}
65877
+ button[hidden]{display:none}
65878
+ #signin-status{min-height:1.5em;padding:.6rem .8rem;border:1px solid var(--line);border-radius:8px;margin-top:1rem}
65879
+ @media (prefers-color-scheme:dark){:root{--heading:#7fb3e8;--btn:#5a8a4f}}</style></head><body>
65880
+ <main>
65881
+ <h1>Sign in with your pod</h1>
65882
+ ${asking}
65883
+ <p>This account belongs to <a id="webid" href="${escapeHtml(webId)}">${escapeHtml(webId)}</a>. Sign in at that pod to allow it.</p>
65884
+ <p><button type="button" id="signin" hidden>Sign in with your pod</button></p>
65885
+ <p id="signin-status" role="status" aria-live="polite"></p>
65886
+ </main>
65887
+ <script type="module" src="${escapeHtml(mount)}/oauth/session/signin.mjs"><\/script>
65888
+ </body></html>`);
65889
+ return true;
65890
+ }
65855
65891
  function sendLoginForm(res, params, error2 = "", client = null, status2 = null, headers = {}) {
65856
65892
  const hidden = [...params.entries()].filter(([k]) => k !== "password").map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`).join("\n");
65857
65893
  let asking = "<p>Enter the agent password to authorize this client.</p>";
@@ -66120,6 +66156,16 @@ async function handle(api, ctx) {
66120
66156
  params = new URLSearchParams(body);
66121
66157
  }
66122
66158
  const redirect = params.get("redirect_uri") || "";
66159
+ let proved = false;
66160
+ if (req.method === "POST" && req.headers.authorization && req.headers.dpop && api.podAuth) {
66161
+ const proof = await api.podAuth(req, pathname);
66162
+ if (!proof.ok) return send(proof.status || 401, { error: proof.error });
66163
+ proved = true;
66164
+ }
66165
+ const ownerWebId = api.ownerWebId?.() || null;
66166
+ if (req.method === "GET" && !params.get("client_id") && params.get("code") && params.get("state") && ownerWebId) {
66167
+ return sendPodSigninPage(res, null, ownerWebId, api.mount || "");
66168
+ }
66123
66169
  const app = api.findApp(params.get("client_id") || "");
66124
66170
  const doc = app ? null : await api.resolveClientDocument(params.get("client_id") || "");
66125
66171
  const external = !!app || !!doc;
@@ -66127,13 +66173,14 @@ async function handle(api, ctx) {
66127
66173
  api.log(`authorize refused: cross-site navigation to the mint from ${req.headers.referer || "nowhere"}`);
66128
66174
  return send(403, { error: "a cross-site navigation may not authorize a client" });
66129
66175
  }
66130
- if (external && !api.store.getConfig()?.uiPassword && !api.redirectAllowed(redirect)) {
66176
+ const client = { name: app?.name || doc?.name || null, redirect, scope: params.get("scope") || "read" };
66177
+ if (external && !proved && !api.store.getConfig()?.uiPassword && !api.redirectAllowed(redirect)) {
66178
+ if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "");
66131
66179
  api.log(`authorize refused: no UI password, and "${redirect}" is not an address of this agent`);
66132
66180
  return send(403, {
66133
66181
  error: "this client asks to be sent somewhere other than this agent, and no password is set to approve that with. Run `fedipod passwd` and try again."
66134
66182
  });
66135
66183
  }
66136
- const client = { name: app?.name || doc?.name || null, redirect, scope: params.get("scope") || "read" };
66137
66184
  if (app) {
66138
66185
  if (!app.redirectUris.includes(redirect)) {
66139
66186
  api.log(`authorize refused: redirect_uri "${redirect}" not registered for ${app.clientId}`);
@@ -66152,7 +66199,7 @@ async function handle(api, ctx) {
66152
66199
  api.log(`authorize refused: redirect_uri "${redirect}" is not this agent`);
66153
66200
  return send(400, { error: "redirect_uri must be an address of this agent" });
66154
66201
  }
66155
- if (req.method === "POST") {
66202
+ if (req.method === "POST" && !proved) {
66156
66203
  if (api.rateLimited()) {
66157
66204
  api.log("authorize rate limited");
66158
66205
  return sendLoginForm(
@@ -66167,12 +66214,13 @@ async function handle(api, ctx) {
66167
66214
  if (!pw || !checkPassword(pw, body.password || "")) {
66168
66215
  return sendLoginForm(res, params, "wrong password \u2014 try again", client);
66169
66216
  }
66170
- } else if (pw) {
66217
+ } else if (pw && !proved) {
66171
66218
  return sendLoginForm(res, params, "", client);
66172
- } else if (api.allowed && !api.allowed.isLocalRequest(req)) {
66219
+ } else if (!proved && api.allowed && !api.allowed.isLocalRequest(req) && !api.throughDoor?.(req)) {
66220
+ if (req.method === "GET" && ownerWebId) return sendPodSigninPage(res, client, ownerWebId, api.mount || "");
66173
66221
  api.log(`authorize refused: no UI password, and "${req.headers.host}" is not this machine`);
66174
66222
  return send(403, {
66175
- error: "this agent answers on an address outside this machine and has no password set \u2014 " + (api.embedded ? `POST {"password":"\u2026"} to the owner door's /config with its door secret before logging in` : "run `fedipod passwd` before logging in over that address")
66223
+ error: api.embedded ? "Open your account's management page first, then sign in from there." : "Signing in from another machine needs a password. On the machine that runs your account, run: fedipod passwd"
66176
66224
  });
66177
66225
  }
66178
66226
  const code = external ? api.mintCode({
@@ -66186,6 +66234,7 @@ async function handle(api, ctx) {
66186
66234
  const target = new URL(redirect);
66187
66235
  target.searchParams.set("code", code);
66188
66236
  if (params.get("state")) target.searchParams.set("state", params.get("state"));
66237
+ if (proved) return send(200, { redirect: target.href });
66189
66238
  res.writeHead(302, { location: target.href });
66190
66239
  res.end();
66191
66240
  return true;
@@ -68091,6 +68140,7 @@ var MastoApi = class _MastoApi {
68091
68140
  allowed = null,
68092
68141
  scheme = null,
68093
68142
  embedded = false,
68143
+ throughDoor = null,
68094
68144
  mount = "",
68095
68145
  streaming = true,
68096
68146
  webPush = true,
@@ -68101,6 +68151,7 @@ var MastoApi = class _MastoApi {
68101
68151
  this.embedded = embedded;
68102
68152
  this.log = log2;
68103
68153
  this.allowed = allowed;
68154
+ this.throughDoor = throughDoor;
68104
68155
  this.scheme = scheme;
68105
68156
  this.streaming = streaming;
68106
68157
  this.webPush = webPush;
@@ -49,6 +49,5 @@ accounts that only use it as their gateway. Sign in as the server's admin to see
49
49
  </table>
50
50
 
51
51
  <script type="module" src="/admin.js"></script>
52
- <i>(cc) 4.0 By, Jeff Zucker, 2026</i>
53
52
  </body>
54
53
  </html>
@@ -31,6 +31,5 @@ FediPod is experimental and free to use. See the <a href="https://github.com/je
31
31
  </p>
32
32
 
33
33
  <script src="/new-account.js"></script>
34
- <i style="text-align:right">(cc) 4.0 By, Jeff Zucker, 2026</i>
35
34
  </body>
36
35
  </html>
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>FediPod — run your identity on this server</title>
6
+ <title>FediPod — turn your pod into a Fediverse account</title>
7
7
  <link rel="icon" href="/icons/gateway.svg" type="image/svg+xml">
8
8
  <style>
9
9
  :root { color-scheme: light dark; --line:#d9d4c7; --dim:#45443d; --accent:#6a8a5a; }
@@ -15,20 +15,27 @@
15
15
  input[type=text], input[type=url] { width: 100%; padding: .5rem .6rem; font: inherit;
16
16
  border: 1px solid var(--line); border-radius: 8px; box-sizing: border-box; }
17
17
  .hint { color: var(--dim); font-size: .95rem; }
18
+ .pod { border: 1px solid var(--line); border-radius: 9px; padding: .9rem 1.1rem; margin: .8rem 0; }
19
+ .pod p { margin: .3rem 0; }
20
+ .pod .actions { margin-top: .8rem; }
18
21
  .primary { background: var(--accent); color: #fff; border: none; border-radius: 9px;
19
22
  padding: .6rem 1.1rem; font: inherit; cursor: pointer; }
20
23
  button { font: inherit; }
21
24
  .primary[disabled], button[disabled] { opacity: .5; cursor: not-allowed; }
22
25
  [hidden] { display: none !important; }
23
26
  code { background: rgba(120,120,120,.12); padding: .05em .35em; border-radius: 4px; }
27
+ .pod code { background: #fff3c4; color: #1a1a1a; font-size: 16px; }
28
+ @media (prefers-color-scheme: dark) { .pod code { background: #5a4a00; color: #fff; } }
24
29
  pre { background: rgba(120,120,120,.12); padding: .7rem .9rem; border-radius: 8px; overflow-x: auto; }
25
30
  </style>
26
31
  </head>
27
32
  <body>
28
- <h1>Run your identity on this server</h1>
29
- <p class="lede">If your pod is hosted by this server, the server can run your Fediverse identity
30
- for you: it accepts follows, delivers your posts and serves your Mastodon app, with nothing for
31
- you to keep running. Sign in with your pod to prove it is yours.</p>
33
+ <h1>Turn your pod into a Fediverse account</h1>
34
+ <p class="lede">Users with pods on this server may opt-in below to create a Fediverse account
35
+ that stores your data on your pod. You'll be able to interact with Mastodon and users of other
36
+ Fediverse services straight from your pod.</p>
37
+
38
+ <section id="run-known" hidden aria-live="polite"></section>
32
39
 
33
40
  <form id="run-form">
34
41
  <p class="field">
@@ -43,10 +50,9 @@ you to keep running. Sign in with your pod to prove it is yours.</p>
43
50
  <button type="button" id="run-continue" class="primary" disabled>Sign in with your pod &amp; opt in</button>
44
51
  <button type="button" id="run-leave">Stop running it here</button>
45
52
  </p>
46
- <p class="hint" id="run-note" hidden></p>
47
53
  </form>
54
+ <p class="hint" id="run-note" hidden></p>
48
55
 
49
56
  <script type="module" src="/run.js"></script>
50
- <i>(cc) 4.0 By, Jeff Zucker, 2026</i>
51
57
  </body>
52
58
  </html>
package/web/front/run.js CHANGED
@@ -23,22 +23,80 @@ try {
23
23
  session = new SessionCore({ redirect_uris: [HERE], client_name: 'FediPod gateway' });
24
24
  } catch { /* leave null — the "did not load" notes below fire */ }
25
25
 
26
- // Offered only where this server actually runs identities. An
27
- // unauthenticated probe tells the two apart: 501 = not offered here,
28
- // anything else = offered (a real opt-in still needs the signed-in proof).
26
+ // What this server knows before anything is typed. 501: it runs no
27
+ // identities. A pod server that issued the reader's session names their pod
28
+ // and its address, and the page shows one button per pod. Not signed in
29
+ // there: one button, to its login. A Gateway knows nothing: the form below.
29
30
  let offered = true;
31
+ const say = (tag, text) => { const e = document.createElement(tag); e.textContent = text; return e; };
30
32
  (async () => {
31
- const res = await fetch('/api/agent', { method: 'POST',
32
- headers: { 'content-type': 'application/json' }, body: '{}' }).catch(() => null);
33
+ const res = await fetch('/api/agent', { credentials: 'same-origin', headers: { accept: 'application/json' } }).catch(() => null);
34
+ const d = res && res.ok ? await res.json().catch(() => ({})) : {};
35
+ const n = $('run-note');
33
36
  if (!res || res.status === 501) {
34
37
  offered = false;
35
- const n = $('run-note');
36
38
  n.hidden = false;
37
39
  n.textContent = 'This server does not run identities. Your agent stays where it is.';
40
+ } else if (d.session === 'none') {
41
+ $('run-form').hidden = true;
42
+ showSignIn(d.loginUrl);
43
+ } else if (d.session === 'ok' && Array.isArray(d.pods) && d.pods.length) {
44
+ $('run-form').hidden = true;
45
+ showPods(d.pods);
38
46
  }
39
47
  runFormCheck();
40
48
  })();
41
49
 
50
+ async function refreshKnown() {
51
+ const res = await fetch('/api/agent', { credentials: 'same-origin', headers: { accept: 'application/json' } }).catch(() => null);
52
+ const d = res && res.ok ? await res.json().catch(() => ({})) : {};
53
+ if (d.session === 'ok' && Array.isArray(d.pods) && d.pods.length) showPods(d.pods);
54
+ }
55
+
56
+ function showSignIn(loginUrl) {
57
+ const k = $('run-known');
58
+ k.hidden = false;
59
+ k.replaceChildren();
60
+ k.append(say('p', 'You are not signed in at this server.'));
61
+ if (loginUrl) {
62
+ const go = say('button', 'Sign in');
63
+ go.type = 'button'; go.className = 'primary';
64
+ go.onclick = () => { location.href = loginUrl; };
65
+ const p = document.createElement('p'); p.className = 'actions'; p.append(go);
66
+ k.append(p, say('p', 'Sign in, then come back to this page.'));
67
+ k.lastChild.className = 'hint';
68
+ }
69
+ }
70
+
71
+ function showPods(pods) {
72
+ const k = $('run-known');
73
+ k.hidden = false;
74
+ k.replaceChildren();
75
+ for (const pod of pods) {
76
+ const box = document.createElement('div'); box.className = 'pod';
77
+ // The names stand out from the sentences around them.
78
+ const line = (before, id, after = '.') => { const p = say('p', before); p.append(say('code', id), after); return p; };
79
+ box.append(line('Solid Identity (WebID): ', pod.webId, ''));
80
+ const actions = document.createElement('p'); actions.className = 'actions';
81
+ box.append(line('Fediverse Identity: ', pod.address, ''));
82
+ if (pod.running) {
83
+ const links = say('p', 'Manage your account: ');
84
+ const m = say('a', pod.manage.split('?')[0]); m.href = pod.manage;
85
+ links.append(m);
86
+ box.append(links);
87
+ const stop = say('button', 'Close this Fediverse account'); stop.type = 'button'; stop.className = 'primary';
88
+ stop.onclick = () => runStart('opt-out', pod.podBase);
89
+ actions.append(stop);
90
+ } else {
91
+ const go = say('button', 'Create a Fediverse account'); go.type = 'button'; go.className = 'primary';
92
+ go.onclick = () => runStart('opt-in', pod.podBase);
93
+ actions.append(go);
94
+ }
95
+ box.append(actions);
96
+ k.append(box);
97
+ }
98
+ }
99
+
42
100
  function runFormCheck() {
43
101
  let ok = false;
44
102
  try { ok = /^https?:$/.test(new URL($('run-pod-url').value.trim()).protocol); } catch { /* not yet */ }
@@ -63,9 +121,9 @@ $('run-issuer').addEventListener('input', runFormCheck);
63
121
  // a filled-in form, with nothing to say why.
64
122
  runFormCheck();
65
123
 
66
- const runStart = async (action) => {
124
+ const runStart = async (action, podBase = null) => {
67
125
  const n = $('run-note');
68
- const u = new URL($('run-pod-url').value.trim());
126
+ const u = new URL(podBase || $('run-pod-url').value.trim());
69
127
  if (u.pathname === '') u.pathname = '/';
70
128
  if (!u.pathname.endsWith('/')) u.pathname += '/';
71
129
  u.search = ''; u.hash = '';
@@ -103,16 +161,14 @@ async function showReply(res, p) {
103
161
  // back from the server and one comes from sessionStorage, and pasting any
104
162
  // of them into innerHTML means whatever markup they contain is rendered.
105
163
  // Only the fixed wording is markup here; everything variable is text.
106
- const say = (tag, text) => { const e = document.createElement(tag); e.textContent = text; return e; };
107
- n.textContent = 'Your identity runs here now. This is your door secret — ';
108
- n.append(say('b', 'save it now'));
109
- n.append(', it is shown only this once. It opens your admin pages at ');
110
- n.append(say('code', p.podBase.replace(/\/$/, '') + d.doorPath));
111
- n.append(':', say('pre', d.doorSecret));
112
- n.append('To check it works:', say('pre', d.command));
113
- n.append('Lost it? Opt in again — that mints a fresh one and retires this one.');
164
+ const manage = `${p.podBase.replace(/\/$/, '')}${d.doorPath}?dk-token=${encodeURIComponent(d.doorSecret)}`;
165
+ n.textContent = 'Your Fediverse account is ready. Manage it at ';
166
+ const a = say('a', p.podBase.replace(/\/$/, '') + d.doorPath); a.href = manage;
167
+ n.append(a, '.');
168
+ refreshKnown();
114
169
  } else if (res && res.status === 200) {
115
- n.textContent = 'Done — this server no longer runs that identity. Your pod and its data are untouched.';
170
+ n.textContent = 'Your Fediverse account is closed. Your pod and its data are untouched.';
171
+ refreshKnown();
116
172
  } else {
117
173
  n.textContent = (p.action === 'opt-in' ? 'opt-in' : 'opt-out') + ' failed: '
118
174
  + (d.error || (res ? 'HTTP ' + res.status : 'no response'));