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.
- package/lib/client/masto/index.mjs +4 -1
- package/lib/client/masto/oauth.mjs +66 -9
- package/lib/core/publisher/collections.mjs +6 -1
- package/lib/core/publisher/index.mjs +1 -0
- package/lib/device/admin/surface.mjs +24 -1
- package/lib/device/certs.mjs +18 -2
- package/lib/gateway/front-core.mjs +41 -6
- package/package.json +1 -1
- package/run-agent.mjs +14 -0
- package/vendor/gate.cjs +6 -3
- package/web/admin/oauth-signin.mjs +37 -0
- package/web/app/dist/sw.js +58 -7
- package/web/app/dist/sw.js.map +2 -2
- package/web/app/site/admin/client/index.html +2 -2
- package/web/app/site/admin/client-phanpy/index.html +2 -2
- package/web/app/site/admin/index.html +2 -2
- package/web/app/site/admin/oauth-signin.mjs +37 -0
- package/web/app/site/admin/setup/index.html +2 -2
- package/web/app/site/app/compose/index.html +2 -2
- package/web/app/site/app/index.html +2 -2
- package/web/app/site/authorize_interaction.html +2 -2
- package/web/app/site/bb/index.html +2 -2
- package/web/app/site/build.json +1 -1
- package/web/app/site/index.html +2 -2
- package/web/app/site/sw.js +58 -7
- package/web/front/admin.html +0 -1
- package/web/front/new-account.html +0 -1
- package/web/front/run.html +13 -7
- package/web/front/run.js +73 -17
|
@@ -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.
|
|
23
|
-
<meta name="fedipod-build" content="
|
|
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.
|
|
23
|
-
<meta name="fedipod-build" content="
|
|
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.
|
|
172
|
-
<meta name="fedipod-build" content="
|
|
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.
|
|
58
|
-
<meta name="fedipod-build" content="
|
|
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.
|
|
16
|
-
<meta name="fedipod-build" content="
|
|
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.
|
|
69
|
-
<meta name="fedipod-build" content="
|
|
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.
|
|
39
|
-
<meta name="fedipod-build" content="
|
|
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.
|
|
215
|
-
<meta name="fedipod-build" content="
|
|
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>
|
package/web/app/site/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"build":"
|
|
1
|
+
{"build":"940565ccb8f5","version":"1.27.3","at":"2026-09-22T03:52:57.316Z"}
|
package/web/app/site/index.html
CHANGED
|
@@ -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.
|
|
76
|
-
<meta name="fedipod-build" content="
|
|
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>
|
package/web/app/site/sw.js
CHANGED
|
@@ -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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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
|
-
|
|
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: "
|
|
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;
|
package/web/front/admin.html
CHANGED
package/web/front/run.html
CHANGED
|
@@ -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 —
|
|
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>
|
|
29
|
-
<p class="lede">
|
|
30
|
-
|
|
31
|
-
|
|
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 & 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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
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', {
|
|
32
|
-
|
|
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
|
|
107
|
-
n.textContent = 'Your
|
|
108
|
-
|
|
109
|
-
n.append(
|
|
110
|
-
|
|
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 = '
|
|
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'));
|