fedipod 1.5.0 → 1.7.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 +5 -5
- package/architecture.md +4 -4
- package/cli.md +7 -0
- package/{installed-agent.md → device-agent.md} +6 -4
- package/gateway.md +1 -1
- package/groups.md +2 -2
- package/gui.md +2 -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 -2
- 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/README.md
CHANGED
|
@@ -16,9 +16,9 @@ There are also a number of [other ways to run FediPod](#other-ways-to-run-fedipo
|
|
|
16
16
|
- A Solid pod with a host name of its own, such as
|
|
17
17
|
`https://alice.solidcommunity.net/`. Sign-up can create one for you at
|
|
18
18
|
solidcommunity.net or another provider, or use a pod you already have. A
|
|
19
|
-
pod that lives on a
|
|
19
|
+
pod that lives on a suffix-based host, like `https://server.example/alice/`,
|
|
20
20
|
cannot be a Fediverse address.
|
|
21
|
-
A pod on a
|
|
21
|
+
A pod on a suffix-based host, like `https://server.example/alice/`,
|
|
22
22
|
works too. Its address is then `@handle@fedipod.net`, because the shared
|
|
23
23
|
host cannot answer for the handle; your posts, key and data stay on your pod.
|
|
24
24
|
- Followers-only and direct posts need a pod that enforces WAC access control.
|
|
@@ -33,7 +33,7 @@ There are also a number of [other ways to run FediPod](#other-ways-to-run-fedipo
|
|
|
33
33
|
permanent; display name, bio and pictures are set later in the client.
|
|
34
34
|
With a pod at its own host you also choose where the address lives: on
|
|
35
35
|
your pod, `@handle@yourpod`, or at this site, `@handle@fedipod.net`. A pod
|
|
36
|
-
on a
|
|
36
|
+
on a suffix-based host gets the fedipod.net address. The choice is
|
|
37
37
|
permanent.
|
|
38
38
|
4. Enter your pod password once. It creates the account and locks your
|
|
39
39
|
signing key. The password is not stored.
|
|
@@ -73,7 +73,7 @@ an alias, so a Move from it lands here.
|
|
|
73
73
|
**The manage page.** `manage account` in the bar opens it: your profile,
|
|
74
74
|
aliases, the gateway, key rotation, recovering posts, parking, moving to
|
|
75
75
|
another server, retiring, and clearing a backlog. It is the same interface
|
|
76
|
-
the
|
|
76
|
+
the DeviceAgent has, described in [the admin interface](gui.md).
|
|
77
77
|
|
|
78
78
|
**More than one browser.** One browser runs your account at a time. Opening
|
|
79
79
|
it in a second browser shows your timeline read-only, and the moment you act
|
|
@@ -103,7 +103,7 @@ to a gateway of your own. Your address and your data do not change.
|
|
|
103
103
|
|
|
104
104
|
## Other ways to run FediPod
|
|
105
105
|
|
|
106
|
-
- [The
|
|
106
|
+
- [The DeviceAgent](device-agent.md) runs on your own machine and
|
|
107
107
|
adds scheduled posts, push notifications, live updates, group hosting and
|
|
108
108
|
the use of any Mastodon client.
|
|
109
109
|
- [Groups](groups.md): hosting a discussion group of Fediverse and Bluesky
|
package/architecture.md
CHANGED
|
@@ -5,7 +5,7 @@ discovery and stores the public record: the actor, its outbox, followers and
|
|
|
5
5
|
posts, and the inbox that receives deliveries. The agent provides the
|
|
6
6
|
ActivityPub actions: it drains the inbox, builds the timeline, signs and
|
|
7
7
|
delivers, and answers the Mastodon client. The agent runs either in your
|
|
8
|
-
browser, served by fedipod.net, or as [the
|
|
8
|
+
browser, served by fedipod.net, or as [the DeviceAgent](device-agent.md)
|
|
9
9
|
on your own machine. Private direct messages, followers-only posts and the
|
|
10
10
|
pending-follow and blocked collections live on the pod in an area protected by
|
|
11
11
|
access control.
|
|
@@ -13,7 +13,7 @@ access control.
|
|
|
13
13
|
A [gateway](gateway.md) can stand in front of the pod: an always-on door that
|
|
14
14
|
verifies each delivery where the signature can still be checked, drops spam,
|
|
15
15
|
and forwards the rest to the pod inbox with a receipt. It holds no key. The
|
|
16
|
-
browser version always has one; the
|
|
16
|
+
browser version always has one; the DeviceAgent may use one. Any
|
|
17
17
|
lightweight host will do, Netlify included.
|
|
18
18
|
|
|
19
19
|
[FediPod Server](packages/fedipod-server/README.md) puts the agent inside a
|
|
@@ -22,13 +22,13 @@ Fediverse account fed by the server itself.
|
|
|
22
22
|
|
|
23
23
|

|
|
24
24
|
|
|
25
|
-

|
|
26
26
|
|
|
27
27
|
## Protocol conformance
|
|
28
28
|
|
|
29
29
|
FediPod is a full ActivityPub server, on both of the spec's profiles:
|
|
30
30
|
server-to-server (§7) and client-to-server (§6). `POST /ap/outbox` on the
|
|
31
|
-
|
|
31
|
+
DeviceAgent takes an activity, or a bare Note, and does the id-minting,
|
|
32
32
|
side-effects and delivery, authenticated by a Solid-OIDC token whose WebID is
|
|
33
33
|
the owner's. The Mastodon REST API is the everyday client interface; C2S is
|
|
34
34
|
the spec's own.
|
package/cli.md
CHANGED
|
@@ -29,6 +29,13 @@ pod account as part of it, `--keys pod` puts the signing key in pod state for
|
|
|
29
29
|
multi-device use, and `AP_PASSWORD` supplies the pod password without a
|
|
30
30
|
prompt. `--profile NAME` names the new identity when you have more than one.
|
|
31
31
|
|
|
32
|
+
`--address pod` (the default) puts your address on your pod, `@you@yourpod`;
|
|
33
|
+
`--address front` puts it at a gateway, `@you@the-gateway`, with your posts,
|
|
34
|
+
key and data still on your pod. `--gateway <url>` names the gateway
|
|
35
|
+
(`https://fedipod.net` by default). A pod on a suffix-based host cannot
|
|
36
|
+
answer WebFinger for a handle, so it always takes a gateway address, whichever
|
|
37
|
+
`--address` you gave.
|
|
38
|
+
|
|
32
39
|
## Which identity a command acts on
|
|
33
40
|
|
|
34
41
|
`--profile NAME` works on every command, not just `up`; `AP_PROFILE` is the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# The
|
|
1
|
+
# The DeviceAgent
|
|
2
2
|
|
|
3
3
|
FediPod can also run as a program on your own machine, in front of the same
|
|
4
4
|
kind of pod. It does everything the browser version at fedipod.net does, plus
|
|
@@ -10,9 +10,11 @@ connect to it; and it can host a [group](groups.md).
|
|
|
10
10
|
## Requirements
|
|
11
11
|
|
|
12
12
|
- Node 20 or newer.
|
|
13
|
-
- A Solid pod with a host name of its own, such as
|
|
14
|
-
`https://alice.solidcommunity.net
|
|
15
|
-
|
|
13
|
+
- A Solid pod, either with a host name of its own, such as
|
|
14
|
+
`https://alice.solidcommunity.net/`, or on a suffix-based host, such as
|
|
15
|
+
`https://server.example/alice/`. A pod at its own host carries its address on
|
|
16
|
+
the pod; a pod on a suffix-based host takes its address at a gateway,
|
|
17
|
+
`@handle@fedipod.net`, with the posts, key and data staying on the pod.
|
|
16
18
|
- Followers-only and direct posts need a pod that enforces WAC access control;
|
|
17
19
|
on one that does not, the composer refuses those two and says why.
|
|
18
20
|
- While the agent is off, your mail waits on your pod's host. Run it as a
|
package/gateway.md
CHANGED
|
@@ -29,7 +29,7 @@ password is typed anywhere:
|
|
|
29
29
|
restarts the agent itself to publish under it.
|
|
30
30
|
|
|
31
31
|
In the BrowserAgent the same choice is made once, at sign-up, and cannot be
|
|
32
|
-
changed afterwards. A pod on a
|
|
32
|
+
changed afterwards. A pod on a suffix-based host, like
|
|
33
33
|
`https://server.example/alice/`, always takes the gateway-based name: nothing
|
|
34
34
|
at that host answers for the handle, so the gateway does. Its posts, key and
|
|
35
35
|
data stay on the pod.
|
package/groups.md
CHANGED
|
@@ -14,7 +14,7 @@ and pick group rather than person, or take the group path on a signup page.
|
|
|
14
14
|
You can turn on join review as you create it. The group's pod must be the root
|
|
15
15
|
of its own host, so its handle resolves.
|
|
16
16
|
|
|
17
|
-
**Running a group needs the
|
|
17
|
+
**Running a group needs the DeviceAgent.** The in-browser build makes
|
|
18
18
|
personal identities only — the sign-up wizard has no group option, and the
|
|
19
19
|
moderation surface (join review, members, muting, the moderation queue) is not
|
|
20
20
|
part of that build. You can *join* a group from the browser exactly as from
|
|
@@ -59,7 +59,7 @@ A followed group's own announced deletion of a post it carried to you is honoure
|
|
|
59
59
|
## Inviting people
|
|
60
60
|
|
|
61
61
|
A group has a page anyone can open, at `ap/profile.html` under its pod's
|
|
62
|
-
app container — `<pod>/
|
|
62
|
+
app container — `<pod>/fedipod/ap/profile.html`. It
|
|
63
63
|
carries the group's address and a Follow box that sends a visitor to their
|
|
64
64
|
own server's follow screen, so it is the link to put where people will find
|
|
65
65
|
it. Posts the group carries appear in members' timelines as the group
|
package/gui.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
In the browser version at fedipod.net, `manage account` in the bar opens this
|
|
4
4
|
same page for your account. The rest of this page describes it as the
|
|
5
|
-
|
|
5
|
+
DeviceAgent serves it; the controls are the same, minus the manual inbox
|
|
6
6
|
drain and the local log, which a browser does not have.
|
|
7
7
|
|
|
8
8
|
Open `https://localhost:8030/` while any agent is running — it forwards you to the agent — then choose `manage account` and select the actor you want from the local actors dropdown.
|
|
@@ -46,7 +46,7 @@ its own port.
|
|
|
46
46
|
## Sharing an account
|
|
47
47
|
|
|
48
48
|
Each identity has a page anyone can open, at `ap/profile.html` under its pod
|
|
49
|
-
— for example `https://your-pod.example/
|
|
49
|
+
— for example `https://your-pod.example/fedipod/ap/profile.html`. It
|
|
50
50
|
shows the name, bio and address, and offers a Follow box: a visitor types
|
|
51
51
|
their own server and lands on that server's follow screen. Hand out that
|
|
52
52
|
link, or the `@name@host` address itself, which works in the search box of
|
|
@@ -32,8 +32,14 @@ export { attachmentType, extensionFor } from './media.mjs';
|
|
|
32
32
|
|
|
33
33
|
export class MastoApi {
|
|
34
34
|
constructor({ agent, log = console.log, allowed = null, scheme = null, embedded = false,
|
|
35
|
-
streaming = true, webPush = true, scheduling = true }) {
|
|
35
|
+
mount = '', streaming = true, webPush = true, scheduling = true }) {
|
|
36
36
|
this.agent = agent;
|
|
37
|
+
// The path this identity's surface answers under, when it shares its origin
|
|
38
|
+
// with others (a suffix pod, e.g. `/aisha`). Empty for a host-root or
|
|
39
|
+
// subdomain pod. Folded into the self-URLs the client is handed —
|
|
40
|
+
// pagination links, the OAuth issuer — so they name the address the client
|
|
41
|
+
// actually reached.
|
|
42
|
+
this.mount = mount;
|
|
37
43
|
// A server-hosted identity has no CLI of its own, so the advice this gives
|
|
38
44
|
// when it refuses has to name the route that identity really has.
|
|
39
45
|
this.embedded = embedded;
|
|
@@ -254,7 +254,7 @@ export async function handle(api, ctx) {
|
|
|
254
254
|
|
|
255
255
|
// A client pages by following these rather than by guessing ids.
|
|
256
256
|
if (page.length) {
|
|
257
|
-
const base = `${api.scheme || (req.socket?.encrypted ? 'https' : 'http')}://${req.headers.host}${pathname}`;
|
|
257
|
+
const base = `${api.scheme || (req.socket?.encrypted ? 'https' : 'http')}://${req.headers.host}${api.mount || ''}${pathname}`;
|
|
258
258
|
const link = (params) => {
|
|
259
259
|
const u = new URL(base);
|
|
260
260
|
for (const [k, v] of q) if (k !== 'max_id' && k !== 'since_id' && k !== 'min_id') u.searchParams.append(k, v);
|
package/lib/client/oidc-auth.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The verifier is injected so offline tests stub it, and wrapped so the
|
|
9
9
|
// library (CJS, older jose) can be replaced without touching any caller.
|
|
10
10
|
|
|
11
|
-
export function makeC2sAuth({ agent, masto = null, verifier = null, log = () => {}, scheme = null }) {
|
|
11
|
+
export function makeC2sAuth({ agent, masto = null, verifier = null, log = () => {}, scheme = null, mount = '' }) {
|
|
12
12
|
let verify = verifier;
|
|
13
13
|
const loadVerifier = async () => {
|
|
14
14
|
if (!verify) {
|
|
@@ -31,9 +31,11 @@ export function makeC2sAuth({ agent, masto = null, verifier = null, log = () =>
|
|
|
31
31
|
// The URL the client signed its proof over. The Host header already
|
|
32
32
|
// passed the Authorities firewall, so whichever alias the client used
|
|
33
33
|
// (localhost, 127.0.0.1, the named origin) is one this agent answers on;
|
|
34
|
-
// the scheme is whichever listener the request arrived on.
|
|
34
|
+
// the scheme is whichever listener the request arrived on. `pathname` is
|
|
35
|
+
// relative to this identity's mount, so a suffix pod folds the mount back
|
|
36
|
+
// in — the client signed over the full path it actually requested.
|
|
35
37
|
const htu = `${scheme ? scheme.replace(/:$/u, '') : req.socket?.encrypted ? 'https' : 'http'
|
|
36
|
-
}://${req.headers.host}${pathname}`;
|
|
38
|
+
}://${req.headers.host}${mount}${pathname}`;
|
|
37
39
|
({ webid } = await v(
|
|
38
40
|
req.headers.authorization,
|
|
39
41
|
req.headers.dpop ? { header: req.headers.dpop, method: req.method, url: htu } : undefined,
|
|
@@ -201,7 +201,7 @@ export class Publisher {
|
|
|
201
201
|
this.log(this.config.gateway?.frontActor || wire.webfingerHost(urls.base)
|
|
202
202
|
? `profile published: @${pubName}@${pubHost} → ${urls.actor}`
|
|
203
203
|
: `profile published → ${urls.actor} — NOT discoverable as @${pubName}@${pubHost}: `
|
|
204
|
-
+ 'this pod is a
|
|
204
|
+
+ 'this pod is a suffix-based host, and WebFinger is only answered at a host root');
|
|
205
205
|
return { unreachable, updated };
|
|
206
206
|
}
|
|
207
207
|
|
package/lib/core/wire.mjs
CHANGED
|
@@ -20,7 +20,10 @@ export { webfingerHost } from '../pod/urls.mjs';
|
|
|
20
20
|
// contains; the browser build states `fedipod/` on its own configs. A config
|
|
21
21
|
// that names a root is always believed — this is only the answer for one that
|
|
22
22
|
// does not.
|
|
23
|
-
|
|
23
|
+
// Every build writes its data into a `fedipod/` container in the pod. This is
|
|
24
|
+
// the answer for a config that names no root; a config that names one is always
|
|
25
|
+
// believed. (`activitypods-js/` was an earlier name, now abandoned.)
|
|
26
|
+
export const DEFAULT_ROOT = 'fedipod/';
|
|
24
27
|
|
|
25
28
|
// The handle the fediverse sees: a fronted identity's name is the front's.
|
|
26
29
|
export function publicHandle(config) {
|
|
@@ -47,7 +47,7 @@ export async function post(p, body, ctx, req, res) { // eslint-disable-line no
|
|
|
47
47
|
};
|
|
48
48
|
const podActorId = () => {
|
|
49
49
|
const base = cfg.remotePod.endsWith('/') ? cfg.remotePod : `${cfg.remotePod}/`;
|
|
50
|
-
const root = cfg.root ? (cfg.root.endsWith('/') ? cfg.root : `${cfg.root}/`) : '
|
|
50
|
+
const root = cfg.root ? (cfg.root.endsWith('/') ? cfg.root : `${cfg.root}/`) : 'fedipod/';
|
|
51
51
|
return `${base}${root}ap/actor`;
|
|
52
52
|
};
|
|
53
53
|
// The reply first, the restart a beat later — same shape as /update.
|
|
@@ -102,7 +102,7 @@ export async function post(p, body, ctx, req, res) { // eslint-disable-line no
|
|
|
102
102
|
// A Move target nobody can resolve is a landing pad nobody lands on.
|
|
103
103
|
if (!webfingerHost(urls.base) && !cfg.gateway?.frontActor) {
|
|
104
104
|
return json(res, 400, {
|
|
105
|
-
error: 'this pod is a
|
|
105
|
+
error: 'this pod is a suffix-based host, so WebFinger cannot answer for it '
|
|
106
106
|
+ '— other servers could never resolve this account as a Move target',
|
|
107
107
|
});
|
|
108
108
|
}
|
|
@@ -12,6 +12,7 @@ import { identityHomes, rootOf, tildify, writeJsonAtomic } from '../../home.mjs'
|
|
|
12
12
|
import { copyPrivateHalf, isCurrent, CURRENT_LAYOUT } from '../../migrate.mjs';
|
|
13
13
|
import { insecureUrlReason } from '../../../shared/safefetch.mjs';
|
|
14
14
|
import { newRun, preflight, runSetup, setupInputError, hasCredential, credentialPath } from '../../setup.mjs';
|
|
15
|
+
import { podLayout } from '../../../pod/root.mjs';
|
|
15
16
|
import { portFree, freePortFrom } from '../../ports.mjs';
|
|
16
17
|
import { yieldDirectory } from '../../../gateway/directory.mjs';
|
|
17
18
|
import { localFetch } from '../../../client/localapi.mjs';
|
|
@@ -113,7 +114,25 @@ export async function post(p, body, ctx, req, res) { // eslint-disable-line no
|
|
|
113
114
|
return true;
|
|
114
115
|
}
|
|
115
116
|
// ---- setup, driven by the page at /admin/setup/ ----
|
|
116
|
-
case '/setup/check':
|
|
117
|
+
case '/setup/check': {
|
|
118
|
+
const pre = preflight(body);
|
|
119
|
+
// For a new pod, ask the provider where it puts pods, so the page can
|
|
120
|
+
// show the Gateway address before the pod exists. A provider on paths
|
|
121
|
+
// means the address must live at the Gateway; the run decides for real.
|
|
122
|
+
if (pre.ok && body.mode === 'new' && body.shape !== 'front') {
|
|
123
|
+
const layout = await podLayout(fetch, body.issuer || '').catch(() => null);
|
|
124
|
+
if (layout === 'path') {
|
|
125
|
+
if ((body.kind || 'person') === 'group') {
|
|
126
|
+
return json(res, 200, { ...pre, ok: false, layout, refusal: 'group-needs-host-root' });
|
|
127
|
+
}
|
|
128
|
+
let gwHost; try { gwHost = new URL(body.gatewayOrigin || 'https://fedipod.net').host; } catch { gwHost = 'fedipod.net'; }
|
|
129
|
+
return json(res, 200, { ...pre, layout, fronted: true, forced: true, shape: 'front', gatewayHost: gwHost,
|
|
130
|
+
address: `@${body.handle}@${gwHost}`, resolvable: true });
|
|
131
|
+
}
|
|
132
|
+
return json(res, 200, { ...pre, layout });
|
|
133
|
+
}
|
|
134
|
+
return json(res, 200, pre);
|
|
135
|
+
}
|
|
117
136
|
// Discard a credential that never finished setup, so the account and
|
|
118
137
|
// pod can be entered again. The credential a CSS server mints is shown
|
|
119
138
|
// once, so a setup that stops after the mint (a wrong pod answers 401
|
|
@@ -76,14 +76,14 @@ const ROUTES = [owner, setup, lifecycle, gateway, social, connections];
|
|
|
76
76
|
// a fediverse instance must let strangers reach /api and /oauth, so the gate
|
|
77
77
|
// guards the operator's door (basePath) instead of the whole surface.
|
|
78
78
|
export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
79
|
-
port = null, handle = null, embedded = false, basePath = '/',
|
|
79
|
+
port = null, handle = null, embedded = false, basePath = '/', mount = '',
|
|
80
80
|
publicOrigin = null, scheme = null,
|
|
81
81
|
versionOnDisk = () => localVersion(projectRoot) }) {
|
|
82
82
|
const json = (res, status, obj) => sendJson(res, status, obj, allowed);
|
|
83
|
-
const masto = new MastoApi({ agent, log, allowed, scheme, embedded });
|
|
83
|
+
const masto = new MastoApi({ agent, log, allowed, scheme, embedded, mount });
|
|
84
84
|
// The spec's own write API (§6), beside the facade. Its bearer fallback is
|
|
85
85
|
// the facade's token, so the two surfaces share one notion of the operator.
|
|
86
|
-
const c2s = new C2S({ agent, log, auth: makeC2sAuth({ agent, masto, log, scheme }) });
|
|
86
|
+
const c2s = new C2S({ agent, log, auth: makeC2sAuth({ agent, masto, log, scheme, mount }) });
|
|
87
87
|
const streaming = new Streaming({ masto, log, allowed, gate, gateOptional: embedded });
|
|
88
88
|
// Asked per request, not once here: startAdmin runs before connect, so the
|
|
89
89
|
// kind is not known yet at mount time.
|
|
@@ -104,21 +104,29 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
|
104
104
|
} catch (e) { log(`streaming broadcast: ${e.message}`); }
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
// A path as the browser must ask for it:
|
|
108
|
-
|
|
107
|
+
// A path as the browser must ask for it: under this identity's mount (a
|
|
108
|
+
// suffix pod's own path, or nothing) and behind the door, prefixed with it.
|
|
109
|
+
const atPath = (p_) => mount + (basePath === '/' ? p_ : basePath.slice(0, -1) + p_);
|
|
109
110
|
|
|
110
111
|
// What every route may reach: the agent and the deployment's facts.
|
|
111
112
|
const ctx = { agent, log, allowed, embedded, port, handle, publicOrigin, versionOnDisk, isGroup, json, setup: setup_ };
|
|
112
113
|
|
|
113
114
|
const handler = async (req, res) => {
|
|
114
115
|
const url = new URL(req.url, 'http://localhost');
|
|
116
|
+
// A suffix pod's surface answers under its mount (its own path on a shared
|
|
117
|
+
// host). Strip it once, here, so every route below is matched relative to
|
|
118
|
+
// the mount and a host-root/subdomain pod (empty mount) is unchanged. The
|
|
119
|
+
// full path stays on `url`/`req.url` for self-URLs that fold the mount back
|
|
120
|
+
// in themselves (the pagination base, the DPoP htu).
|
|
121
|
+
let p = url.pathname;
|
|
122
|
+
if (mount && (p === mount || p.startsWith(mount + '/'))) p = p.slice(mount.length) || '/';
|
|
115
123
|
// Mastodon-style: the bearer-gated client API and the OAuth + nodeinfo
|
|
116
124
|
// routes answer any origin — a browser client is served the way any
|
|
117
125
|
// instance serves it. CORS headers and the preflight make that work; the
|
|
118
126
|
// bearer stays the only credential, and the Host check below (which is
|
|
119
127
|
// what stops DNS rebinding) still runs.
|
|
120
|
-
const apiPath =
|
|
121
|
-
||
|
|
128
|
+
const apiPath = p.startsWith('/api/') || p.startsWith('/oauth/')
|
|
129
|
+
|| p === '/.well-known/nodeinfo' || p === '/nodeinfo/2.0';
|
|
122
130
|
if (apiPath) {
|
|
123
131
|
res.setHeader('access-control-allow-origin', '*');
|
|
124
132
|
res.setHeader('access-control-expose-headers', 'Link');
|
|
@@ -142,11 +150,11 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
|
142
150
|
res.end('forbidden\n');
|
|
143
151
|
return;
|
|
144
152
|
}
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
153
|
+
// Embedded, the operator's door is one path on the pod's origin (under the
|
|
154
|
+
// mount, when there is one). Behind it is everything that was the admin
|
|
155
|
+
// server; in front of it are the protocol routes, which have to answer
|
|
156
|
+
// strangers because that is what makes the pod an instance other software
|
|
157
|
+
// can talk to.
|
|
150
158
|
let atDoor = !embedded;
|
|
151
159
|
if (embedded && basePath !== '/'
|
|
152
160
|
&& (p === basePath.slice(0, -1) || p.startsWith(basePath))) {
|
|
@@ -171,7 +179,7 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
|
171
179
|
// above still decides who gets this far.
|
|
172
180
|
if (p === '/.well-known/oauth-authorization-server') {
|
|
173
181
|
const scheme = req.socket.encrypted || req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
|
|
174
|
-
return json(res, 200, masto.authorizationServerMetadata(`${scheme}://${req.headers.host}`));
|
|
182
|
+
return json(res, 200, masto.authorizationServerMetadata(`${scheme}://${req.headers.host}${mount}`));
|
|
175
183
|
}
|
|
176
184
|
if (atDoor && gate(req, res)) return;
|
|
177
185
|
if (p === '/api/v1/streaming/health') {
|
|
@@ -180,7 +188,7 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
|
180
188
|
// NodeInfo on the agent origin — clients probe it at login.
|
|
181
189
|
if (p === '/.well-known/nodeinfo') {
|
|
182
190
|
return json(res, 200, nodeinfoPointer(
|
|
183
|
-
`${req.socket.encrypted || req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http'}://${req.headers.host}/nodeinfo/2.0`));
|
|
191
|
+
`${req.socket.encrypted || req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http'}://${req.headers.host}${mount}/nodeinfo/2.0`));
|
|
184
192
|
}
|
|
185
193
|
if (p === '/nodeinfo/2.0') {
|
|
186
194
|
return json(res, 200, nodeinfoDoc({
|
|
@@ -206,8 +214,8 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
|
206
214
|
// Our own pages come before the group check: a group is set up in the
|
|
207
215
|
// browser like anything else, and it has a record to edit. It still
|
|
208
216
|
// serves no fediverse client — see the 404 two lines down.
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
217
|
+
const wmount = webMount(p);
|
|
218
|
+
if (wmount) {
|
|
211
219
|
// Without the slash a page's own relative <script src> resolves one
|
|
212
220
|
// level up and 404s — and that is true at any depth, so ask the
|
|
213
221
|
// filesystem rather than only special-casing the mount itself.
|
|
@@ -217,7 +225,7 @@ export function buildAdminSurface({ agent, gate, allowed, log = console.log,
|
|
|
217
225
|
res.end();
|
|
218
226
|
return;
|
|
219
227
|
}
|
|
220
|
-
return serveWeb(res, p,
|
|
228
|
+
return serveWeb(res, p, wmount, allowed);
|
|
221
229
|
}
|
|
222
230
|
// The bare URL means "show me what this agent wants from me now".
|
|
223
231
|
// Keyed on the credential FILE, never on configured(): a healthy
|
|
@@ -18,7 +18,7 @@ export async function setup() {
|
|
|
18
18
|
if (process.stdin.isTTY && !has('cli') && !IDENTITY_FLAGS.some(f => args.includes('--' + f))) {
|
|
19
19
|
return runBrowserSetup();
|
|
20
20
|
}
|
|
21
|
-
const root = flag('root');
|
|
21
|
+
const root = flag('root') || 'fedipod/'; // new installs default to the fedipod/ container
|
|
22
22
|
const kind = has('group') ? 'group' : 'person';
|
|
23
23
|
const approveJoins = has('group') && has('approve-joins');
|
|
24
24
|
const summary = flag('summary');
|
|
@@ -48,6 +48,14 @@ if (!newAccount && !pod) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
if (!newAccount && !pod) { console.error('no pod given'); process.exit(2); }
|
|
51
|
+
if (!newAccount) {
|
|
52
|
+
const { resourceExists } = await import(new URL('../../../../lib/pod/root.mjs', import.meta.url));
|
|
53
|
+
const { apUrls, DEFAULT_ROOT: DR } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
|
|
54
|
+
if (await resourceExists(fetch, apUrls(pod, DR).actor)) {
|
|
55
|
+
console.error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
51
59
|
|
|
52
60
|
const issuer = flag('issuer') || await ask('Solid identity provider', 'https://solidcommunity.net');
|
|
53
61
|
// Before the password is asked for, let alone sent. The issuer is where it
|
|
@@ -79,9 +87,26 @@ const name = flag('name') || await ask('display name (shown above your address)'
|
|
|
79
87
|
// A handle resolves through <host>/.well-known/webfinger, so it only works
|
|
80
88
|
// when the pod owns the root of its host. Whether a NEW pod gets its own
|
|
81
89
|
// subdomain is the server's call, so promise nothing here we cannot keep.
|
|
82
|
-
const { webfingerHost } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
|
|
90
|
+
const { webfingerHost, apUrls, DEFAULT_ROOT } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
|
|
83
91
|
const issuerHost = new URL(issuer).host;
|
|
84
92
|
const wfHost = newAccount ? null : webfingerHost(pod);
|
|
93
|
+
|
|
94
|
+
// Where the address lives: on the pod (default), or at a gateway. A pod on a
|
|
95
|
+
// suffix-based host cannot answer WebFinger for a handle, so its address
|
|
96
|
+
// is at the gateway whatever was asked. `--address front` takes a gateway
|
|
97
|
+
// address for a host-root pod too; `--gateway <origin>` names the gateway.
|
|
98
|
+
const addressShape = String(flag('address') || 'pod').toLowerCase();
|
|
99
|
+
if (addressShape !== 'pod' && addressShape !== 'front') { console.error('--address must be pod or front'); process.exit(2); }
|
|
100
|
+
const gatewayOrigin = flag('gateway') || 'https://fedipod.net';
|
|
101
|
+
{ const badGw = insecureUrlReason(gatewayOrigin, 'gateway address'); if (badGw) { console.error(badGw); process.exit(2); } }
|
|
102
|
+
const gatewayHost = new URL(gatewayOrigin).host;
|
|
103
|
+
// For an EXISTING pod the shape is known now; for a NEW pod the path case is
|
|
104
|
+
// decided after the pod is made, below.
|
|
105
|
+
let fronted = (!newAccount && !wfHost) || addressShape === 'front';
|
|
106
|
+
if (fronted && kind === 'group') {
|
|
107
|
+
console.error('a group needs a pod at its own host — a gateway address for a group is not supported yet.');
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
85
110
|
// A person warned about an unresolvable handle is the one who suffers, so a
|
|
86
111
|
// warning is their call to accept. Nobody could ever find this group, and the
|
|
87
112
|
// people it would fail are not the operator reading the warning.
|
|
@@ -93,20 +118,16 @@ if (kind === 'group' && !newAccount && !wfHost) {
|
|
|
93
118
|
}
|
|
94
119
|
console.log(kind === 'group' ? '\nThe group will be:\n' : '\nYou will be:\n');
|
|
95
120
|
console.log(` ${name}`);
|
|
96
|
-
if (
|
|
121
|
+
if (fronted) {
|
|
122
|
+
console.log(` @${handle}@${gatewayHost}\n`);
|
|
123
|
+
console.log(`— your address lives at ${gatewayHost}; your posts, key and data stay on your pod.\n`);
|
|
124
|
+
} else if (wfHost) {
|
|
97
125
|
console.log(` @${handle}@${wfHost}\n`);
|
|
98
126
|
} else if (newAccount) {
|
|
99
127
|
console.log(` @${handle}@${podName}.${issuerHost}\n`);
|
|
100
|
-
console.log(`— provided ${issuerHost} gives each pod its own subdomain.
|
|
101
|
-
console.log(`pods at ${issuerHost}/${podName}/ instead
|
|
102
|
-
console.log(
|
|
103
|
-
console.log('before publishing anything.\n');
|
|
104
|
-
} else {
|
|
105
|
-
console.log(` @${handle}@${new URL(pod).host} — WILL NOT RESOLVE\n`);
|
|
106
|
-
console.log(`This pod is ${pod} — a path on ${new URL(pod).host}, not the root of its own`);
|
|
107
|
-
console.log('host. WebFinger is answered only at a host root, which this pod cannot');
|
|
108
|
-
console.log('write to, so other servers will not find you. Posting and reading still');
|
|
109
|
-
console.log('work; being discovered does not.\n');
|
|
128
|
+
console.log(`— provided ${issuerHost} gives each pod its own subdomain. A server that puts`);
|
|
129
|
+
console.log(`pods at ${issuerHost}/${podName}/ instead cannot answer WebFinger for an address,`);
|
|
130
|
+
console.log(`so setup takes an address at ${gatewayHost} for you and says so.\n`);
|
|
110
131
|
}
|
|
111
132
|
console.log('The display name can be changed later; the handle and pod cannot.');
|
|
112
133
|
const go = await ask(newAccount
|
|
@@ -123,14 +144,15 @@ if (newAccount) {
|
|
|
123
144
|
pod = made.pod;
|
|
124
145
|
console.log(`account + pod created: ${pod}`);
|
|
125
146
|
if (!webfingerHost(pod)) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (!/^y/i.test(cont)) {
|
|
131
|
-
console.log('stopping \u2014 the pod exists, but no actor was published');
|
|
147
|
+
if (kind === 'group') {
|
|
148
|
+
console.log(`\n${issuerHost} created the pod at ${pod} — a suffix-based host.`);
|
|
149
|
+
console.log('WebFinger is answered only at a host root, so nobody could find this group.');
|
|
150
|
+
console.log('The pod exists; no actor was published.');
|
|
132
151
|
process.exit(0);
|
|
133
152
|
}
|
|
153
|
+
// A path pod's address lives at the gateway; nothing to warn about.
|
|
154
|
+
fronted = true;
|
|
155
|
+
console.log(`${issuerHost} puts pods on paths, so your address will be @${handle}@${gatewayHost}.`);
|
|
134
156
|
}
|
|
135
157
|
}
|
|
136
158
|
|
|
@@ -164,13 +186,23 @@ if (rootOf(HOME) === AP_ROOT) recordLastUsed(AP_ROOT, path.basename(HOME));
|
|
|
164
186
|
recordAgent({ port: PORT, handle }); // later commands need no --port
|
|
165
187
|
console.log(`credential minted and saved to ${path.join(HOME, 'credential.json')}`);
|
|
166
188
|
|
|
189
|
+
let gatewayCfg = null;
|
|
190
|
+
if (fronted) {
|
|
191
|
+
const { takeGatewayAddress } = await import(new URL('../../../../lib/device/setup.mjs', import.meta.url));
|
|
192
|
+
const urls = apUrls(pod, root || DEFAULT_ROOT);
|
|
193
|
+
console.log(`taking a gateway address at ${gatewayHost}`);
|
|
194
|
+
gatewayCfg = await takeGatewayAddress({
|
|
195
|
+
home: HOME, credential: rec, gatewayOrigin, handle,
|
|
196
|
+
podHome: urls.home, actorUrl: urls.actor, kind, log: (...a) => console.log('[setup]', ...a),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
167
199
|
const { Agent } = await import(new URL('../../../../run-agent.mjs', import.meta.url));
|
|
168
200
|
const agent = new Agent({ home: HOME, log: (...a) => console.log('[setup]', ...a) });
|
|
169
|
-
await agent.bootstrap({ handle, name, root, kind, approveJoins, summary, icon });
|
|
201
|
+
await agent.bootstrap({ handle, name, root, kind, approveJoins, summary, icon, gateway: gatewayCfg });
|
|
170
202
|
await agent.connect({ repair: false }); // publishProfile below is the publish
|
|
171
203
|
await agent.publisher.publishProfile();
|
|
172
204
|
await agent.store.flush();
|
|
173
|
-
const finalHost = webfingerHost(rec.remotePod);
|
|
205
|
+
const finalHost = gatewayCfg ? gatewayHost : webfingerHost(rec.remotePod);
|
|
174
206
|
const what = kind === 'group' ? 'group' : 'actor';
|
|
175
207
|
console.log(finalHost
|
|
176
208
|
? `${what} published: @${handle}@${finalHost}`
|