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/README.md
CHANGED
|
@@ -72,6 +72,6 @@ See the `MastoApi` options in `agent.mjs`.
|
|
|
72
72
|
| **Streaming** | a service worker answers fetches, not sockets. No streaming URL is advertised, so clients poll. |
|
|
73
73
|
| **Web push** | `shims/web-push.mjs` is a no-op. `vapid` is omitted, and a client that subscribes anyway gets a 422 rather than a subscription nothing will push to. |
|
|
74
74
|
| **Scheduled posts** | nothing runs between now and the scheduled time. A `scheduled_at` is refused with a 422 that says so — accepting one was silent loss. |
|
|
75
|
-
| **Groups** | sign-up makes personal identities only (`signup.mjs`), and the moderation surface is not here. Joining a group works; hosting one needs the
|
|
76
|
-
| **
|
|
75
|
+
| **Groups** | sign-up makes personal identities only (`signup.mjs`), and the moderation surface is not here. Joining a group works; hosting one needs the DeviceAgent. See `groups.md`. |
|
|
76
|
+
| **Changing where the address lives** | the shape is chosen at sign-up (`signup.mjs`): on the pod, `@you@yourpod` with the gateway as a mail door, or at the gateway, `@you@front` — and a pod on a suffix-based host is always fronted. `admin-facade.mjs` refuses changing it afterwards, because a rename needs a restart a browser does not have. |
|
|
77
77
|
| **Moving the private half** | `/state-move` is about filesystem paths and `credential.json`. A browser has neither; its private half is always on the pod. |
|
package/web/app/admin-facade.mjs
CHANGED
|
@@ -317,7 +317,7 @@ export class AdminFacade {
|
|
|
317
317
|
const aliases = [...(cfg.aliases || [])];
|
|
318
318
|
if (body.add) {
|
|
319
319
|
if (!webfingerHost(urls.base) && !cfg.gateway?.frontActor) {
|
|
320
|
-
return json(400, { error: 'this pod is a
|
|
320
|
+
return json(400, { error: 'this pod is a suffix-based host, so other servers could never resolve it as a Move target' });
|
|
321
321
|
}
|
|
322
322
|
const id = await this.resolveActor(body.add);
|
|
323
323
|
if (!id) return json(400, { error: `could not fetch the old account (${body.add}) — enter its URL or @user@host, and it must answer` });
|
package/web/app/agent.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { MastoApi } from '../../lib/client/masto/index.mjs';
|
|
|
17
17
|
import { TagFeed } from '../../lib/connections/tagfeed.mjs';
|
|
18
18
|
import { makeDpopSession } from './pod-auth.mjs';
|
|
19
19
|
import { BrowserRemotePod } from './pod-remote.mjs';
|
|
20
|
-
import { importSigningKey, loadKeysFromPod, cacheOpenedKeys } from './keys-browser.mjs';
|
|
20
|
+
import { importSigningKey, loadKeysFromPod, cacheOpenedKeys, podActorOf } from './keys-browser.mjs';
|
|
21
21
|
import { generateKeys, wrapKeys } from './keystore.mjs';
|
|
22
22
|
import { RelayDeliverer, doorKeyOf } from './deliver-relay.mjs';
|
|
23
23
|
import { AdminFacade } from './admin-facade.mjs';
|
|
@@ -26,6 +26,7 @@ import { BskyFeed } from '../../lib/connections/bskyfeed.mjs';
|
|
|
26
26
|
import { BrowserFediAccounts } from './fediacct-browser.mjs';
|
|
27
27
|
import { AcctFeed } from '../../lib/connections/acctfeed.mjs';
|
|
28
28
|
import { followActor, unfollowActor, resolveHandle } from '../../lib/core/social.mjs';
|
|
29
|
+
import { podBaseOfWebId } from '../../lib/pod/urls.mjs';
|
|
29
30
|
import { ImportWorker } from '../../lib/connections/import.mjs';
|
|
30
31
|
|
|
31
32
|
// The authorities this identity answers on: exactly one, this origin. The Node
|
|
@@ -159,7 +160,7 @@ export class BrowserAgent {
|
|
|
159
160
|
if (oidc) {
|
|
160
161
|
session = { fetch: (u, i) => oidc.fetch(u, i) };
|
|
161
162
|
webId = oidc.webId;
|
|
162
|
-
remotePod =
|
|
163
|
+
remotePod = podBaseOfWebId(webId); // the pod, which on a suffix-based host is a path
|
|
163
164
|
} else {
|
|
164
165
|
const dpop = await makeDpopSession(credential);
|
|
165
166
|
session = { fetch: (u, i) => dpop.fetch(u, i) };
|
|
@@ -169,6 +170,8 @@ export class BrowserAgent {
|
|
|
169
170
|
this.webId = webId;
|
|
170
171
|
const root = (config && config.root) || 'fedipod/';
|
|
171
172
|
this.remote = new BrowserRemotePod(session, { webId, log: this.log });
|
|
173
|
+
// Pod-native for now: the state store below is read with these, and only
|
|
174
|
+
// the config it holds says whether this identity is fronted.
|
|
172
175
|
this.urls = apUrls(remotePod, root);
|
|
173
176
|
|
|
174
177
|
// State store, on the pod.
|
|
@@ -189,9 +192,18 @@ export class BrowserAgent {
|
|
|
189
192
|
// document it published landed under `activitypods-js/`. One root, decided
|
|
190
193
|
// once, carried by the config everything downstream reads.
|
|
191
194
|
this.store.setConfig({ ...(this.store.getConfig() || {}), ...cfg, root });
|
|
195
|
+
config = this.store.getConfig();
|
|
196
|
+
// A fronted identity (config.gateway.frontActor) advertises its ids at the
|
|
197
|
+
// gateway; the documents stay on the pod. Same three lines as the Node
|
|
198
|
+
// agent (run-agent.mjs connect): the advertised urls, and the map that
|
|
199
|
+
// turns an advertised url back into the pod one at the transport's single
|
|
200
|
+
// write and read choke point. State and media stay pod-native either way.
|
|
201
|
+
const publicBase = config.gateway?.frontActor
|
|
202
|
+
? config.gateway.frontActor.replace(/ap\/actor\/?$/, '') : null;
|
|
203
|
+
this.urls = apUrls(remotePod, root, { publicBase });
|
|
204
|
+
if (this.urls.toPod) this.remote.setUrlMap(this.urls.toPod);
|
|
192
205
|
// Keys: handed in (offline), or the owner-only keys.json read from the pod.
|
|
193
206
|
const keys = keysRecord ? await importSigningKey(keysRecord) : await loadKeysFromPod(this.remote, this.urls);
|
|
194
|
-
config = this.store.getConfig();
|
|
195
207
|
|
|
196
208
|
|
|
197
209
|
// `passive`: no queue-drain timer until this device is the active one.
|
|
@@ -212,7 +224,7 @@ export class BrowserAgent {
|
|
|
212
224
|
this.publisher = new Publisher({
|
|
213
225
|
config: this.store.getConfig(), remote: this.remote, store: this.store,
|
|
214
226
|
deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem, assertionKey: null, log: this.log,
|
|
215
|
-
// Who a post names, resolved — the same lookup the
|
|
227
|
+
// Who a post names, resolved — the same lookup the DeviceAgent
|
|
216
228
|
// gives its publisher. Without it no mention from the browser ever
|
|
217
229
|
// resolved: a direct message went to nobody, a mention notified no one.
|
|
218
230
|
resolveMention: (h) => resolveHandle(this, h),
|
|
@@ -370,7 +382,7 @@ export class BrowserAgent {
|
|
|
370
382
|
const rec = await generateKeys();
|
|
371
383
|
rec.mintedFor = this.urls.actor; // one key, one actor (lib/keys.mjs)
|
|
372
384
|
await podState.writeWrappedKeys(this.remote, this.urls, await wrapKeys(rec, password));
|
|
373
|
-
const keys = await cacheOpenedKeys(this.urls
|
|
385
|
+
const keys = await cacheOpenedKeys(podActorOf(this.urls), rec);
|
|
374
386
|
this.publisher.publicKeyPem = keys.rsaPublicPem;
|
|
375
387
|
this.deliverer.rsaPrivate = keys.rsaPrivate;
|
|
376
388
|
await this.publisher.publishProfile();
|
package/web/app/boot.mjs
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { signUp, handleProblem, AP_ROOT } from './signup.mjs';
|
|
13
13
|
import * as podActor from '../../lib/pod/actor.mjs';
|
|
14
14
|
import * as podState from '../../lib/pod/state.mjs';
|
|
15
|
+
import { podBaseOfWebId } from '../../lib/pod/urls.mjs';
|
|
16
|
+
import { podLayout } from '../../lib/pod/root.mjs';
|
|
15
17
|
import { BrowserRemotePod } from './pod-remote.mjs';
|
|
16
18
|
import { beginLogin, completeLogin, getSession, signOut } from './oidc-session.mjs';
|
|
17
19
|
import { unwrapKeys, isKeyEnvelope } from './keystore.mjs';
|
|
@@ -68,7 +70,7 @@ window.fedipodUnlock = async (password) => {
|
|
|
68
70
|
if (!session) throw new Error('Sign in first.');
|
|
69
71
|
// The config on the pod says where this account's state lives; the key sits
|
|
70
72
|
// beside it. Both are read with the session, as the owner.
|
|
71
|
-
const podFromWebId =
|
|
73
|
+
const podFromWebId = podBaseOfWebId(session.webId); // a suffix-based host, or its own host
|
|
72
74
|
const state = `${podFromWebId}${AP_ROOT}ap-state/`;
|
|
73
75
|
// Through the transport rather than the bare session: this is a pod read
|
|
74
76
|
// like any other, and going round it skipped the retry ladder that exists
|
|
@@ -112,21 +114,38 @@ export function parseAddress(input) {
|
|
|
112
114
|
}
|
|
113
115
|
async function issuerForPod(pod) {
|
|
114
116
|
// The pod's actor says where a client signs in (oauthAuthorizationEndpoint's
|
|
115
|
-
// origin); failing that, the account provider is the pod host's parent
|
|
117
|
+
// origin); failing that, the account provider is the pod host's parent
|
|
118
|
+
// domain for a subdomain pod, and the host itself for a pod on a path.
|
|
116
119
|
try {
|
|
117
120
|
const authz = await podActor.readIssuer(`${pod}${AP_ROOT}ap/actor`);
|
|
118
121
|
if (authz) return new URL(authz).origin;
|
|
119
122
|
} catch { /* fall through */ }
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
const u = new URL(pod);
|
|
124
|
+
if (u.pathname !== '/') return u.origin;
|
|
125
|
+
const parent = u.host.split('.').slice(1).join('.');
|
|
126
|
+
return `https://${parent || u.host}`;
|
|
127
|
+
}
|
|
128
|
+
// An address at this site names a fronted identity. Its documents live on a
|
|
129
|
+
// pod this site's WebFinger names as an alias — the pod's own actor id.
|
|
130
|
+
async function podForFrontedAddress(handle) {
|
|
131
|
+
const res = await fetch(`/.well-known/webfinger?resource=${encodeURIComponent(`acct:${handle}@${location.host}`)}`,
|
|
132
|
+
{ headers: { accept: 'application/jrd+json, application/json' } }).catch(() => null);
|
|
133
|
+
if (!res || res.status >= 400) throw new Error(`nobody at this site is called @${handle}@${location.host}`);
|
|
134
|
+
const doc = await res.json().catch(() => ({}));
|
|
135
|
+
const podActorId = (doc.aliases || []).find((a) => /\/ap\/actor$/u.test(String(a)));
|
|
136
|
+
if (!podActorId) throw new Error(`@${handle}@${location.host} lives here but names no pod to sign in to`);
|
|
137
|
+
const tail = `${AP_ROOT}ap/actor`;
|
|
138
|
+
if (!podActorId.endsWith(tail)) throw new Error(`the pod actor ${podActorId} is not where a FediPod pod keeps one`);
|
|
139
|
+
return podActorId.slice(0, -tail.length);
|
|
123
140
|
}
|
|
124
141
|
window.fedipodSignin = async ({ address }) => {
|
|
125
142
|
const parsed = parseAddress(address);
|
|
126
143
|
if (!parsed) throw new Error('Enter your address as @you@yourpod (for example @alice@alice.solidcommunity.net).');
|
|
127
144
|
const bad = handleProblem(parsed.handle);
|
|
128
145
|
if (bad) throw new Error(bad);
|
|
129
|
-
const pod =
|
|
146
|
+
const pod = parsed.host === location.host.toLowerCase()
|
|
147
|
+
? await podForFrontedAddress(parsed.handle)
|
|
148
|
+
: `https://${parsed.host}/`;
|
|
130
149
|
const issuer = await issuerForPod(pod);
|
|
131
150
|
const { authorizationUrl } = await beginLogin({ issuer, redirectUri: REDIRECT });
|
|
132
151
|
location.href = authorizationUrl;
|
|
@@ -262,24 +281,61 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
262
281
|
// --- register form: two screens, pod first then Fediverse identity ---
|
|
263
282
|
const f = () => $('form').elements;
|
|
264
283
|
// The pod provider is a free-text URL; default and normalise to a scheme.
|
|
265
|
-
|
|
284
|
+
// The provider is picked from the list, or typed under "Other…".
|
|
285
|
+
const providerUrl = () => { let v = (f().provider.value || f().providerOther.value).trim(); if (!v) return ''; if (!/^https?:\/\//i.test(v)) v = 'https://' + v; return v; };
|
|
266
286
|
const providerHost = () => { try { return new URL(providerUrl()).host; } catch { return ''; } };
|
|
267
|
-
//
|
|
268
|
-
//
|
|
287
|
+
// A new pod is named by its subdomain under the provider; an existing pod is
|
|
288
|
+
// brought by its address, which may be its own host or a path on a shared one.
|
|
269
289
|
const podHostOf = () => { const sub = f().podName.value.trim().toLowerCase(); const ph = providerHost(); return (sub && ph) ? `${sub}.${ph}` : ''; };
|
|
290
|
+
const podUrl = () => { let v = f().pod.value.trim(); if (!v) return ''; if (!/^https?:\/\//i.test(v)) v = 'https://' + v; if (!v.endsWith('/')) v += '/'; try { return new URL(v).href; } catch { return ''; } };
|
|
291
|
+
// A pod on a suffix-based host cannot answer WebFinger, so its address
|
|
292
|
+
// lives at this site; a pod at its own host root gets the choice.
|
|
293
|
+
const isPathPod = (u) => { try { return new URL(u).pathname !== '/'; } catch { return false; } };
|
|
294
|
+
// Where the chosen provider puts new pods, asked of the provider itself
|
|
295
|
+
// (lib/pod/root.mjs podLayout) and remembered per provider: 'host', 'path',
|
|
296
|
+
// or null when it would not say.
|
|
297
|
+
const layouts = new Map();
|
|
298
|
+
let layout = null;
|
|
299
|
+
const learnLayout = async () => {
|
|
300
|
+
const origin = providerHost() ? new URL(providerUrl()).origin : '';
|
|
301
|
+
if (!origin) { layout = null; return; }
|
|
302
|
+
if (!layouts.has(origin)) layouts.set(origin, podLayout(fetch, origin).catch(() => null));
|
|
303
|
+
const known = await layouts.get(origin);
|
|
304
|
+
if (providerHost() && new URL(providerUrl()).origin === origin) { layout = known; applyShape(); previewAddr(); }
|
|
305
|
+
};
|
|
306
|
+
const pathPod = () => (f().mode.value === 'existing' ? isPathPod(podUrl()) : layout === 'path');
|
|
307
|
+
const shape = () => (pathPod() ? 'front' : f().shape.value);
|
|
270
308
|
const answers = () => {
|
|
271
309
|
const mode = f().mode.value;
|
|
272
310
|
const a = { mode, handle: f().handle.value.trim().toLowerCase(), email: f().email.value.trim(),
|
|
273
|
-
password: f().password.value, issuer: providerUrl() };
|
|
311
|
+
password: f().password.value, issuer: providerUrl(), shape: shape() };
|
|
274
312
|
if (mode === 'new') a.podName = f().podName.value.trim().toLowerCase();
|
|
275
|
-
else a.pod =
|
|
313
|
+
else a.pod = podUrl();
|
|
276
314
|
return a;
|
|
277
315
|
};
|
|
278
316
|
const previewAddr = () => {
|
|
279
|
-
const handle = f().handle.value.trim().toLowerCase();
|
|
280
|
-
|
|
317
|
+
const handle = f().handle.value.trim().toLowerCase();
|
|
318
|
+
const host = shape() === 'front' ? location.host
|
|
319
|
+
: (f().mode.value === 'existing' ? (podUrl() ? new URL(podUrl()).host : '') : podHostOf());
|
|
320
|
+
$('preview').textContent = (handle && host) ? `@${handle}@${host}` : '@…@…';
|
|
321
|
+
};
|
|
322
|
+
// The shape choice is fixed for a path pod, and open for a host-root pod.
|
|
323
|
+
// A path pod has no choice to make: the radios go away and the note says why.
|
|
324
|
+
const applyShape = () => {
|
|
325
|
+
const fixed = pathPod();
|
|
326
|
+
for (const r of f().shape) { if (fixed) r.checked = r.value === 'front'; }
|
|
327
|
+
$('shape-group').hidden = fixed;
|
|
328
|
+
$('shape-hint').hidden = !fixed;
|
|
281
329
|
};
|
|
282
|
-
|
|
330
|
+
const applyMode = () => {
|
|
331
|
+
const existing = f().mode.value === 'existing';
|
|
332
|
+
$('pod-field').hidden = !existing;
|
|
333
|
+
$('podname-field').hidden = existing;
|
|
334
|
+
$('provider-other-field').hidden = f().provider.value !== '';
|
|
335
|
+
};
|
|
336
|
+
for (const el of $('form').elements) for (const evt of ['input', 'change']) el.addEventListener(evt, () => { applyMode(); applyShape(); previewAddr(); });
|
|
337
|
+
for (const evt of ['input', 'change']) { $('provider').addEventListener(evt, learnLayout); $('providerOther').addEventListener(evt, learnLayout); }
|
|
338
|
+
applyMode(); learnLayout();
|
|
283
339
|
|
|
284
340
|
// Step machine: one screen at a time, each gated by its own validation.
|
|
285
341
|
const STEP_IDS = ['step-1', 'step-2'];
|
|
@@ -287,15 +343,19 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
287
343
|
const goStep = (n) => {
|
|
288
344
|
STEP_IDS.forEach((id, i) => { $(id).hidden = i !== n - 1; });
|
|
289
345
|
$('err-1').textContent = ''; $('form-error').textContent = '';
|
|
290
|
-
if (n === 2) previewAddr();
|
|
346
|
+
if (n === 2) { applyShape(); previewAddr(); learnLayout(); }
|
|
291
347
|
if (FOCUS[n]) $(FOCUS[n]).focus();
|
|
292
348
|
};
|
|
293
349
|
const validateStep1 = () => {
|
|
294
|
-
if (!providerHost()) return 'A valid pod provider URL is required.';
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
350
|
+
if (!providerHost()) return f().provider.value === '' ? 'A pod provider address is required under Other….' : 'A valid pod provider URL is required.';
|
|
351
|
+
if (f().mode.value === 'existing') {
|
|
352
|
+
if (!podUrl()) return 'A pod address is required, like https://alice.solidcommunity.net/ or https://server.example/alice/.';
|
|
353
|
+
} else {
|
|
354
|
+
const sub = f().podName.value.trim().toLowerCase();
|
|
355
|
+
if (!sub) return 'A pod username/subdomain is required.';
|
|
356
|
+
const sp = window.fedipodHandleProblem(sub);
|
|
357
|
+
if (sp) return `Pod username: ${sp}`;
|
|
358
|
+
}
|
|
299
359
|
if (!f().email.value.trim()) return 'A pod email is required.';
|
|
300
360
|
if (!f().password.value) return 'A pod password is required.';
|
|
301
361
|
return null;
|
package/web/app/dist/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;
|