fedipod 1.4.1 → 1.6.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 +10 -3
- package/architecture.md +4 -4
- package/cli.md +7 -0
- package/gateway.md +6 -0
- package/groups.md +1 -1
- package/gui.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/installed-agent.md +0 -97
package/lib/pod/urls.mjs
CHANGED
|
@@ -65,6 +65,20 @@ export function apUrls(remotePod, root, { publicBase = null } = {}) {
|
|
|
65
65
|
// here. @name@host is looked up at https://host/.well-known/webfinger, so only
|
|
66
66
|
// a pod that owns the root of its host can answer for one; a pod living at
|
|
67
67
|
// https://server/name/ may publish the document but nothing will ever ask.
|
|
68
|
+
/**
|
|
69
|
+
* The pod a WebID lives in: the URL up to the profile document's container.
|
|
70
|
+
* `https://alice.pod/profile/card#me` → `https://alice.pod/`, and on a shared
|
|
71
|
+
* host `https://server/alice/profile/card#me` → `https://server/alice/`. Taking
|
|
72
|
+
* the origin alone named the wrong pod for the second, which is every pod on a
|
|
73
|
+
* path.
|
|
74
|
+
*/
|
|
75
|
+
export function podBaseOfWebId(webId) {
|
|
76
|
+
const u = new URL(webId);
|
|
77
|
+
u.hash = ''; u.search = '';
|
|
78
|
+
const dir = u.pathname.replace(/profile\/card$/u, '').replace(/[^/]*$/u, '');
|
|
79
|
+
return `${u.origin}${dir.endsWith('/') ? dir : dir + '/'}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
export function webfingerHost(podUrl) {
|
|
69
83
|
const u = new URL(podUrl);
|
|
70
84
|
return u.pathname === '/' ? u.host : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fedipod",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Standalone single-actor ActivityPub agent whose wire face, RDF truth and state all live on a Solid pod (CSS). Bundles a Phanpy UI and a Mastodon client-API facade.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -130,9 +130,20 @@ button:disabled { opacity: .5; cursor: default; }
|
|
|
130
130
|
<label for="issuer-new">Solid pod provider</label>
|
|
131
131
|
<select id="issuer-new" name="issuerNew" aria-describedby="issuer-new-hint">
|
|
132
132
|
<option value="https://solidcommunity.net">solidcommunity.net</option>
|
|
133
|
+
<option value="https://privatedatapod.com">privatedatapod.com</option>
|
|
134
|
+
<option value="https://solid.redpencil.io">redpencil.io</option>
|
|
135
|
+
<option value="https://pods.solidcommunity.au">solidcommunity.au</option>
|
|
136
|
+
<option value="https://solidweb.me">solidweb.me</option>
|
|
137
|
+
<option value="https://teamid.live">teamid.live</option>
|
|
138
|
+
<option value="">Other…</option>
|
|
133
139
|
</select>
|
|
134
|
-
<
|
|
135
|
-
|
|
140
|
+
<div id="row-issuer-other" hidden>
|
|
141
|
+
<label for="issuerOther">Provider address</label>
|
|
142
|
+
<input type="url" id="issuerOther" name="issuerOther" autocomplete="url" placeholder="https://pods.example.org">
|
|
143
|
+
</div>
|
|
144
|
+
<p class="hint" id="issuer-new-hint">The Community Solid Server providers from
|
|
145
|
+
solidproject.org. A provider that puts pods on paths gives your address at a
|
|
146
|
+
gateway instead.</p>
|
|
136
147
|
</div>
|
|
137
148
|
|
|
138
149
|
<div id="row-issuer-existing" hidden>
|
|
@@ -170,6 +181,18 @@ button:disabled { opacity: .5; cursor: default; }
|
|
|
170
181
|
Wrong account or pod?
|
|
171
182
|
<button type="button" id="form-reenter" title=" Discard the credential and enter the account and pod again">Re-enter credentials</button></p>
|
|
172
183
|
|
|
184
|
+
<fieldset id="fs-shape" hidden>
|
|
185
|
+
<legend>Where your address lives</legend>
|
|
186
|
+
<label class="choice"><input type="radio" name="shape" value="pod" checked> On your pod</label>
|
|
187
|
+
<label class="choice"><input type="radio" name="shape" value="front"> At a gateway</label>
|
|
188
|
+
<div id="row-gateway" hidden>
|
|
189
|
+
<label for="gatewayOrigin">Gateway</label>
|
|
190
|
+
<input type="url" id="gatewayOrigin" name="gatewayOrigin" value="https://fedipod.net" autocomplete="url">
|
|
191
|
+
<p class="hint">The gateway answers your address and forwards your mail to your pod.</p>
|
|
192
|
+
</div>
|
|
193
|
+
<p class="hint" id="shape-note" hidden></p>
|
|
194
|
+
</fieldset>
|
|
195
|
+
|
|
173
196
|
<h2>You will be</h2>
|
|
174
197
|
<p class="address" id="preview">…</p>
|
|
175
198
|
<div id="preview-notes"></div>
|
package/web/admin/setup/setup.js
CHANGED
|
@@ -93,12 +93,18 @@ function answers() {
|
|
|
93
93
|
const f = $('form').elements;
|
|
94
94
|
const kind = f.kind.value;
|
|
95
95
|
const mode = state.resumable ? 'existing' : f.mode.value;
|
|
96
|
+
const issuer = mode === 'new'
|
|
97
|
+
? (f.issuerNew.value || (f.issuerOther ? f.issuerOther.value : '')).trim()
|
|
98
|
+
: f.issuer.value.trim();
|
|
96
99
|
const a = {
|
|
97
100
|
kind,
|
|
98
101
|
mode,
|
|
99
102
|
handle: f.handle.value.trim(),
|
|
100
|
-
issuer
|
|
103
|
+
issuer,
|
|
101
104
|
email: f.email.value.trim(),
|
|
105
|
+
// Where the address lives; a group cannot front (yet), so it stays 'pod'.
|
|
106
|
+
shape: (kind === 'group') ? 'pod' : (f.shape ? f.shape.value : 'pod'),
|
|
107
|
+
gatewayOrigin: (f.gatewayOrigin ? f.gatewayOrigin.value.trim() : '') || 'https://fedipod.net',
|
|
102
108
|
};
|
|
103
109
|
if (mode === 'new') a.podName = f.podName.value.trim() || a.handle;
|
|
104
110
|
else a.pod = f.pod.value.trim();
|
|
@@ -128,6 +134,9 @@ function onEdit() {
|
|
|
128
134
|
$('row-pod').hidden = mode === 'new';
|
|
129
135
|
$('row-issuer-new').hidden = mode !== 'new';
|
|
130
136
|
$('row-issuer-existing').hidden = mode === 'new';
|
|
137
|
+
const f = $('form').elements;
|
|
138
|
+
if ($('row-issuer-other')) $('row-issuer-other').hidden = !(mode === 'new' && f.issuerNew.value === '');
|
|
139
|
+
if ($('row-gateway')) $('row-gateway').hidden = !(f.shape && f.shape.value === 'front');
|
|
131
140
|
clearTimeout(editTimer);
|
|
132
141
|
editTimer = setTimeout(preview, 150);
|
|
133
142
|
}
|
|
@@ -140,6 +149,26 @@ async function preview() {
|
|
|
140
149
|
const { json } = await postJson('/setup/check', a);
|
|
141
150
|
if (!json) return;
|
|
142
151
|
$('preview').textContent = json.address || '…';
|
|
152
|
+
// The address-shape choice: hidden for a group (a group cannot front yet),
|
|
153
|
+
// locked to the gateway for a pod on a suffix-based host, an open choice
|
|
154
|
+
// for a pod at its own host.
|
|
155
|
+
const f = $('form').elements;
|
|
156
|
+
const shapeFs = $('fs-shape');
|
|
157
|
+
if (shapeFs) {
|
|
158
|
+
const isGroup = a.kind === 'group';
|
|
159
|
+
shapeFs.hidden = isGroup || (a.mode === 'existing' && !a.pod);
|
|
160
|
+
const forced = !!json.forced;
|
|
161
|
+
for (const r of f.shape) r.disabled = forced;
|
|
162
|
+
if (forced) { for (const r of f.shape) r.checked = r.value === 'front'; }
|
|
163
|
+
if ($('row-gateway')) $('row-gateway').hidden = !(f.shape.value === 'front');
|
|
164
|
+
const note = $('shape-note');
|
|
165
|
+
if (note) {
|
|
166
|
+
note.hidden = !forced;
|
|
167
|
+
note.textContent = forced
|
|
168
|
+
? 'Your pod is on a suffix-based host, so its address lives at the gateway. Your posts, key and data stay on your pod.'
|
|
169
|
+
: '';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
143
172
|
const notes = $('preview-notes');
|
|
144
173
|
for (const w of json.warnings || []) {
|
|
145
174
|
const p = document.createElement('p');
|
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;
|