fedipod-server 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/client/masto/index.mjs +6 -1
- package/lib/core/publisher/index.mjs +1 -1
- package/lib/core/wire.mjs +2 -1
- package/lib/device/admin/routes/lifecycle.mjs +1 -1
- package/lib/device/admin/routes/setup.mjs +20 -1
- package/lib/device/cli/commands/setup.mjs +44 -20
- package/lib/device/setup.mjs +99 -25
- package/lib/gateway/front-core.mjs +14 -2
- package/lib/pod/root.mjs +27 -0
- package/lib/pod/transport.mjs +7 -2
- package/lib/pod/urls.mjs +14 -0
- package/package.json +1 -1
- package/web/admin/setup/index.html +25 -2
- package/web/admin/setup/setup.js +30 -1
- package/web/app/README.md +2 -2
- package/web/app/admin-facade.mjs +1 -1
- package/web/app/agent.mjs +17 -5
- package/web/app/boot.mjs +80 -20
- package/web/app/dist/boot.js +187 -49
- package/web/app/dist/boot.js.map +3 -3
- package/web/app/dist/sw.js +31 -13
- package/web/app/dist/sw.js.map +2 -2
- package/web/app/index.html +46 -8
- package/web/app/keys-browser.mjs +8 -3
- package/web/app/signup.mjs +68 -40
- package/web/app/site/admin/setup/index.html +25 -2
- package/web/app/site/admin/setup/setup.js +30 -1
- package/web/app/site/boot.js +187 -49
- package/web/app/site/index.html +46 -8
- package/web/app/site/sw.js +31 -13
package/web/app/site/boot.js
CHANGED
|
@@ -33025,9 +33025,10 @@ var PodTransport = class {
|
|
|
33025
33025
|
return serialize(doc, g, url, "text/turtle");
|
|
33026
33026
|
}
|
|
33027
33027
|
async setAcl(targetUrl, publicModes, opts = {}) {
|
|
33028
|
-
const
|
|
33028
|
+
const podTarget = this.toPod ? this.toPod(targetUrl) : targetUrl;
|
|
33029
|
+
const url = await this.aclUrlFor(podTarget);
|
|
33029
33030
|
if (!await this.aclWritable(url)) return null;
|
|
33030
|
-
return this.put(url, this.aclDoc(
|
|
33031
|
+
return this.put(url, this.aclDoc(podTarget, publicModes, { ...opts, aclUrl: url }), "text/turtle");
|
|
33031
33032
|
}
|
|
33032
33033
|
// Child documents of an LDP container (URLs under it, excluding aux docs).
|
|
33033
33034
|
// Revalidated: the inbox is polled every couple of minutes and is usually
|
|
@@ -33266,6 +33267,15 @@ function handleProblem(handle) {
|
|
|
33266
33267
|
}
|
|
33267
33268
|
var PROGRESS = /* @__PURE__ */ new Map();
|
|
33268
33269
|
var progressKey = (a) => [a.issuer, a.mode, a.handle, a.mode === "new" ? a.podName || a.handle : a.pod].join("|");
|
|
33270
|
+
async function assertFrontNameFree(frontOrigin, handle) {
|
|
33271
|
+
const res = await fetch(
|
|
33272
|
+
`${frontOrigin.replace(/\/$/, "")}/api/handle?handle=${encodeURIComponent(handle)}`,
|
|
33273
|
+
{ headers: { accept: "application/json" } }
|
|
33274
|
+
).catch(() => null);
|
|
33275
|
+
const d = res ? await res.json().catch(() => ({})) : null;
|
|
33276
|
+
if (!d) throw new Error(`${new URL(frontOrigin).host} did not answer whether @${handle} is free`);
|
|
33277
|
+
if (!d.available) throw new Error(d.reason || `the name @${handle}@${new URL(frontOrigin).host} is taken \u2014 choose another handle`);
|
|
33278
|
+
}
|
|
33269
33279
|
async function signUp(answers, { onStep = () => {
|
|
33270
33280
|
}, frontOrigin = null } = {}) {
|
|
33271
33281
|
const { mode, issuer, email, password, handle } = answers;
|
|
@@ -33274,6 +33284,9 @@ async function signUp(answers, { onStep = () => {
|
|
|
33274
33284
|
if (!email) throw new Error("an email is required");
|
|
33275
33285
|
if (!password) throw new Error("a password is required");
|
|
33276
33286
|
if (mode === "existing" && !answers.pod) throw new Error("a pod address is required");
|
|
33287
|
+
const wantsFront = answers.shape === "front";
|
|
33288
|
+
if (wantsFront && !frontOrigin) throw new Error("an address at the gateway needs a gateway, and this page has none");
|
|
33289
|
+
if (wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
33277
33290
|
const key = progressKey(answers);
|
|
33278
33291
|
const prog = PROGRESS.get(key) || {};
|
|
33279
33292
|
PROGRESS.set(key, prog);
|
|
@@ -33305,11 +33318,14 @@ async function signUp(answers, { onStep = () => {
|
|
|
33305
33318
|
}
|
|
33306
33319
|
const pod = prog.pod;
|
|
33307
33320
|
const webId = prog.webId || null;
|
|
33308
|
-
const
|
|
33309
|
-
|
|
33310
|
-
|
|
33321
|
+
const pathPod = new URL(pod).pathname !== "/";
|
|
33322
|
+
const fronted = pathPod || wantsFront;
|
|
33323
|
+
if (fronted && !frontOrigin) {
|
|
33324
|
+
throw new Error(`${pod} is a suffix-based host, so its address must live at a gateway, and this page has none.`);
|
|
33311
33325
|
}
|
|
33326
|
+
if (pathPod && !wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
33312
33327
|
const actorUrl = actorUrlFor(pod);
|
|
33328
|
+
const frontActor = fronted ? `${frontOrigin.replace(/\/$/, "")}/u/${handle}/ap/actor` : null;
|
|
33313
33329
|
const cred = step("credential");
|
|
33314
33330
|
let credential;
|
|
33315
33331
|
if (!prog.credential) {
|
|
@@ -33324,12 +33340,36 @@ async function signUp(answers, { onStep = () => {
|
|
|
33324
33340
|
cred.ok();
|
|
33325
33341
|
}
|
|
33326
33342
|
const session = await makeDpopSession(credential);
|
|
33343
|
+
let gateway = answers.gateway || prog.gateway || null;
|
|
33344
|
+
if (frontOrigin && !gateway) {
|
|
33345
|
+
const gw = step("gateway");
|
|
33346
|
+
gw.running(fronted ? `taking your address at ${new URL(frontOrigin).host}` : `connecting your mail door on ${new URL(frontOrigin).host}`);
|
|
33347
|
+
const res = await session.fetch(`${frontOrigin.replace(/\/$/, "")}/api/attach`, {
|
|
33348
|
+
method: "POST",
|
|
33349
|
+
headers: { "content-type": "application/json" },
|
|
33350
|
+
// podHome is the AP CONTAINER, not the pod root: the front builds the
|
|
33351
|
+
// delivery target as `podHome + 'ap/inbox/'` (lib/front-core.mjs), so a
|
|
33352
|
+
// bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
|
|
33353
|
+
// the container the agent drains, where nothing would ever read it. The
|
|
33354
|
+
// manage surface has always sent `urls.home`; this is the same value.
|
|
33355
|
+
body: JSON.stringify({ handle, podHome: `${pod}${AP_ROOT}`, actorUrl, kind: "person", fronted })
|
|
33356
|
+
});
|
|
33357
|
+
const d = await res.json().catch(() => ({}));
|
|
33358
|
+
if (res.status !== 201 || !d.hmacSecret) {
|
|
33359
|
+
throw new Error(`could not connect the gateway (HTTP ${res.status}): ${d.error || ""}`);
|
|
33360
|
+
}
|
|
33361
|
+
gateway = fronted ? { url: `${frontOrigin.replace(/\/$/, "")}/u/${handle}/ap/inbox/`, frontActor: String(d.frontActor || frontActor), hmacSecret: d.hmacSecret, mode: "trust" } : { url: d.doorInbox, hmacSecret: d.hmacSecret, mode: "trust" };
|
|
33362
|
+
prog.gateway = gateway;
|
|
33363
|
+
gw.ok();
|
|
33364
|
+
} else if (frontOrigin && gateway) {
|
|
33365
|
+
step("gateway").ok();
|
|
33366
|
+
}
|
|
33327
33367
|
const keysStep = step("keys");
|
|
33328
33368
|
let keys;
|
|
33329
33369
|
if (!prog.keysStored) {
|
|
33330
33370
|
keysStep.running("making your signing key and locking it under your password");
|
|
33331
33371
|
keys = await generateKeys();
|
|
33332
|
-
keys.mintedFor = actorUrl;
|
|
33372
|
+
keys.mintedFor = gateway?.frontActor || actorUrl;
|
|
33333
33373
|
const remote = new BrowserRemotePod(session, { webId: credential.webId, role: "signup", log: () => {
|
|
33334
33374
|
} });
|
|
33335
33375
|
try {
|
|
@@ -33349,30 +33389,6 @@ async function signUp(answers, { onStep = () => {
|
|
|
33349
33389
|
keys = prog.keys;
|
|
33350
33390
|
keysStep.ok();
|
|
33351
33391
|
}
|
|
33352
|
-
let gateway = answers.gateway || prog.gateway || null;
|
|
33353
|
-
if (frontOrigin && !gateway) {
|
|
33354
|
-
const gw = step("gateway");
|
|
33355
|
-
gw.running(`connecting your mail door on ${new URL(frontOrigin).host}`);
|
|
33356
|
-
const res = await session.fetch(`${frontOrigin.replace(/\/$/, "")}/api/attach`, {
|
|
33357
|
-
method: "POST",
|
|
33358
|
-
headers: { "content-type": "application/json" },
|
|
33359
|
-
// podHome is the AP CONTAINER, not the pod root: the front builds the
|
|
33360
|
-
// delivery target as `podHome + 'ap/inbox/'` (lib/front-core.mjs), so a
|
|
33361
|
-
// bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
|
|
33362
|
-
// the container the agent drains, where nothing would ever read it. The
|
|
33363
|
-
// manage surface has always sent `urls.home`; this is the same value.
|
|
33364
|
-
body: JSON.stringify({ handle, podHome: `${pod}${AP_ROOT}`, actorUrl, kind: "person" })
|
|
33365
|
-
});
|
|
33366
|
-
const d = await res.json().catch(() => ({}));
|
|
33367
|
-
if (res.status !== 201 || !d.hmacSecret) {
|
|
33368
|
-
throw new Error(`could not connect the mail door (HTTP ${res.status}): ${d.error || ""}`);
|
|
33369
|
-
}
|
|
33370
|
-
gateway = { url: d.doorInbox, hmacSecret: d.hmacSecret, mode: "trust" };
|
|
33371
|
-
prog.gateway = gateway;
|
|
33372
|
-
gw.ok();
|
|
33373
|
-
} else if (frontOrigin && gateway) {
|
|
33374
|
-
step("gateway").ok();
|
|
33375
|
-
}
|
|
33376
33392
|
const config = {
|
|
33377
33393
|
remotePod: pod,
|
|
33378
33394
|
root: AP_ROOT,
|
|
@@ -33397,7 +33413,7 @@ async function signUp(answers, { onStep = () => {
|
|
|
33397
33413
|
onStep("credential", "ok", "this browser is ready (the setup credential could not be revoked automatically \u2014 you can remove it from your pod's account page)");
|
|
33398
33414
|
}
|
|
33399
33415
|
PROGRESS.delete(key);
|
|
33400
|
-
const host = new URL(pod).host;
|
|
33416
|
+
const host = gateway?.frontActor ? new URL(gateway.frontActor).host : new URL(pod).host;
|
|
33401
33417
|
return {
|
|
33402
33418
|
credential,
|
|
33403
33419
|
config,
|
|
@@ -33425,6 +33441,45 @@ async function readIssuer(actorUrl, fetchImpl = fetch) {
|
|
|
33425
33441
|
}
|
|
33426
33442
|
}
|
|
33427
33443
|
|
|
33444
|
+
// lib/pod/urls.mjs
|
|
33445
|
+
function podBaseOfWebId(webId) {
|
|
33446
|
+
const u = new URL(webId);
|
|
33447
|
+
u.hash = "";
|
|
33448
|
+
u.search = "";
|
|
33449
|
+
const dir = u.pathname.replace(/profile\/card$/u, "").replace(/[^/]*$/u, "");
|
|
33450
|
+
return `${u.origin}${dir.endsWith("/") ? dir : dir + "/"}`;
|
|
33451
|
+
}
|
|
33452
|
+
|
|
33453
|
+
// lib/pod/root.mjs
|
|
33454
|
+
var OWNER_LOOKUP_MS = 5e3;
|
|
33455
|
+
var PUBLIC_DOC_MAX_BYTES = 1024 * 1024;
|
|
33456
|
+
async function podLayout(fetchImpl, providerOrigin, { timeoutMs = OWNER_LOOKUP_MS } = {}) {
|
|
33457
|
+
let origin;
|
|
33458
|
+
try {
|
|
33459
|
+
origin = new URL(providerOrigin).origin;
|
|
33460
|
+
} catch {
|
|
33461
|
+
return null;
|
|
33462
|
+
}
|
|
33463
|
+
let res;
|
|
33464
|
+
try {
|
|
33465
|
+
res = await fetchImpl(
|
|
33466
|
+
`${origin}/.well-known/solid`,
|
|
33467
|
+
{ headers: { accept: "text/turtle" }, signal: AbortSignal.timeout(timeoutMs) }
|
|
33468
|
+
);
|
|
33469
|
+
} catch {
|
|
33470
|
+
return null;
|
|
33471
|
+
}
|
|
33472
|
+
if (res.status === 501) return "host";
|
|
33473
|
+
if (res.status !== 200) return null;
|
|
33474
|
+
let body = "";
|
|
33475
|
+
try {
|
|
33476
|
+
body = await readCapped(res, 64 * 1024);
|
|
33477
|
+
} catch {
|
|
33478
|
+
return null;
|
|
33479
|
+
}
|
|
33480
|
+
return /ns\/pim\/space#Storage|pim:Storage/u.test(body) ? "path" : null;
|
|
33481
|
+
}
|
|
33482
|
+
|
|
33428
33483
|
// web/app/oidc-session.mjs
|
|
33429
33484
|
var DB2 = "fedipod-oidc";
|
|
33430
33485
|
var STORE = "session";
|
|
@@ -33674,7 +33729,7 @@ window.fedipodUnlock = async (password) => {
|
|
|
33674
33729
|
if (!password) throw new Error("Enter your account password.");
|
|
33675
33730
|
const session = await getSession();
|
|
33676
33731
|
if (!session) throw new Error("Sign in first.");
|
|
33677
|
-
const podFromWebId =
|
|
33732
|
+
const podFromWebId = podBaseOfWebId(session.webId);
|
|
33678
33733
|
const state = `${podFromWebId}${AP_ROOT}ap-state/`;
|
|
33679
33734
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: "signup", log: () => {
|
|
33680
33735
|
} });
|
|
@@ -33711,16 +33766,30 @@ async function issuerForPod(pod) {
|
|
|
33711
33766
|
if (authz) return new URL(authz).origin;
|
|
33712
33767
|
} catch {
|
|
33713
33768
|
}
|
|
33714
|
-
const
|
|
33715
|
-
|
|
33716
|
-
|
|
33769
|
+
const u = new URL(pod);
|
|
33770
|
+
if (u.pathname !== "/") return u.origin;
|
|
33771
|
+
const parent = u.host.split(".").slice(1).join(".");
|
|
33772
|
+
return `https://${parent || u.host}`;
|
|
33773
|
+
}
|
|
33774
|
+
async function podForFrontedAddress(handle) {
|
|
33775
|
+
const res = await fetch(
|
|
33776
|
+
`/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${location.host}`)}`,
|
|
33777
|
+
{ headers: { accept: "application/jrd+json, application/json" } }
|
|
33778
|
+
).catch(() => null);
|
|
33779
|
+
if (!res || res.status >= 400) throw new Error(`nobody at this site is called @${handle}@${location.host}`);
|
|
33780
|
+
const doc = await res.json().catch(() => ({}));
|
|
33781
|
+
const podActorId = (doc.aliases || []).find((a) => /\/ap\/actor$/u.test(String(a)));
|
|
33782
|
+
if (!podActorId) throw new Error(`@${handle}@${location.host} lives here but names no pod to sign in to`);
|
|
33783
|
+
const tail = `${AP_ROOT}ap/actor`;
|
|
33784
|
+
if (!podActorId.endsWith(tail)) throw new Error(`the pod actor ${podActorId} is not where a FediPod pod keeps one`);
|
|
33785
|
+
return podActorId.slice(0, -tail.length);
|
|
33717
33786
|
}
|
|
33718
33787
|
window.fedipodSignin = async ({ address }) => {
|
|
33719
33788
|
const parsed = parseAddress(address);
|
|
33720
33789
|
if (!parsed) throw new Error("Enter your address as @you@yourpod (for example @alice@alice.solidcommunity.net).");
|
|
33721
33790
|
const bad = handleProblem(parsed.handle);
|
|
33722
33791
|
if (bad) throw new Error(bad);
|
|
33723
|
-
const pod = `https://${parsed.host}/`;
|
|
33792
|
+
const pod = parsed.host === location.host.toLowerCase() ? await podForFrontedAddress(parsed.handle) : `https://${parsed.host}/`;
|
|
33724
33793
|
const issuer = await issuerForPod(pod);
|
|
33725
33794
|
const { authorizationUrl } = await beginLogin({ issuer, redirectUri: REDIRECT });
|
|
33726
33795
|
location.href = authorizationUrl;
|
|
@@ -33866,8 +33935,8 @@ ${e.detail}` : "");
|
|
|
33866
33935
|
});
|
|
33867
33936
|
const f = () => $("form").elements;
|
|
33868
33937
|
const providerUrl = () => {
|
|
33869
|
-
let v = f().provider.value.trim();
|
|
33870
|
-
if (!v)
|
|
33938
|
+
let v = (f().provider.value || f().providerOther.value).trim();
|
|
33939
|
+
if (!v) return "";
|
|
33871
33940
|
if (!/^https?:\/\//i.test(v)) v = "https://" + v;
|
|
33872
33941
|
return v;
|
|
33873
33942
|
};
|
|
@@ -33883,6 +33952,42 @@ ${e.detail}` : "");
|
|
|
33883
33952
|
const ph = providerHost();
|
|
33884
33953
|
return sub && ph ? `${sub}.${ph}` : "";
|
|
33885
33954
|
};
|
|
33955
|
+
const podUrl = () => {
|
|
33956
|
+
let v = f().pod.value.trim();
|
|
33957
|
+
if (!v) return "";
|
|
33958
|
+
if (!/^https?:\/\//i.test(v)) v = "https://" + v;
|
|
33959
|
+
if (!v.endsWith("/")) v += "/";
|
|
33960
|
+
try {
|
|
33961
|
+
return new URL(v).href;
|
|
33962
|
+
} catch {
|
|
33963
|
+
return "";
|
|
33964
|
+
}
|
|
33965
|
+
};
|
|
33966
|
+
const isPathPod = (u) => {
|
|
33967
|
+
try {
|
|
33968
|
+
return new URL(u).pathname !== "/";
|
|
33969
|
+
} catch {
|
|
33970
|
+
return false;
|
|
33971
|
+
}
|
|
33972
|
+
};
|
|
33973
|
+
const layouts = /* @__PURE__ */ new Map();
|
|
33974
|
+
let layout = null;
|
|
33975
|
+
const learnLayout = async () => {
|
|
33976
|
+
const origin = providerHost() ? new URL(providerUrl()).origin : "";
|
|
33977
|
+
if (!origin) {
|
|
33978
|
+
layout = null;
|
|
33979
|
+
return;
|
|
33980
|
+
}
|
|
33981
|
+
if (!layouts.has(origin)) layouts.set(origin, podLayout(fetch, origin).catch(() => null));
|
|
33982
|
+
const known = await layouts.get(origin);
|
|
33983
|
+
if (providerHost() && new URL(providerUrl()).origin === origin) {
|
|
33984
|
+
layout = known;
|
|
33985
|
+
applyShape();
|
|
33986
|
+
previewAddr();
|
|
33987
|
+
}
|
|
33988
|
+
};
|
|
33989
|
+
const pathPod = () => f().mode.value === "existing" ? isPathPod(podUrl()) : layout === "path";
|
|
33990
|
+
const shape = () => pathPod() ? "front" : f().shape.value;
|
|
33886
33991
|
const answers = () => {
|
|
33887
33992
|
const mode = f().mode.value;
|
|
33888
33993
|
const a = {
|
|
@@ -33890,18 +33995,43 @@ ${e.detail}` : "");
|
|
|
33890
33995
|
handle: f().handle.value.trim().toLowerCase(),
|
|
33891
33996
|
email: f().email.value.trim(),
|
|
33892
33997
|
password: f().password.value,
|
|
33893
|
-
issuer: providerUrl()
|
|
33998
|
+
issuer: providerUrl(),
|
|
33999
|
+
shape: shape()
|
|
33894
34000
|
};
|
|
33895
34001
|
if (mode === "new") a.podName = f().podName.value.trim().toLowerCase();
|
|
33896
|
-
else a.pod =
|
|
34002
|
+
else a.pod = podUrl();
|
|
33897
34003
|
return a;
|
|
33898
34004
|
};
|
|
33899
34005
|
const previewAddr = () => {
|
|
33900
34006
|
const handle = f().handle.value.trim().toLowerCase();
|
|
33901
|
-
const
|
|
33902
|
-
$("preview").textContent = handle &&
|
|
34007
|
+
const host = shape() === "front" ? location.host : f().mode.value === "existing" ? podUrl() ? new URL(podUrl()).host : "" : podHostOf();
|
|
34008
|
+
$("preview").textContent = handle && host ? `@${handle}@${host}` : "@\u2026@\u2026";
|
|
34009
|
+
};
|
|
34010
|
+
const applyShape = () => {
|
|
34011
|
+
const fixed = pathPod();
|
|
34012
|
+
for (const r of f().shape) {
|
|
34013
|
+
if (fixed) r.checked = r.value === "front";
|
|
34014
|
+
}
|
|
34015
|
+
$("shape-group").hidden = fixed;
|
|
34016
|
+
$("shape-hint").hidden = !fixed;
|
|
33903
34017
|
};
|
|
33904
|
-
|
|
34018
|
+
const applyMode = () => {
|
|
34019
|
+
const existing = f().mode.value === "existing";
|
|
34020
|
+
$("pod-field").hidden = !existing;
|
|
34021
|
+
$("podname-field").hidden = existing;
|
|
34022
|
+
$("provider-other-field").hidden = f().provider.value !== "";
|
|
34023
|
+
};
|
|
34024
|
+
for (const el of $("form").elements) for (const evt of ["input", "change"]) el.addEventListener(evt, () => {
|
|
34025
|
+
applyMode();
|
|
34026
|
+
applyShape();
|
|
34027
|
+
previewAddr();
|
|
34028
|
+
});
|
|
34029
|
+
for (const evt of ["input", "change"]) {
|
|
34030
|
+
$("provider").addEventListener(evt, learnLayout);
|
|
34031
|
+
$("providerOther").addEventListener(evt, learnLayout);
|
|
34032
|
+
}
|
|
34033
|
+
applyMode();
|
|
34034
|
+
learnLayout();
|
|
33905
34035
|
const STEP_IDS = ["step-1", "step-2"];
|
|
33906
34036
|
const FOCUS = { 1: "provider", 2: "handle" };
|
|
33907
34037
|
const goStep = (n) => {
|
|
@@ -33910,15 +34040,23 @@ ${e.detail}` : "");
|
|
|
33910
34040
|
});
|
|
33911
34041
|
$("err-1").textContent = "";
|
|
33912
34042
|
$("form-error").textContent = "";
|
|
33913
|
-
if (n === 2)
|
|
34043
|
+
if (n === 2) {
|
|
34044
|
+
applyShape();
|
|
34045
|
+
previewAddr();
|
|
34046
|
+
learnLayout();
|
|
34047
|
+
}
|
|
33914
34048
|
if (FOCUS[n]) $(FOCUS[n]).focus();
|
|
33915
34049
|
};
|
|
33916
34050
|
const validateStep1 = () => {
|
|
33917
|
-
if (!providerHost()) return "A valid pod provider URL is required.";
|
|
33918
|
-
|
|
33919
|
-
|
|
33920
|
-
|
|
33921
|
-
|
|
34051
|
+
if (!providerHost()) return f().provider.value === "" ? "A pod provider address is required under Other\u2026." : "A valid pod provider URL is required.";
|
|
34052
|
+
if (f().mode.value === "existing") {
|
|
34053
|
+
if (!podUrl()) return "A pod address is required, like https://alice.solidcommunity.net/ or https://server.example/alice/.";
|
|
34054
|
+
} else {
|
|
34055
|
+
const sub = f().podName.value.trim().toLowerCase();
|
|
34056
|
+
if (!sub) return "A pod username/subdomain is required.";
|
|
34057
|
+
const sp = window.fedipodHandleProblem(sub);
|
|
34058
|
+
if (sp) return `Pod username: ${sp}`;
|
|
34059
|
+
}
|
|
33922
34060
|
if (!f().email.value.trim()) return "A pod email is required.";
|
|
33923
34061
|
if (!f().password.value) return "A pod password is required.";
|
|
33924
34062
|
return null;
|
package/web/app/site/index.html
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
--heading: #123f74; --sub: #565656; --line: #b0b0b0; --accent: #12467e;
|
|
14
14
|
--btn: #3a5f43; --btn-hover: #30503a; --btn-text: #ffffff;
|
|
15
15
|
--field-bg: #ececec; --field-text: #141414; --field-border: #6f6f6f;
|
|
16
|
+
--select-bg: #dce7f3; --select-border: #12467e;
|
|
16
17
|
--err: #b00020; --warn: #8a4b00; --focus: #12467e;
|
|
17
18
|
}
|
|
18
19
|
@media (prefers-color-scheme: dark) {
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
--bg: #191919; --fg: #f3f3f3;
|
|
21
22
|
--heading: #8fc2f2; --sub: #d2d2d2; --line: #6d6d6d; --accent: #8fc2f2;
|
|
22
23
|
--field-bg: #474747; --field-text: #f5f5f5; --field-border: #9aa0a6;
|
|
24
|
+
--select-bg: #2f4a66; --select-border: #8fc2f2;
|
|
23
25
|
--btn: #4a7a54; --btn-hover: #3f6a49;
|
|
24
26
|
--err: #ff9b9b; --warn: #ffb366; --focus: #8fc2f2;
|
|
25
27
|
}
|
|
@@ -42,6 +44,10 @@ input[type=text], input[type=email], input[type=url], input[type=password], sele
|
|
|
42
44
|
font: inherit; font-size: 1rem; width: 100%; padding: .55rem; box-sizing: border-box;
|
|
43
45
|
border: 1px solid var(--field-border); border-radius: .3rem;
|
|
44
46
|
background: var(--field-bg); color: var(--field-text); }
|
|
47
|
+
/* A list to pick from looks different from a box to type in: its own tint,
|
|
48
|
+
the accent border, and the same on its options. */
|
|
49
|
+
select { background: var(--select-bg); border-color: var(--select-border); }
|
|
50
|
+
select option { background: var(--select-bg); color: var(--field-text); }
|
|
45
51
|
::placeholder { color: var(--sub); opacity: 1; }
|
|
46
52
|
.choice { display: block; margin: .4rem 0; font-weight: normal; }
|
|
47
53
|
.choice input { margin-right: .5rem; width: auto; }
|
|
@@ -88,7 +94,7 @@ a { color: var(--accent); }
|
|
|
88
94
|
<section id="landing" hidden>
|
|
89
95
|
<h2>Already have a FediPod account?</h2>
|
|
90
96
|
<p class="row">
|
|
91
|
-
<input type="text" id="signin-address" autocapitalize="off" spellcheck="false" placeholder="@me@mypod"
|
|
97
|
+
<input type="text" id="signin-address" autocomplete="username" autocapitalize="off" spellcheck="false" placeholder="@me@mypod"
|
|
92
98
|
aria-label="Your FediPod address, in the form @you@yourpod">
|
|
93
99
|
<button type="button" class="primary" id="signin">sign in</button>
|
|
94
100
|
</p>
|
|
@@ -136,18 +142,42 @@ a { color: var(--accent); }
|
|
|
136
142
|
</div>
|
|
137
143
|
|
|
138
144
|
<label for="provider">Pod provider</label>
|
|
139
|
-
|
|
145
|
+
<!-- The providers listed at https://solidproject.org/get_a_pod that run Community
|
|
146
|
+
Solid Server, whose account API is what "A new pod" uses. Checked 2026-09-14. -->
|
|
147
|
+
<select id="provider" name="provider">
|
|
148
|
+
<option value="https://solidcommunity.net">solidcommunity.net</option>
|
|
149
|
+
<option value="https://privatedatapod.com">privatedatapod.com</option>
|
|
150
|
+
<option value="https://solid.redpencil.io">redpencil.io</option>
|
|
151
|
+
<option value="https://pods.solidcommunity.au">solidcommunity.au</option>
|
|
152
|
+
<option value="https://solidweb.me">solidweb.me</option>
|
|
153
|
+
<option value="https://teamid.live">teamid.live</option>
|
|
154
|
+
<option value="">Other…</option>
|
|
155
|
+
</select>
|
|
156
|
+
<div id="provider-other-field" hidden>
|
|
157
|
+
<label for="providerOther">Provider address</label>
|
|
158
|
+
<input type="url" id="providerOther" name="providerOther" autocomplete="url" placeholder="https://pods.example.org">
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div id="podname-field">
|
|
162
|
+
<label for="podName">Pod username</label>
|
|
163
|
+
<input type="text" id="podName" name="podName" autocomplete="off" aria-describedby="podname-hint">
|
|
164
|
+
<p class="hint" id="podname-hint">Depending on your provider's setup, your pod will be at
|
|
165
|
+
either https://USERNAME.podprovider or https://podprovider/USERNAME.</p>
|
|
166
|
+
</div>
|
|
140
167
|
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
168
|
+
<div id="pod-field" hidden>
|
|
169
|
+
<label for="pod">Pod address</label>
|
|
170
|
+
<input type="url" id="pod" name="pod" autocomplete="url" aria-describedby="pod-hint"
|
|
171
|
+
placeholder="https://alice.solidcommunity.net/ or https://server.example/alice/">
|
|
172
|
+
<p class="hint" id="pod-hint">The address of your pod: its own host, or a path on a
|
|
173
|
+
suffix-based host.</p>
|
|
174
|
+
</div>
|
|
145
175
|
|
|
146
|
-
<label for="email">
|
|
176
|
+
<label for="email">Email (used as pod id)</label>
|
|
147
177
|
<input type="email" id="email" name="email" autocomplete="email" placeholder="you@example.org">
|
|
148
178
|
|
|
149
179
|
<label for="password">Pod password</label>
|
|
150
|
-
<input type="password" id="password" name="password" autocomplete="
|
|
180
|
+
<input type="password" id="password" name="password" autocomplete="new-password"
|
|
151
181
|
aria-describedby="password-hint">
|
|
152
182
|
<p class="hint" id="password-hint">Your password is sent once, to make your account and to
|
|
153
183
|
lock your signing key. It is never stored. The credential used to set things up is
|
|
@@ -172,6 +202,14 @@ a { color: var(--accent); }
|
|
|
172
202
|
<p class="hint" id="handle-hint">The name in your Fediverse address. This is permanent.
|
|
173
203
|
Letters, digits, and hyphens only.</p>
|
|
174
204
|
|
|
205
|
+
<div role="radiogroup" aria-labelledby="shape-label" id="shape-group">
|
|
206
|
+
<span id="shape-label" class="grouplabel">Where your address lives</span>
|
|
207
|
+
<label class="choice"><input type="radio" name="shape" value="pod" checked> On your pod</label>
|
|
208
|
+
<label class="choice"><input type="radio" name="shape" value="front"> At this site</label>
|
|
209
|
+
</div>
|
|
210
|
+
<p class="hint" id="shape-hint" hidden>Your pod provider uses suffixes, so your only option
|
|
211
|
+
is to use an account on a gateway like this one.</p>
|
|
212
|
+
|
|
175
213
|
<span id="addr-label" class="grouplabel">Your Fediverse address</span>
|
|
176
214
|
<p class="address" id="preview" role="status" aria-live="polite" aria-labelledby="addr-label">@…@…</p>
|
|
177
215
|
</fieldset>
|
package/web/app/site/sw.js
CHANGED
|
@@ -141,6 +141,13 @@ function apUrls(remotePod, root, { publicBase = null } = {}) {
|
|
|
141
141
|
}
|
|
142
142
|
return urls;
|
|
143
143
|
}
|
|
144
|
+
function podBaseOfWebId(webId) {
|
|
145
|
+
const u = new URL(webId);
|
|
146
|
+
u.hash = "";
|
|
147
|
+
u.search = "";
|
|
148
|
+
const dir = u.pathname.replace(/profile\/card$/u, "").replace(/[^/]*$/u, "");
|
|
149
|
+
return `${u.origin}${dir.endsWith("/") ? dir : dir + "/"}`;
|
|
150
|
+
}
|
|
144
151
|
function webfingerHost(podUrl) {
|
|
145
152
|
const u = new URL(podUrl);
|
|
146
153
|
return u.pathname === "/" ? u.host : null;
|
|
@@ -9651,9 +9658,10 @@ function hostMeta(base) {
|
|
|
9651
9658
|
</XRD>
|
|
9652
9659
|
`;
|
|
9653
9660
|
}
|
|
9654
|
-
function jrd({ handle: handle7, host, actor }) {
|
|
9661
|
+
function jrd({ handle: handle7, host, actor, aliases = [] }) {
|
|
9655
9662
|
return {
|
|
9656
9663
|
subject: `acct:${handle7}@${host}`,
|
|
9664
|
+
...aliases.length ? { aliases } : {},
|
|
9657
9665
|
links: [{ rel: "self", type: "application/activity+json", href: actor }]
|
|
9658
9666
|
};
|
|
9659
9667
|
}
|
|
@@ -56499,7 +56507,7 @@ var Publisher = class {
|
|
|
56499
56507
|
}
|
|
56500
56508
|
const pubName = publicHandle(this.config);
|
|
56501
56509
|
const pubHost = this.config.gateway?.frontActor ? new URL(this.config.gateway.frontActor).host : host;
|
|
56502
|
-
this.log(this.config.gateway?.frontActor || webfingerHost(urls.base) ? `profile published: @${pubName}@${pubHost} \u2192 ${urls.actor}` : `profile published \u2192 ${urls.actor} \u2014 NOT discoverable as @${pubName}@${pubHost}: this pod is a
|
|
56510
|
+
this.log(this.config.gateway?.frontActor || webfingerHost(urls.base) ? `profile published: @${pubName}@${pubHost} \u2192 ${urls.actor}` : `profile published \u2192 ${urls.actor} \u2014 NOT discoverable as @${pubName}@${pubHost}: this pod is a suffix-based host, and WebFinger is only answered at a host root`);
|
|
56503
56511
|
return { unreachable, updated };
|
|
56504
56512
|
}
|
|
56505
56513
|
// Fires when the document differs from the one last published — including
|
|
@@ -66231,8 +66239,11 @@ var MastoApi = class _MastoApi {
|
|
|
66231
66239
|
});
|
|
66232
66240
|
return this._push;
|
|
66233
66241
|
}
|
|
66242
|
+
// The host in the owner's own address: the gateway's when the identity is
|
|
66243
|
+
// fronted (its documents still live on the pod, but its name does not).
|
|
66234
66244
|
get host() {
|
|
66235
|
-
|
|
66245
|
+
if (!this.urls) return "unconfigured.invalid";
|
|
66246
|
+
return new URL(this.urls.publicHome || this.urls.base).host;
|
|
66236
66247
|
}
|
|
66237
66248
|
// Where the live feed is, as the CLIENT must address it: this agent's own
|
|
66238
66249
|
// origin, taken from the request, not the pod's host. An instance document
|
|
@@ -66846,9 +66857,10 @@ var PodTransport = class {
|
|
|
66846
66857
|
return serialize(doc, g, url, "text/turtle");
|
|
66847
66858
|
}
|
|
66848
66859
|
async setAcl(targetUrl, publicModes, opts = {}) {
|
|
66849
|
-
const
|
|
66860
|
+
const podTarget = this.toPod ? this.toPod(targetUrl) : targetUrl;
|
|
66861
|
+
const url = await this.aclUrlFor(podTarget);
|
|
66850
66862
|
if (!await this.aclWritable(url)) return null;
|
|
66851
|
-
return this.put(url, this.aclDoc(
|
|
66863
|
+
return this.put(url, this.aclDoc(podTarget, publicModes, { ...opts, aclUrl: url }), "text/turtle");
|
|
66852
66864
|
}
|
|
66853
66865
|
// Child documents of an LDP container (URLs under it, excluding aux docs).
|
|
66854
66866
|
// Revalidated: the inbox is polled every couple of minutes and is usually
|
|
@@ -67166,14 +67178,16 @@ async function cacheOpenedKeys(actorUrl, keysRecord) {
|
|
|
67166
67178
|
return keys;
|
|
67167
67179
|
}
|
|
67168
67180
|
var keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
|
|
67181
|
+
var podActorOf = (urls) => urls.toPod ? urls.toPod(urls.actor) : urls.actor;
|
|
67169
67182
|
async function loadKeysFromPod(remote, urls) {
|
|
67170
|
-
const
|
|
67183
|
+
const podActor = podActorOf(urls);
|
|
67184
|
+
const cached = await kvGet(keyCacheKey(podActor)).catch(() => null);
|
|
67171
67185
|
if (isOpenedKey(cached)) return fromCache(cached);
|
|
67172
|
-
if (cached?.rsa?.privatePem) return cacheOpenedKeys(
|
|
67186
|
+
if (cached?.rsa?.privatePem) return cacheOpenedKeys(podActor, cached);
|
|
67173
67187
|
const doc = await readWrappedKeys(remote, urls);
|
|
67174
67188
|
if (isKeyEnvelope(doc)) throw new KeyPasswordNeeded();
|
|
67175
67189
|
if (!doc || !doc.rsa) throw new Error("no signing key on the pod \u2014 sign up did not finish");
|
|
67176
|
-
return cacheOpenedKeys(
|
|
67190
|
+
return cacheOpenedKeys(podActor, doc);
|
|
67177
67191
|
}
|
|
67178
67192
|
|
|
67179
67193
|
// lib/core/deliver.mjs
|
|
@@ -68259,7 +68273,7 @@ var AdminFacade = class {
|
|
|
68259
68273
|
const aliases = [...cfg.aliases || []];
|
|
68260
68274
|
if (body.add) {
|
|
68261
68275
|
if (!webfingerHost(urls.base) && !cfg.gateway?.frontActor) {
|
|
68262
|
-
return json2(400, { error: "this pod is a
|
|
68276
|
+
return json2(400, { error: "this pod is a suffix-based host, so other servers could never resolve it as a Move target" });
|
|
68263
68277
|
}
|
|
68264
68278
|
const id = await this.resolveActor(body.add);
|
|
68265
68279
|
if (!id) return json2(400, { error: `could not fetch the old account (${body.add}) \u2014 enter its URL or @user@host, and it must answer` });
|
|
@@ -69602,6 +69616,7 @@ var AcctFeed = class {
|
|
|
69602
69616
|
};
|
|
69603
69617
|
|
|
69604
69618
|
// web/app/agent.mjs
|
|
69619
|
+
init_urls();
|
|
69605
69620
|
var originAuthorities = (host) => ({
|
|
69606
69621
|
set: /* @__PURE__ */ new Set([String(host || "").toLowerCase()]),
|
|
69607
69622
|
has(authority) {
|
|
@@ -69726,7 +69741,7 @@ var BrowserAgent = class _BrowserAgent {
|
|
|
69726
69741
|
if (oidc) {
|
|
69727
69742
|
session = { fetch: (u, i) => oidc.fetch(u, i) };
|
|
69728
69743
|
webId = oidc.webId;
|
|
69729
|
-
remotePod =
|
|
69744
|
+
remotePod = podBaseOfWebId(webId);
|
|
69730
69745
|
} else {
|
|
69731
69746
|
const dpop = await makeDpopSession(credential);
|
|
69732
69747
|
session = { fetch: (u, i) => dpop.fetch(u, i) };
|
|
@@ -69744,8 +69759,11 @@ var BrowserAgent = class _BrowserAgent {
|
|
|
69744
69759
|
const cfg = config || this.store.getConfig();
|
|
69745
69760
|
if (!cfg) throw new Error("no account config on this pod \u2014 sign up first");
|
|
69746
69761
|
this.store.setConfig({ ...this.store.getConfig() || {}, ...cfg, root });
|
|
69747
|
-
const keys = keysRecord ? await importSigningKey(keysRecord) : await loadKeysFromPod(this.remote, this.urls);
|
|
69748
69762
|
config = this.store.getConfig();
|
|
69763
|
+
const publicBase = config.gateway?.frontActor ? config.gateway.frontActor.replace(/ap\/actor\/?$/, "") : null;
|
|
69764
|
+
this.urls = apUrls2(remotePod, root, { publicBase });
|
|
69765
|
+
if (this.urls.toPod) this.remote.setUrlMap(this.urls.toPod);
|
|
69766
|
+
const keys = keysRecord ? await importSigningKey(keysRecord) : await loadKeysFromPod(this.remote, this.urls);
|
|
69749
69767
|
this.deliverer = new RelayDeliverer({
|
|
69750
69768
|
passive: true,
|
|
69751
69769
|
store: this.store,
|
|
@@ -69767,7 +69785,7 @@ var BrowserAgent = class _BrowserAgent {
|
|
|
69767
69785
|
publicKeyPem: keys.rsaPublicPem,
|
|
69768
69786
|
assertionKey: null,
|
|
69769
69787
|
log: this.log,
|
|
69770
|
-
// Who a post names, resolved — the same lookup the
|
|
69788
|
+
// Who a post names, resolved — the same lookup the DeviceAgent
|
|
69771
69789
|
// gives its publisher. Without it no mention from the browser ever
|
|
69772
69790
|
// resolved: a direct message went to nobody, a mention notified no one.
|
|
69773
69791
|
resolveMention: (h) => resolveHandle(this, h)
|
|
@@ -69870,7 +69888,7 @@ var BrowserAgent = class _BrowserAgent {
|
|
|
69870
69888
|
const rec = await generateKeys();
|
|
69871
69889
|
rec.mintedFor = this.urls.actor;
|
|
69872
69890
|
await writeWrappedKeys(this.remote, this.urls, await wrapKeys(rec, password));
|
|
69873
|
-
const keys = await cacheOpenedKeys(this.urls
|
|
69891
|
+
const keys = await cacheOpenedKeys(podActorOf(this.urls), rec);
|
|
69874
69892
|
this.publisher.publicKeyPem = keys.rsaPublicPem;
|
|
69875
69893
|
this.deliverer.rsaPrivate = keys.rsaPrivate;
|
|
69876
69894
|
await this.publisher.publishProfile();
|