fedipod-server 0.28.0 → 0.29.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 +27 -28
- package/config/server.json +22 -0
- package/dist/claims.js +3 -2
- package/dist/directory.d.ts +2 -0
- package/dist/handler.d.ts +4 -1
- package/dist/handler.js +32 -3
- package/dist/handler.jsonld +4 -0
- package/dist/store-pod.js +37 -0
- package/lib/client/c2s.mjs +97 -43
- package/lib/client/masto/accounts.mjs +1 -0
- package/lib/connections/bskyfeed.mjs +6 -0
- package/lib/core/deliver.mjs +68 -22
- package/lib/core/intake/activities.mjs +18 -2
- package/lib/core/intake/group.mjs +3 -1
- package/lib/core/intake/index.mjs +50 -6
- package/lib/core/intake/notes.mjs +12 -5
- package/lib/core/lease.mjs +7 -7
- package/lib/core/place.mjs +82 -0
- package/lib/core/publisher/collections.mjs +37 -2
- package/lib/core/publisher/index.mjs +23 -1
- package/lib/core/publisher/notes.mjs +73 -7
- package/lib/core/publisher/own.mjs +143 -0
- package/lib/core/publisher/questions.mjs +5 -3
- package/lib/core/social.mjs +62 -32
- package/lib/core/store.mjs +36 -6
- package/lib/core/wire.mjs +51 -15
- package/lib/device/admin/routes/lifecycle.mjs +1 -1
- package/lib/device/admin/routes/setup.mjs +17 -1
- package/lib/device/cli/commands/setup.mjs +50 -8
- package/lib/device/cli/context.mjs +1 -1
- package/lib/device/migrate.mjs +1 -1
- package/lib/device/setup.mjs +45 -8
- package/lib/gateway/caches.mjs +36 -0
- package/lib/gateway/front-core.mjs +80 -111
- package/lib/gateway/gateway-core.mjs +91 -9
- package/lib/gateway/headers.mjs +49 -0
- package/lib/gateway/quiet.mjs +7 -2
- package/lib/gateway/relay-extras.mjs +89 -0
- package/lib/gateway/token-claims.mjs +16 -0
- package/lib/pod/containers.mjs +5 -1
- package/lib/pod/inbox.mjs +22 -3
- package/lib/pod/location.mjs +52 -0
- package/lib/pod/notes.mjs +2 -4
- package/lib/pod/transport.mjs +207 -25
- package/lib/pod/type-index.mjs +101 -0
- package/lib/pod/urls.mjs +6 -0
- package/lib/server/embed.mjs +7 -6
- package/lib/session/README.md +14 -0
- package/lib/session/fedi-account.mjs +73 -2
- package/lib/session/package.json +10 -2
- package/package.json +1 -1
- package/run-agent.mjs +18 -0
- package/web/admin/actors.js +2 -0
- package/web/admin/index.html +9 -0
- package/web/admin/record.js +4 -1
- package/web/admin/setup/index.html +17 -1
- package/web/admin/setup/setup.js +18 -5
- package/web/app/README.md +1 -1
- package/web/app/admin-facade.mjs +1 -1
- package/web/app/agent.mjs +24 -10
- package/web/app/boot.mjs +72 -32
- package/web/app/deliver-relay.mjs +47 -7
- package/web/app/dist/boot.js +466 -89
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +2792 -1996
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/index.html +16 -0
- package/web/app/signup.mjs +65 -26
- package/web/app/update.js +4 -0
- package/web/front/run.html +7 -1
- package/web/front/run.js +30 -4
package/web/app/index.html
CHANGED
|
@@ -191,6 +191,13 @@ a { color: var(--accent); }
|
|
|
191
191
|
<span id="signed-pod-label" class="grouplabel">Your pod</span>
|
|
192
192
|
<p id="signed-pod" role="status" aria-labelledby="signed-pod-label"></p>
|
|
193
193
|
|
|
194
|
+
<div id="container-field">
|
|
195
|
+
<label for="container">Store your data in a container named fedipod, inside this container</label>
|
|
196
|
+
<input type="text" id="container" name="container" autocomplete="off" spellcheck="false"
|
|
197
|
+
aria-describedby="container-hint">
|
|
198
|
+
<p class="hint" id="container-hint">Your data will be at <span id="place">…</span>. This is permanent.</p>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
194
201
|
<!-- Shown when the pod's account lives at another gateway: this is a
|
|
195
202
|
move, not a new account. -->
|
|
196
203
|
<p id="movein-note" hidden>Moving <strong id="movein-from"></strong> to this site.
|
|
@@ -217,6 +224,15 @@ a { color: var(--accent); }
|
|
|
217
224
|
<p class="hint">The handle and the pod cannot be changed afterwards. Everything else — display
|
|
218
225
|
name, bio, pictures — you set later in the client.</p>
|
|
219
226
|
<p class="err" id="form-error" role="alert"></p>
|
|
227
|
+
<div id="index-ask" role="alertdialog" aria-labelledby="index-ask-text" hidden>
|
|
228
|
+
<p id="index-ask-text">Your pod has no public type index, the list Solid apps use to find your
|
|
229
|
+
things. FediPod needs one to record where your account lives. Create it? It will be readable
|
|
230
|
+
by anyone, like your profile.</p>
|
|
231
|
+
<p>
|
|
232
|
+
<button type="button" id="index-no">No, stop</button>
|
|
233
|
+
<button type="button" class="primary" id="index-yes">Create it and continue</button>
|
|
234
|
+
</p>
|
|
235
|
+
</div>
|
|
220
236
|
<p>
|
|
221
237
|
<button type="button" id="back-1">Back</button>
|
|
222
238
|
<button type="submit" class="primary" id="submit">Create your Fediverse account</button>
|
package/web/app/signup.mjs
CHANGED
|
@@ -16,14 +16,18 @@ import * as podState from '../../lib/pod/state.mjs';
|
|
|
16
16
|
import { resourceExists } from '../../lib/pod/root.mjs';
|
|
17
17
|
import { podBaseOfWebId } from '../../lib/pod/urls.mjs';
|
|
18
18
|
import { cacheOpenedKeys } from './keys-browser.mjs';
|
|
19
|
+
import { chosenRoot, findAccount, recordPlace, hasPublicIndex } from '../../lib/core/place.mjs';
|
|
19
20
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const
|
|
21
|
+
// Everything the agent publishes hangs under a container named `fedipod`, in
|
|
22
|
+
// the container its owner chose (lib/core/place.mjs). The root is stored on the
|
|
23
|
+
// config, so the agent reads it rather than guessing, and the place is recorded
|
|
24
|
+
// in the owner's public type index, which is how the account is found again.
|
|
25
|
+
const actorUrlFor = (pod, root) => `${pod}${root}ap/actor`;
|
|
26
|
+
const keysDocFor = (pod, root) => `${pod}${root}ap-state/keys.json`;
|
|
27
|
+
const stateOf = (pod, root) => `${pod}${root}ap-state/`;
|
|
28
|
+
|
|
29
|
+
/** The account's config as it sits under `root`, read as its owner. */
|
|
30
|
+
const readConfigAt = (remote, pod) => (root) => podState.readConfig(remote, { state: stateOf(pod, root) });
|
|
27
31
|
|
|
28
32
|
const HANDLE_RE = /^[a-z0-9-]{2,30}$/;
|
|
29
33
|
|
|
@@ -42,7 +46,7 @@ export function handleProblem(handle) {
|
|
|
42
46
|
// identity being built, so changing the handle on the form starts a clean
|
|
43
47
|
// attempt.
|
|
44
48
|
const PROGRESS = new Map();
|
|
45
|
-
const progressKey = (webId, a) => [webId, a.handle, a.shape || 'pod'].join('|');
|
|
49
|
+
const progressKey = (webId, a) => [webId, a.handle, a.shape || 'pod', a.container || ''].join('|');
|
|
46
50
|
|
|
47
51
|
/** A fronted name is one per gateway; a taken one is refused before anything is made. */
|
|
48
52
|
async function assertFrontNameFree(frontOrigin, handle) {
|
|
@@ -58,7 +62,12 @@ async function assertFrontNameFree(frontOrigin, handle) {
|
|
|
58
62
|
* note) so a page can draw the same tick list the CLI setup shows. Resumable:
|
|
59
63
|
* see PROGRESS above.
|
|
60
64
|
*
|
|
61
|
-
* answers: { handle, shape?: 'pod'|'front', gateway? }
|
|
65
|
+
* answers: { handle, shape?: 'pod'|'front', gateway?, container?, createIndex? }
|
|
66
|
+
* container the path of the container that holds `fedipod/` (the pod root
|
|
67
|
+
* when empty)
|
|
68
|
+
* createIndex the person's yes to making a public type index when their
|
|
69
|
+
* profile names none; without it, sign-up stops before writing
|
|
70
|
+
* anything and says so (error code 'needs-index')
|
|
62
71
|
* session: { webId, issuer, fetch } — the Solid-OIDC session from the pod's
|
|
63
72
|
* own login (oidc-session.mjs). The pod is where the WebID lives.
|
|
64
73
|
*
|
|
@@ -91,11 +100,27 @@ export async function signUp(answers, { session, onStep = () => {}, frontOrigin
|
|
|
91
100
|
skip: (note) => onStep(key, 'skipped', note),
|
|
92
101
|
});
|
|
93
102
|
|
|
103
|
+
const place = chosenRoot(pod, answers.container);
|
|
104
|
+
if (place.problem) throw new Error(`Where to store it: ${place.problem}.`);
|
|
105
|
+
const root = place.root;
|
|
106
|
+
const remote = new BrowserRemotePod(session, { webId, role: 'signup', log: () => {} });
|
|
107
|
+
|
|
94
108
|
// --- the pod --- (the session came from it; what is checked is that it has no account yet)
|
|
95
109
|
const podStep = step('pod');
|
|
96
110
|
if (!prog.pod) {
|
|
97
111
|
podStep.running('checking your pod');
|
|
98
|
-
if (await
|
|
112
|
+
if (await findAccount(remote, pod, readConfigAt(remote, pod))
|
|
113
|
+
|| await resourceExists(session.fetch, actorUrlFor(pod, root))) {
|
|
114
|
+
throw new Error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
|
|
115
|
+
}
|
|
116
|
+
if (await resourceExists(session.fetch, `${pod}${root}`)) {
|
|
117
|
+
throw new Error(`${new URL(pod + root).pathname} is already there and is not a FediPod account — choose another container.`);
|
|
118
|
+
}
|
|
119
|
+
// Where the account lives is recorded in the public type index. With none,
|
|
120
|
+
// the person is asked first; nothing is written before they have said yes.
|
|
121
|
+
if (!answers.createIndex && !await hasPublicIndex(remote, pod)) {
|
|
122
|
+
throw Object.assign(new Error('Your profile names no public type index.'), { code: 'needs-index' });
|
|
123
|
+
}
|
|
99
124
|
prog.pod = pod;
|
|
100
125
|
podStep.ok(pod);
|
|
101
126
|
} else {
|
|
@@ -108,11 +133,11 @@ export async function signUp(answers, { session, onStep = () => {}, frontOrigin
|
|
|
108
133
|
const pathPod = new URL(pod).pathname !== '/';
|
|
109
134
|
const fronted = pathPod || wantsFront;
|
|
110
135
|
if (fronted && !frontOrigin) {
|
|
111
|
-
throw new Error(`${pod} is a
|
|
136
|
+
throw new Error(`${pod} is a suffixed pod, so its address must live at a gateway, and this page has none.`);
|
|
112
137
|
}
|
|
113
138
|
if (pathPod && !wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
114
139
|
|
|
115
|
-
const actorUrl = actorUrlFor(pod);
|
|
140
|
+
const actorUrl = actorUrlFor(pod, root); // where the documents live, always
|
|
116
141
|
const frontActor = fronted ? `${frontOrigin.replace(/\/$/, '')}/u/${handle}/ap/actor` : null;
|
|
117
142
|
|
|
118
143
|
// Connect the gateway (fedipod.net) before the key is made, because a
|
|
@@ -133,7 +158,7 @@ export async function signUp(answers, { session, onStep = () => {}, frontOrigin
|
|
|
133
158
|
// bare pod root sends this identity's mail to <pod>/ap/inbox/ — outside
|
|
134
159
|
// the container the agent drains, where nothing would ever read it. The
|
|
135
160
|
// manage surface has always sent `urls.home`; this is the same value.
|
|
136
|
-
body: JSON.stringify({ handle, podHome: `${pod}${
|
|
161
|
+
body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl, kind: 'person', fronted }),
|
|
137
162
|
});
|
|
138
163
|
const d = await res.json().catch(() => ({}));
|
|
139
164
|
if (res.status !== 201 || !d.hmacSecret) {
|
|
@@ -163,7 +188,6 @@ export async function signUp(answers, { session, onStep = () => {}, frontOrigin
|
|
|
163
188
|
// The opened copy is kept in this browser (IndexedDB) so the worker can boot
|
|
164
189
|
// itself after an idle kill with no read. A browser that has no copy reads
|
|
165
190
|
// the pod's with its session — see keys-browser.mjs.
|
|
166
|
-
const remote = new BrowserRemotePod(session, { webId, role: 'signup', log: () => {} });
|
|
167
191
|
const keysStep = step('keys');
|
|
168
192
|
let keys;
|
|
169
193
|
if (!prog.keysStored) {
|
|
@@ -177,13 +201,13 @@ export async function signUp(answers, { session, onStep = () => {}, frontOrigin
|
|
|
177
201
|
// pod this key may sit on.
|
|
178
202
|
try {
|
|
179
203
|
await podState.provisionKey(remote, {
|
|
180
|
-
stateUrl:
|
|
181
|
-
keysUrl: keysDocFor(pod),
|
|
204
|
+
stateUrl: stateOf(pod, root),
|
|
205
|
+
keysUrl: keysDocFor(pod, root),
|
|
182
206
|
keys,
|
|
183
207
|
});
|
|
184
208
|
} catch (e) {
|
|
185
209
|
throw new Error(`could not store the signing key on the pod (${e.message}). `
|
|
186
|
-
+ `You are signed in as ${webId} — that WebID must own ${pod} and
|
|
210
|
+
+ `You are signed in as ${webId} — that WebID must own ${pod} and ${root} in it must be writable by it.`);
|
|
187
211
|
}
|
|
188
212
|
// This browser's own opened copy, so the boot that follows needs no read.
|
|
189
213
|
// Best effort: a browser that refuses IndexedDB (private mode) reads the
|
|
@@ -197,7 +221,7 @@ export async function signUp(answers, { session, onStep = () => {}, frontOrigin
|
|
|
197
221
|
}
|
|
198
222
|
|
|
199
223
|
const config = {
|
|
200
|
-
remotePod: pod, root
|
|
224
|
+
remotePod: pod, root, handle, name: handle, issuer: String(session.issuer || '').replace(/\/+$/, ''),
|
|
201
225
|
createdAt: new Date().toISOString(),
|
|
202
226
|
...(gateway ? { gateway } : {}),
|
|
203
227
|
};
|
|
@@ -205,11 +229,24 @@ export async function signUp(answers, { session, onStep = () => {}, frontOrigin
|
|
|
205
229
|
// sign-in — which arrives with only an OIDC session — can read the account's
|
|
206
230
|
// config and key from the pod and boot.
|
|
207
231
|
try {
|
|
208
|
-
await podState.writeConfig(remote, { state:
|
|
232
|
+
await podState.writeConfig(remote, { state: stateOf(pod, root) }, config);
|
|
209
233
|
} catch (e) {
|
|
210
234
|
throw new Error(`could not store the config on the pod (${e.message})`);
|
|
211
235
|
}
|
|
212
236
|
|
|
237
|
+
// --- where it lives, in the public type index ---
|
|
238
|
+
if (!prog.placed) {
|
|
239
|
+
const placeStep = step('place');
|
|
240
|
+
placeStep.running('recording where your account lives');
|
|
241
|
+
try {
|
|
242
|
+
await recordPlace(remote, pod, actorUrl, { create: !!answers.createIndex });
|
|
243
|
+
} catch (e) {
|
|
244
|
+
throw new Error(`could not record where your account lives in your type index (${e.message})`);
|
|
245
|
+
}
|
|
246
|
+
prog.placed = true;
|
|
247
|
+
placeStep.ok();
|
|
248
|
+
}
|
|
249
|
+
|
|
213
250
|
PROGRESS.delete(key); // finished — nothing left to resume
|
|
214
251
|
|
|
215
252
|
const host = gateway?.frontActor ? new URL(gateway.frontActor).host : new URL(pod).host;
|
|
@@ -232,14 +269,16 @@ export async function signUp(answers, { session, onStep = () => {}, frontOrigin
|
|
|
232
269
|
export async function readAccount(session) {
|
|
233
270
|
const pod = podBaseOfWebId(session.webId);
|
|
234
271
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: 'signup', log: () => {} });
|
|
235
|
-
|
|
236
|
-
|
|
272
|
+
// A person account: the browser build is persons only.
|
|
273
|
+
const found = await findAccount(remote, pod, readConfigAt(remote, pod), (c) => (c.kind || 'person') === 'person');
|
|
274
|
+
if (!found) return null;
|
|
275
|
+
const { root, config } = found;
|
|
237
276
|
const frontActor = config.gateway?.frontActor || null;
|
|
238
277
|
let frontHost = null;
|
|
239
278
|
try { frontHost = frontActor ? new URL(frontActor).host : null; } catch { frontHost = null; }
|
|
240
279
|
let podHost = '';
|
|
241
280
|
try { podHost = new URL(config.remotePod || pod).host; } catch { podHost = ''; }
|
|
242
|
-
return { pod, config, frontActor, frontHost, address: `@${config.handle}@${frontHost || podHost}` };
|
|
281
|
+
return { pod, root, config, frontActor, frontHost, address: `@${config.handle}@${frontHost || podHost}` };
|
|
243
282
|
}
|
|
244
283
|
|
|
245
284
|
/**
|
|
@@ -268,9 +307,9 @@ export async function moveIn(answers, { session, onStep = () => {}, frontOrigin
|
|
|
268
307
|
const here = await readAccount(session);
|
|
269
308
|
if (!here?.frontActor) throw new Error('this pod holds no account at another gateway to move here');
|
|
270
309
|
if (here.frontHost === new URL(origin).host) throw new Error('this account already lives at this gateway');
|
|
271
|
-
const { pod, config: old } = here;
|
|
310
|
+
const { pod, root, config: old } = here;
|
|
272
311
|
const remote = new BrowserRemotePod(session, { webId: session.webId, role: 'signup', log: () => {} });
|
|
273
|
-
const urls = { state:
|
|
312
|
+
const urls = { state: stateOf(pod, root) };
|
|
274
313
|
const keys = await podState.readKeys(remote, urls);
|
|
275
314
|
if (!keys?.rsa) throw new Error('could not read this account\'s signing key on the pod');
|
|
276
315
|
if (isKeyEnvelope(keys)) throw new Error(`this account's key is still under a password from an earlier version — sign in once at ${here.frontHost} first, then come back`);
|
|
@@ -282,7 +321,7 @@ export async function moveIn(answers, { session, onStep = () => {}, frontOrigin
|
|
|
282
321
|
await assertFrontNameFree(origin, handle);
|
|
283
322
|
const res = await session.fetch(`${origin}/api/attach`, {
|
|
284
323
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
285
|
-
body: JSON.stringify({ handle, podHome: `${pod}${
|
|
324
|
+
body: JSON.stringify({ handle, podHome: `${pod}${root}`, actorUrl: actorUrlFor(pod, root), kind: old.kind || 'person', fronted: true }),
|
|
286
325
|
});
|
|
287
326
|
const d = await res.json().catch(() => ({}));
|
|
288
327
|
if (res.status !== 201 || !d.hmacSecret) throw new Error(`could not attach to this gateway (HTTP ${res.status}): ${d.error || ''}`);
|
|
@@ -311,7 +350,7 @@ export async function moveIn(answers, { session, onStep = () => {}, frontOrigin
|
|
|
311
350
|
} catch (e) {
|
|
312
351
|
throw new Error(`could not update your account on the pod (${e.message})`);
|
|
313
352
|
}
|
|
314
|
-
await cacheOpenedKeys(actorUrlFor(pod), keys);
|
|
353
|
+
await cacheOpenedKeys(actorUrlFor(pod, root), keys);
|
|
315
354
|
keysStep.ok();
|
|
316
355
|
|
|
317
356
|
return { config, address: `@${handle}@${new URL(origin).host}`, movedFrom: config.movedFrom };
|
package/web/app/update.js
CHANGED
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
const b = await fetch('/build.json', { cache: 'no-store', headers: { accept: 'application/json' } })
|
|
52
52
|
.then(x => (x.ok ? x.json() : null)).catch(() => null);
|
|
53
53
|
if (b?.build && b.build !== myBuild) { go(); return; }
|
|
54
|
+
// A build that matched is the whole answer. Falling through here asked
|
|
55
|
+
// the site's function the same question once a minute from every open
|
|
56
|
+
// tab, for nothing — a third of everything the site was doing.
|
|
57
|
+
if (b?.build) return;
|
|
54
58
|
}
|
|
55
59
|
const r = await fetch('/api/handle?handle=__probe__', { cache: 'no-store', headers: { accept: 'application/json' } });
|
|
56
60
|
const v = (await r.json())?.version;
|
package/web/front/run.html
CHANGED
|
@@ -46,12 +46,18 @@ Fediverse services straight from your pod.</p>
|
|
|
46
46
|
<label for="run-issuer">Your identity provider</label>
|
|
47
47
|
<input type="url" id="run-issuer" autocomplete="off" placeholder="https://this.server">
|
|
48
48
|
</p>
|
|
49
|
+
<p class="field">
|
|
50
|
+
<label for="run-container">Store your data in a container named fedipod, inside this container</label>
|
|
51
|
+
<input type="text" id="run-container" autocomplete="off" spellcheck="false" placeholder="/"
|
|
52
|
+
aria-describedby="run-container-hint">
|
|
53
|
+
</p>
|
|
54
|
+
<p class="hint" id="run-container-hint">Leave it empty for your pod's root. This is permanent.</p>
|
|
49
55
|
<p class="actions">
|
|
50
56
|
<button type="button" id="run-continue" class="primary" disabled>Sign in with your pod & opt in</button>
|
|
51
57
|
<button type="button" id="run-leave">Stop running it here</button>
|
|
52
58
|
</p>
|
|
53
59
|
</form>
|
|
54
|
-
<
|
|
60
|
+
<div class="hint" id="run-note" hidden></div>
|
|
55
61
|
|
|
56
62
|
<script type="module" src="/run.js"></script>
|
|
57
63
|
</body>
|
package/web/front/run.js
CHANGED
|
@@ -88,8 +88,19 @@ function showPods(pods) {
|
|
|
88
88
|
stop.onclick = () => runStart('opt-out', pod.podBase);
|
|
89
89
|
actions.append(stop);
|
|
90
90
|
} else {
|
|
91
|
+
// Where on the pod the account goes: a container named fedipod, inside
|
|
92
|
+
// the one named here (empty: the pod's own root).
|
|
93
|
+
const field = document.createElement('p'); field.className = 'field';
|
|
94
|
+
const id = `container-${k.children.length}`;
|
|
95
|
+
const label = say('label', 'Store your data in a container named fedipod, inside this container');
|
|
96
|
+
label.htmlFor = id;
|
|
97
|
+
const input = document.createElement('input');
|
|
98
|
+
input.type = 'text'; input.id = id; input.autocomplete = 'off'; input.spellcheck = false;
|
|
99
|
+
try { input.placeholder = new URL(pod.podBase).pathname; } catch { input.placeholder = '/'; }
|
|
100
|
+
field.append(label, input);
|
|
101
|
+
box.append(field);
|
|
91
102
|
const go = say('button', 'Create a Fediverse account'); go.type = 'button'; go.className = 'primary';
|
|
92
|
-
go.onclick = () => runStart('opt-in', pod.podBase);
|
|
103
|
+
go.onclick = () => runStart('opt-in', pod.podBase, input.value.trim());
|
|
93
104
|
actions.append(go);
|
|
94
105
|
}
|
|
95
106
|
box.append(actions);
|
|
@@ -121,13 +132,15 @@ $('run-issuer').addEventListener('input', runFormCheck);
|
|
|
121
132
|
// a filled-in form, with nothing to say why.
|
|
122
133
|
runFormCheck();
|
|
123
134
|
|
|
124
|
-
const runStart = async (action, podBase = null) => {
|
|
135
|
+
const runStart = async (action, podBase = null, container = null, createIndex = false) => {
|
|
125
136
|
const n = $('run-note');
|
|
126
137
|
const u = new URL(podBase || $('run-pod-url').value.trim());
|
|
127
138
|
if (u.pathname === '') u.pathname = '/';
|
|
128
139
|
if (!u.pathname.endsWith('/')) u.pathname += '/';
|
|
129
140
|
u.search = ''; u.hash = '';
|
|
130
|
-
const p = { podBase: u.href, action
|
|
141
|
+
const p = { podBase: u.href, action,
|
|
142
|
+
container: container ?? ($('run-container') ? $('run-container').value.trim() : ''),
|
|
143
|
+
...(createIndex ? { createIndex: true } : {}) };
|
|
131
144
|
|
|
132
145
|
// A pod server issued the session this page is being read with, so ask it
|
|
133
146
|
// first: where it knows who you are, there is nothing to sign in to again.
|
|
@@ -156,6 +169,18 @@ async function showReply(res, p) {
|
|
|
156
169
|
const n = $('run-note');
|
|
157
170
|
n.hidden = false;
|
|
158
171
|
const d = res ? await res.json().catch(() => ({})) : {};
|
|
172
|
+
// No public type index: nothing has been written. Ask.
|
|
173
|
+
if (res && res.status === 409 && d.code === 'needs-index') {
|
|
174
|
+
n.replaceChildren(say('p', `${d.error} Create it? It will be readable by anyone, like your profile.`));
|
|
175
|
+
const yes = say('button', 'Create it and continue'); yes.type = 'button'; yes.className = 'primary';
|
|
176
|
+
const no = say('button', 'No, stop'); no.type = 'button';
|
|
177
|
+
yes.onclick = () => runStart(p.action, p.podBase, p.container || '', true);
|
|
178
|
+
no.onclick = () => { n.textContent = 'Stopped. Nothing was written to your pod.'; };
|
|
179
|
+
const row = document.createElement('p'); row.className = 'actions'; row.append(no, yes);
|
|
180
|
+
n.append(row);
|
|
181
|
+
yes.focus();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
159
184
|
if (res && res.status === 201 && d.doorSecret) {
|
|
160
185
|
// Built as nodes, not as a string of HTML. Three of the pieces below come
|
|
161
186
|
// back from the server and one comes from sessionStorage, and pasting any
|
|
@@ -189,7 +214,8 @@ async function showReply(res, p) {
|
|
|
189
214
|
// The signed fetch builds a DPoP proof from the URL, so it must be absolute.
|
|
190
215
|
const res = await session.authFetch(location.origin + '/api/agent', {
|
|
191
216
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
192
|
-
body: JSON.stringify({ action: p.action, podBase: p.podBase
|
|
217
|
+
body: JSON.stringify({ action: p.action, podBase: p.podBase, container: p.container || '',
|
|
218
|
+
...(p.createIndex ? { createIndex: true } : {}) }),
|
|
193
219
|
}).catch(() => null);
|
|
194
220
|
await showReply(res, p);
|
|
195
221
|
})();
|