fedipod-server 0.15.0 → 0.17.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 +37 -15
- package/dist/claims.d.ts +10 -11
- package/dist/claims.js +19 -17
- package/dist/directory.d.ts +11 -3
- package/dist/directory.js +28 -14
- package/dist/handler.d.ts +30 -7
- package/dist/handler.js +133 -42
- package/dist/handler.jsonld +14 -6
- package/dist/streaming-handler.js +7 -2
- package/lib/client/masto/index.mjs +7 -1
- package/lib/client/masto/timelines.mjs +1 -1
- package/lib/client/oidc-auth.mjs +5 -3
- package/lib/core/publisher/index.mjs +1 -1
- package/lib/core/wire.mjs +4 -1
- package/lib/device/admin/routes/gateway.mjs +1 -1
- package/lib/device/admin/routes/lifecycle.mjs +1 -1
- package/lib/device/admin/routes/setup.mjs +20 -1
- package/lib/device/admin/surface.mjs +25 -17
- package/lib/device/cli/commands/setup.mjs +53 -21
- package/lib/device/setup.mjs +105 -25
- package/lib/gateway/front-core.mjs +23 -11
- package/lib/pod/root.mjs +12 -1
- package/lib/server/embed.mjs +23 -6
- package/package.json +2 -1
- package/run-agent.mjs +1 -1
- package/vendor/gate.cjs +5 -2
- 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 +3 -3
- package/web/app/boot.mjs +2 -2
- package/web/app/dist/boot.js +43 -31
- package/web/app/dist/boot.js.map +3 -3
- package/web/app/dist/sw.js +4 -4
- package/web/app/dist/sw.js.map +2 -2
- package/web/app/index.html +1 -1
- package/web/app/signup.mjs +5 -3
- 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 +1 -1
- package/web/app/site/index.html +1 -1
- package/web/app/site/sw.js +3 -3
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
|
-
| **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
|
|
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
|
@@ -160,7 +160,7 @@ export class BrowserAgent {
|
|
|
160
160
|
if (oidc) {
|
|
161
161
|
session = { fetch: (u, i) => oidc.fetch(u, i) };
|
|
162
162
|
webId = oidc.webId;
|
|
163
|
-
remotePod = podBaseOfWebId(webId); // the pod, which on a
|
|
163
|
+
remotePod = podBaseOfWebId(webId); // the pod, which on a suffix-based host is a path
|
|
164
164
|
} else {
|
|
165
165
|
const dpop = await makeDpopSession(credential);
|
|
166
166
|
session = { fetch: (u, i) => dpop.fetch(u, i) };
|
|
@@ -189,7 +189,7 @@ export class BrowserAgent {
|
|
|
189
189
|
// builds its own urls from `config.root` (publisher.mjs), and a config
|
|
190
190
|
// without one falls to the Node default — so an account set up elsewhere
|
|
191
191
|
// and signed into here would keep its state under `fedipod/` while every
|
|
192
|
-
// document it published landed under
|
|
192
|
+
// document it published landed under a different root. One root, decided
|
|
193
193
|
// once, carried by the config everything downstream reads.
|
|
194
194
|
this.store.setConfig({ ...(this.store.getConfig() || {}), ...cfg, root });
|
|
195
195
|
config = this.store.getConfig();
|
|
@@ -224,7 +224,7 @@ export class BrowserAgent {
|
|
|
224
224
|
this.publisher = new Publisher({
|
|
225
225
|
config: this.store.getConfig(), remote: this.remote, store: this.store,
|
|
226
226
|
deliverer: this.deliverer, publicKeyPem: keys.rsaPublicPem, assertionKey: null, log: this.log,
|
|
227
|
-
// Who a post names, resolved — the same lookup the
|
|
227
|
+
// Who a post names, resolved — the same lookup the DeviceAgent
|
|
228
228
|
// gives its publisher. Without it no mention from the browser ever
|
|
229
229
|
// resolved: a direct message went to nobody, a mention notified no one.
|
|
230
230
|
resolveMention: (h) => resolveHandle(this, h),
|
package/web/app/boot.mjs
CHANGED
|
@@ -70,7 +70,7 @@ window.fedipodUnlock = async (password) => {
|
|
|
70
70
|
if (!session) throw new Error('Sign in first.');
|
|
71
71
|
// The config on the pod says where this account's state lives; the key sits
|
|
72
72
|
// beside it. Both are read with the session, as the owner.
|
|
73
|
-
const podFromWebId = podBaseOfWebId(session.webId); // a
|
|
73
|
+
const podFromWebId = podBaseOfWebId(session.webId); // a suffix-based host, or its own host
|
|
74
74
|
const state = `${podFromWebId}${AP_ROOT}ap-state/`;
|
|
75
75
|
// Through the transport rather than the bare session: this is a pod read
|
|
76
76
|
// like any other, and going round it skipped the retry ladder that exists
|
|
@@ -288,7 +288,7 @@ if (typeof document !== 'undefined') (async () => {
|
|
|
288
288
|
// brought by its address, which may be its own host or a path on a shared one.
|
|
289
289
|
const podHostOf = () => { const sub = f().podName.value.trim().toLowerCase(); const ph = providerHost(); return (sub && ph) ? `${sub}.${ph}` : ''; };
|
|
290
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
|
|
291
|
+
// A pod on a suffix-based host cannot answer WebFinger, so its address
|
|
292
292
|
// lives at this site; a pod at its own host root gets the choice.
|
|
293
293
|
const isPathPod = (u) => { try { return new URL(u).pathname !== '/'; } catch { return false; } };
|
|
294
294
|
// Where the chosen provider puts new pods, asked of the provider itself
|
package/web/app/dist/boot.js
CHANGED
|
@@ -33208,6 +33208,47 @@ async function provisionKey(pod, { stateUrl, keysUrl, envelope }) {
|
|
|
33208
33208
|
await pod.putJson(keysUrl, envelope, "application/json");
|
|
33209
33209
|
}
|
|
33210
33210
|
|
|
33211
|
+
// lib/pod/root.mjs
|
|
33212
|
+
var OWNER_LOOKUP_MS = 5e3;
|
|
33213
|
+
var PUBLIC_DOC_MAX_BYTES = 1024 * 1024;
|
|
33214
|
+
async function podLayout(fetchImpl, providerOrigin, { timeoutMs = OWNER_LOOKUP_MS } = {}) {
|
|
33215
|
+
let origin;
|
|
33216
|
+
try {
|
|
33217
|
+
origin = new URL(providerOrigin).origin;
|
|
33218
|
+
} catch {
|
|
33219
|
+
return null;
|
|
33220
|
+
}
|
|
33221
|
+
let res;
|
|
33222
|
+
try {
|
|
33223
|
+
res = await fetchImpl(
|
|
33224
|
+
`${origin}/.well-known/solid`,
|
|
33225
|
+
{ headers: { accept: "text/turtle" }, signal: AbortSignal.timeout(timeoutMs) }
|
|
33226
|
+
);
|
|
33227
|
+
} catch {
|
|
33228
|
+
return null;
|
|
33229
|
+
}
|
|
33230
|
+
if (res.status === 501) return "host";
|
|
33231
|
+
if (res.status !== 200) return null;
|
|
33232
|
+
let body = "";
|
|
33233
|
+
try {
|
|
33234
|
+
body = await readCapped(res, 64 * 1024);
|
|
33235
|
+
} catch {
|
|
33236
|
+
return null;
|
|
33237
|
+
}
|
|
33238
|
+
return /ns\/pim\/space#Storage|pim:Storage/u.test(body) ? "path" : null;
|
|
33239
|
+
}
|
|
33240
|
+
async function resourceExists(fetchImpl, url, { timeoutMs = OWNER_LOOKUP_MS } = {}) {
|
|
33241
|
+
try {
|
|
33242
|
+
const res = await fetchImpl(
|
|
33243
|
+
url,
|
|
33244
|
+
{ headers: { accept: "application/activity+json" }, signal: AbortSignal.timeout(timeoutMs) }
|
|
33245
|
+
);
|
|
33246
|
+
return !!res && res.status === 200;
|
|
33247
|
+
} catch {
|
|
33248
|
+
return false;
|
|
33249
|
+
}
|
|
33250
|
+
}
|
|
33251
|
+
|
|
33211
33252
|
// web/app/idb-kv.mjs
|
|
33212
33253
|
var DB = "fedipod-accounts";
|
|
33213
33254
|
function open() {
|
|
@@ -33310,6 +33351,7 @@ async function signUp(answers, { onStep = () => {
|
|
|
33310
33351
|
acct.running("checking your pod");
|
|
33311
33352
|
const head = await fetch(brought, { method: "HEAD" }).catch(() => null);
|
|
33312
33353
|
if (!head || head.status >= 400) throw new Error(`the pod at ${brought} did not answer (HTTP ${head?.status || "no response"})`);
|
|
33354
|
+
if (await resourceExists(fetch, actorUrlFor(brought))) throw new Error("The pod already hosts a FediPod account. If you want a second account, put it on a different pod.");
|
|
33313
33355
|
prog.pod = brought;
|
|
33314
33356
|
acct.skip("using the pod you brought");
|
|
33315
33357
|
}
|
|
@@ -33321,7 +33363,7 @@ async function signUp(answers, { onStep = () => {
|
|
|
33321
33363
|
const pathPod = new URL(pod).pathname !== "/";
|
|
33322
33364
|
const fronted = pathPod || wantsFront;
|
|
33323
33365
|
if (fronted && !frontOrigin) {
|
|
33324
|
-
throw new Error(`${pod} is a
|
|
33366
|
+
throw new Error(`${pod} is a suffix-based host, so its address must live at a gateway, and this page has none.`);
|
|
33325
33367
|
}
|
|
33326
33368
|
if (pathPod && !wantsFront) await assertFrontNameFree(frontOrigin, handle);
|
|
33327
33369
|
const actorUrl = actorUrlFor(pod);
|
|
@@ -33450,36 +33492,6 @@ function podBaseOfWebId(webId) {
|
|
|
33450
33492
|
return `${u.origin}${dir.endsWith("/") ? dir : dir + "/"}`;
|
|
33451
33493
|
}
|
|
33452
33494
|
|
|
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
|
-
|
|
33483
33495
|
// web/app/oidc-session.mjs
|
|
33484
33496
|
var DB2 = "fedipod-oidc";
|
|
33485
33497
|
var STORE = "session";
|