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/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/keys-browser.mjs
CHANGED
|
@@ -44,6 +44,10 @@ export async function cacheOpenedKeys(actorUrl, keysRecord) {
|
|
|
44
44
|
// already relies on. It is what lets the worker boot itself after an idle kill
|
|
45
45
|
// without a soul around to type anything.
|
|
46
46
|
export const keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
|
|
47
|
+
// The cache is keyed by the POD actor, whatever the identity advertises: a
|
|
48
|
+
// fronted identity's advertised actor is the gateway's, and keying by that
|
|
49
|
+
// would hide the opened copy sign-up stored and ask for the password again.
|
|
50
|
+
export const podActorOf = (urls) => (urls.toPod ? urls.toPod(urls.actor) : urls.actor);
|
|
47
51
|
|
|
48
52
|
// Read the signing key for this actor and import it for signing.
|
|
49
53
|
//
|
|
@@ -54,16 +58,17 @@ export const keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
|
|
|
54
58
|
// there is from before wrapping and is used as it stands, while an envelope
|
|
55
59
|
// needs the password and so has to go back to the page.
|
|
56
60
|
export async function loadKeysFromPod(remote, urls) {
|
|
57
|
-
const
|
|
61
|
+
const podActor = podActorOf(urls);
|
|
62
|
+
const cached = await kvGet(keyCacheKey(podActor)).catch(() => null);
|
|
58
63
|
if (isOpenedKey(cached)) return fromCache(cached);
|
|
59
64
|
// A copy stored as PEM by an earlier build: import it and store the key form
|
|
60
65
|
// in its place, so the PEM is gone from storage after one boot.
|
|
61
|
-
if (cached?.rsa?.privatePem) return cacheOpenedKeys(
|
|
66
|
+
if (cached?.rsa?.privatePem) return cacheOpenedKeys(podActor, cached);
|
|
62
67
|
|
|
63
68
|
const doc = await podState.readWrappedKeys(remote, urls);
|
|
64
69
|
if (isKeyEnvelope(doc)) throw new KeyPasswordNeeded();
|
|
65
70
|
if (!doc || !doc.rsa) throw new Error('no signing key on the pod — sign up did not finish');
|
|
66
71
|
// A pre-wrapping install. Cache it so the next boot is one read, and leave
|
|
67
72
|
// the pod's copy alone: re-wrapping it would need the password we do not have.
|
|
68
|
-
return cacheOpenedKeys(
|
|
73
|
+
return cacheOpenedKeys(podActor, doc);
|
|
69
74
|
}
|
package/web/app/signup.mjs
CHANGED
|
@@ -48,9 +48,24 @@ const progressKey = (a) => [a.issuer, a.mode, a.handle, a.mode === 'new' ? (a.po
|
|
|
48
48
|
* draw the same tick list the CLI setup shows. Resumable: see PROGRESS above.
|
|
49
49
|
*
|
|
50
50
|
* answers: { mode:'new'|'existing', issuer, email, password, handle,
|
|
51
|
-
* podName?, pod?, gateway
|
|
51
|
+
* podName?, pod?, gateway?, shape?: 'pod'|'front' }
|
|
52
|
+
*
|
|
53
|
+
* `shape` is where the address lives. On the pod, `@handle@yourpod`, with the
|
|
54
|
+
* gateway as a mail door only — the default. At the gateway, `@handle@front`,
|
|
55
|
+
* a fronted identity whose documents still live on the pod. A pod on a path of
|
|
56
|
+
* a suffix-based host cannot answer WebFinger, so it is fronted whatever was asked.
|
|
57
|
+
*
|
|
52
58
|
* returns: { credential, config, actorUrl, address, keysPublic }
|
|
53
59
|
*/
|
|
60
|
+
/** A fronted name is one per gateway; a taken one is refused before anything is made. */
|
|
61
|
+
async function assertFrontNameFree(frontOrigin, handle) {
|
|
62
|
+
const res = await fetch(`${frontOrigin.replace(/\/$/, '')}/api/handle?handle=${encodeURIComponent(handle)}`,
|
|
63
|
+
{ headers: { accept: 'application/json' } }).catch(() => null);
|
|
64
|
+
const d = res ? await res.json().catch(() => ({})) : null;
|
|
65
|
+
if (!d) throw new Error(`${new URL(frontOrigin).host} did not answer whether @${handle} is free`);
|
|
66
|
+
if (!d.available) throw new Error(d.reason || `the name @${handle}@${new URL(frontOrigin).host} is taken — choose another handle`);
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
export async function signUp(answers, { onStep = () => {}, frontOrigin = null } = {}) {
|
|
55
70
|
const { mode, issuer, email, password, handle } = answers;
|
|
56
71
|
const bad = handleProblem(handle);
|
|
@@ -58,6 +73,10 @@ export async function signUp(answers, { onStep = () => {}, frontOrigin = null }
|
|
|
58
73
|
if (!email) throw new Error('an email is required'); // recovery + account login
|
|
59
74
|
if (!password) throw new Error('a password is required');
|
|
60
75
|
if (mode === 'existing' && !answers.pod) throw new Error('a pod address is required');
|
|
76
|
+
const wantsFront = answers.shape === 'front';
|
|
77
|
+
if (wantsFront && !frontOrigin) throw new Error('an address at the gateway needs a gateway, and this page has none');
|
|
78
|
+
// A name at the gateway is one per gateway: settle it before making anything.
|
|
79
|
+
if (wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
61
80
|
|
|
62
81
|
const key = progressKey(answers);
|
|
63
82
|
const prog = PROGRESS.get(key) || {};
|
|
@@ -91,17 +110,18 @@ export async function signUp(answers, { onStep = () => {}, frontOrigin = null }
|
|
|
91
110
|
}
|
|
92
111
|
const pod = prog.pod; const webId = prog.webId || null;
|
|
93
112
|
|
|
94
|
-
// WebFinger is answered at a host root
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
+ 'Use a pod that is the root of its own subdomain.');
|
|
113
|
+
// WebFinger is answered only at a host root. A pod on a path of a shared
|
|
114
|
+
// host therefore takes its address at the gateway, whatever was asked; a
|
|
115
|
+
// pod at its own root keeps the choice made on the form.
|
|
116
|
+
const pathPod = new URL(pod).pathname !== '/';
|
|
117
|
+
const fronted = pathPod || wantsFront;
|
|
118
|
+
if (fronted && !frontOrigin) {
|
|
119
|
+
throw new Error(`${pod} is a suffix-based host, so its address must live at a gateway, and this page has none.`);
|
|
102
120
|
}
|
|
121
|
+
if (pathPod && !wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
103
122
|
|
|
104
|
-
const actorUrl = actorUrlFor(pod);
|
|
123
|
+
const actorUrl = actorUrlFor(pod); // where the documents live, always
|
|
124
|
+
const frontActor = fronted ? `${frontOrigin.replace(/\/$/, '')}/u/${handle}/ap/actor` : null;
|
|
105
125
|
|
|
106
126
|
// --- credential --- (a fresh attempt re-mints; a resumed one reuses it)
|
|
107
127
|
const cred = step('credential');
|
|
@@ -143,12 +163,48 @@ export async function signUp(answers, { onStep = () => {}, frontOrigin = null }
|
|
|
143
163
|
// The opened copy is kept in this browser (IndexedDB) so the worker can boot
|
|
144
164
|
// itself after an idle kill with nobody there to type a password. A browser
|
|
145
165
|
// that has no copy asks for the password once — see boot.mjs.
|
|
166
|
+
// Connect the gateway (fedipod.net) before the key is made, because a
|
|
167
|
+
// fronted identity's key is stamped with the gateway actor. Inbox-only: the
|
|
168
|
+
// actor advertises the door as its inbox and keeps its own ids. Fronted: the
|
|
169
|
+
// gateway answers WebFinger for @handle@front and serves the actor at its own
|
|
170
|
+
// address, rewriting reads onto the pod. Either way the door verifies each
|
|
171
|
+
// delivery and forwards the clean mail to the pod; the agent trusts the
|
|
172
|
+
// door's receipt. The same DPoP session proves the pod to the gateway — no
|
|
173
|
+
// password reaches it.
|
|
174
|
+
let gateway = answers.gateway || prog.gateway || null;
|
|
175
|
+
if (frontOrigin && !gateway) {
|
|
176
|
+
const gw = step('gateway');
|
|
177
|
+
gw.running(fronted ? `taking your address at ${new URL(frontOrigin).host}` : `connecting your mail door on ${new URL(frontOrigin).host}`);
|
|
178
|
+
const res = await session.fetch(`${frontOrigin.replace(/\/$/, '')}/api/attach`, {
|
|
179
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
180
|
+
// podHome is the AP CONTAINER, not the pod root: the front builds the
|
|
181
|
+
// delivery target as `podHome + 'ap/inbox/'` (lib/front-core.mjs), so a
|
|
182
|
+
// bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
|
|
183
|
+
// the container the agent drains, where nothing would ever read it. The
|
|
184
|
+
// manage surface has always sent `urls.home`; this is the same value.
|
|
185
|
+
body: JSON.stringify({ handle, podHome: `${pod}${AP_ROOT}`, actorUrl, kind: 'person', fronted }),
|
|
186
|
+
});
|
|
187
|
+
const d = await res.json().catch(() => ({}));
|
|
188
|
+
if (res.status !== 201 || !d.hmacSecret) {
|
|
189
|
+
throw new Error(`could not connect the gateway (HTTP ${res.status}): ${d.error || ''}`);
|
|
190
|
+
}
|
|
191
|
+
gateway = fronted
|
|
192
|
+
? { url: `${frontOrigin.replace(/\/$/, '')}/u/${handle}/ap/inbox/`, frontActor: String(d.frontActor || frontActor), hmacSecret: d.hmacSecret, mode: 'trust' }
|
|
193
|
+
: { url: d.doorInbox, hmacSecret: d.hmacSecret, mode: 'trust' };
|
|
194
|
+
prog.gateway = gateway;
|
|
195
|
+
gw.ok();
|
|
196
|
+
} else if (frontOrigin && gateway) {
|
|
197
|
+
step('gateway').ok(); // resumed
|
|
198
|
+
}
|
|
199
|
+
|
|
146
200
|
const keysStep = step('keys');
|
|
147
201
|
let keys;
|
|
148
202
|
if (!prog.keysStored) {
|
|
149
203
|
keysStep.running('making your signing key and locking it under your password');
|
|
150
204
|
keys = await generateKeys();
|
|
151
|
-
|
|
205
|
+
// One key, one actor (lib/keys.mjs): the actor the world knows, which for
|
|
206
|
+
// a fronted identity is the gateway's address for it.
|
|
207
|
+
keys.mintedFor = gateway?.frontActor || actorUrl;
|
|
152
208
|
const remote = new BrowserRemotePod(session, { webId: credential.webId, role: 'signup', log: () => {} });
|
|
153
209
|
// Owner-only, and THEN the key — one operation, so the order cannot be got
|
|
154
210
|
// wrong here or anywhere else. A pod that refuses the ACL write is not a
|
|
@@ -174,34 +230,6 @@ export async function signUp(answers, { onStep = () => {}, frontOrigin = null }
|
|
|
174
230
|
keysStep.ok();
|
|
175
231
|
}
|
|
176
232
|
|
|
177
|
-
// Connect the mail door: attach to the gateway (fedipod.net), inbox-only, so
|
|
178
|
-
// the actor advertises the door as its inbox. The door verifies each delivery
|
|
179
|
-
// and forwards the clean mail to the pod; the agent trusts the door's receipt.
|
|
180
|
-
// The same DPoP session proves the pod to the gateway — no password reaches it.
|
|
181
|
-
let gateway = answers.gateway || prog.gateway || null;
|
|
182
|
-
if (frontOrigin && !gateway) {
|
|
183
|
-
const gw = step('gateway');
|
|
184
|
-
gw.running(`connecting your mail door on ${new URL(frontOrigin).host}`);
|
|
185
|
-
const res = await session.fetch(`${frontOrigin.replace(/\/$/, '')}/api/attach`, {
|
|
186
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
187
|
-
// podHome is the AP CONTAINER, not the pod root: the front builds the
|
|
188
|
-
// delivery target as `podHome + 'ap/inbox/'` (lib/front-core.mjs), so a
|
|
189
|
-
// bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
|
|
190
|
-
// the container the agent drains, where nothing would ever read it. The
|
|
191
|
-
// manage surface has always sent `urls.home`; this is the same value.
|
|
192
|
-
body: JSON.stringify({ handle, podHome: `${pod}${AP_ROOT}`, actorUrl, kind: 'person' }),
|
|
193
|
-
});
|
|
194
|
-
const d = await res.json().catch(() => ({}));
|
|
195
|
-
if (res.status !== 201 || !d.hmacSecret) {
|
|
196
|
-
throw new Error(`could not connect the mail door (HTTP ${res.status}): ${d.error || ''}`);
|
|
197
|
-
}
|
|
198
|
-
gateway = { url: d.doorInbox, hmacSecret: d.hmacSecret, mode: 'trust' };
|
|
199
|
-
prog.gateway = gateway;
|
|
200
|
-
gw.ok();
|
|
201
|
-
} else if (frontOrigin && gateway) {
|
|
202
|
-
step('gateway').ok(); // resumed
|
|
203
|
-
}
|
|
204
|
-
|
|
205
233
|
const config = {
|
|
206
234
|
remotePod: pod, root: AP_ROOT, handle, name: handle, issuer: credential.issuerOrigin,
|
|
207
235
|
...(gateway ? { gateway } : {}),
|
|
@@ -233,7 +261,7 @@ export async function signUp(answers, { onStep = () => {}, frontOrigin = null }
|
|
|
233
261
|
|
|
234
262
|
PROGRESS.delete(key); // finished — nothing left to resume
|
|
235
263
|
|
|
236
|
-
const host = new URL(pod).host;
|
|
264
|
+
const host = gateway?.frontActor ? new URL(gateway.frontActor).host : new URL(pod).host;
|
|
237
265
|
return {
|
|
238
266
|
credential, config, actorUrl,
|
|
239
267
|
address: `@${handle}@${host}`,
|
|
@@ -132,9 +132,20 @@ button:disabled { opacity: .5; cursor: default; }
|
|
|
132
132
|
<label for="issuer-new">Solid pod provider</label>
|
|
133
133
|
<select id="issuer-new" name="issuerNew" aria-describedby="issuer-new-hint">
|
|
134
134
|
<option value="https://solidcommunity.net">solidcommunity.net</option>
|
|
135
|
+
<option value="https://privatedatapod.com">privatedatapod.com</option>
|
|
136
|
+
<option value="https://solid.redpencil.io">redpencil.io</option>
|
|
137
|
+
<option value="https://pods.solidcommunity.au">solidcommunity.au</option>
|
|
138
|
+
<option value="https://solidweb.me">solidweb.me</option>
|
|
139
|
+
<option value="https://teamid.live">teamid.live</option>
|
|
140
|
+
<option value="">Other…</option>
|
|
135
141
|
</select>
|
|
136
|
-
<
|
|
137
|
-
|
|
142
|
+
<div id="row-issuer-other" hidden>
|
|
143
|
+
<label for="issuerOther">Provider address</label>
|
|
144
|
+
<input type="url" id="issuerOther" name="issuerOther" autocomplete="url" placeholder="https://pods.example.org">
|
|
145
|
+
</div>
|
|
146
|
+
<p class="hint" id="issuer-new-hint">The Community Solid Server providers from
|
|
147
|
+
solidproject.org. A provider that puts pods on paths gives your address at a
|
|
148
|
+
gateway instead.</p>
|
|
138
149
|
</div>
|
|
139
150
|
|
|
140
151
|
<div id="row-issuer-existing" hidden>
|
|
@@ -172,6 +183,18 @@ button:disabled { opacity: .5; cursor: default; }
|
|
|
172
183
|
Wrong account or pod?
|
|
173
184
|
<button type="button" id="form-reenter" title=" Discard the credential and enter the account and pod again">Re-enter credentials</button></p>
|
|
174
185
|
|
|
186
|
+
<fieldset id="fs-shape" hidden>
|
|
187
|
+
<legend>Where your address lives</legend>
|
|
188
|
+
<label class="choice"><input type="radio" name="shape" value="pod" checked> On your pod</label>
|
|
189
|
+
<label class="choice"><input type="radio" name="shape" value="front"> At a gateway</label>
|
|
190
|
+
<div id="row-gateway" hidden>
|
|
191
|
+
<label for="gatewayOrigin">Gateway</label>
|
|
192
|
+
<input type="url" id="gatewayOrigin" name="gatewayOrigin" value="https://fedipod.net" autocomplete="url">
|
|
193
|
+
<p class="hint">The gateway answers your address and forwards your mail to your pod.</p>
|
|
194
|
+
</div>
|
|
195
|
+
<p class="hint" id="shape-note" hidden></p>
|
|
196
|
+
</fieldset>
|
|
197
|
+
|
|
175
198
|
<h2>You will be</h2>
|
|
176
199
|
<p class="address" id="preview">…</p>
|
|
177
200
|
<div id="preview-notes"></div>
|
|
@@ -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');
|