fedipod 1.39.0 → 1.42.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 +20 -7
- package/architecture.md +9 -3
- package/architecture.svg +93 -85
- package/gateway.md +73 -7
- package/lib/client/c2s.mjs +16 -5
- package/lib/client/masto/bridge.mjs +61 -0
- package/lib/client/masto/index.mjs +4 -1
- package/lib/client/masto/oauth.mjs +2 -2
- package/lib/client/masto/statuses.mjs +3 -0
- package/lib/connections/acctfeed.mjs +13 -9
- package/lib/connections/bskyfeed.mjs +13 -9
- package/lib/connections/tagfeed.mjs +14 -9
- package/lib/connections/vault.mjs +1 -1
- package/lib/core/agent.mjs +720 -0
- package/lib/core/gateway-move.mjs +58 -0
- package/lib/core/intake/index.mjs +193 -46
- package/lib/core/intake/verify.mjs +11 -0
- package/lib/core/keys.mjs +1 -1
- package/lib/core/lease.mjs +19 -4
- package/lib/core/pod-only.mjs +4 -0
- package/lib/core/publisher/index.mjs +6 -6
- package/lib/core/publisher/notes.mjs +73 -4
- package/lib/core/remote.mjs +54 -0
- package/lib/core/scheduled.mjs +39 -0
- package/lib/core/shapes/shapes-text.mjs +11 -3
- package/lib/core/storage.mjs +67 -0
- package/lib/core/store.mjs +66 -4
- package/lib/core/wire.mjs +4 -1
- package/lib/device/admin/index.mjs +4 -11
- package/lib/device/admin/routes/setup.mjs +60 -5
- package/lib/device/admin/server.mjs +18 -5
- package/lib/device/cli/commands/account.mjs +1 -1
- package/lib/device/cli/commands/run.mjs +2 -2
- package/lib/device/cli/commands/service.mjs +1 -1
- package/lib/device/cli/commands/setup.mjs +1 -1
- package/lib/device/cli/commands/state.mjs +1 -1
- package/lib/device/cli/context.mjs +1 -1
- package/lib/{gateway → device}/directory.mjs +1 -1
- package/lib/device/gateway-move.mjs +3 -58
- package/lib/device/home.mjs +3 -50
- package/lib/{client → device}/localapi.mjs +2 -2
- package/lib/device/remote.mjs +3 -54
- package/lib/gateway/account-agent.mjs +111 -0
- package/lib/gateway/copy.mjs +387 -0
- package/lib/gateway/front-core.mjs +30 -25
- package/lib/gateway/front-pages.mjs +31 -0
- package/lib/gateway/gateway-core.mjs +20 -1
- package/lib/gateway/held-mail.mjs +198 -0
- package/lib/gateway/keeper-due.mjs +39 -0
- package/lib/gateway/keeper-session.mjs +19 -0
- package/lib/gateway/keeper.mjs +72 -0
- package/lib/gateway/masto-gateway.mjs +559 -0
- package/lib/gateway/outbox-door.mjs +31 -0
- package/lib/gateway/quiet.mjs +7 -2
- package/lib/gateway/relay-extras.mjs +6 -6
- package/lib/gateway/state-api.mjs +239 -0
- package/lib/pod/containers.mjs +17 -0
- package/lib/pod/transport.mjs +43 -2
- package/lib/server/embed.mjs +4 -4
- package/lib/session/README.md +5 -5
- package/lib/session/demo.html +1 -1
- package/lib/session/package.json +1 -1
- package/lib/shared/files.mjs +49 -0
- package/lib/shared/ua.mjs +3 -0
- package/lib/surface/index.mjs +7 -0
- package/lib/{device/admin → surface}/origins.mjs +1 -1
- package/lib/{device/admin → surface}/routes/gateway.mjs +7 -11
- package/lib/{device/admin → surface}/routes/lifecycle.mjs +3 -3
- package/lib/{device/admin → surface}/routes/owner.mjs +4 -60
- package/lib/{device/admin → surface}/routes/social.mjs +1 -1
- package/lib/{device/admin → surface}/static.mjs +6 -1
- package/lib/{device/admin → surface}/surface.mjs +19 -16
- package/package.json +23 -5
- package/run-agent.mjs +18 -726
- package/scripts/build-app.mjs +4 -0
- package/scripts/check-netlify-bundles.mjs +84 -0
- package/scripts/check-pod-calls.mjs +42 -0
- package/scripts/stage-site.mjs +17 -4
- package/scripts/vendor-sanitize.mjs +27 -0
- package/vendor/idp-grant.cjs +11 -5
- package/vendor/sanitize-html.cjs +11456 -0
- package/web/admin/gateway.js +14 -1
- package/web/admin/index.html +4 -0
- package/web/admin/oauth-signin.mjs +1 -1
- package/web/app/README.md +1 -1
- package/web/app/admin-facade.mjs +11 -1
- package/web/app/agent.mjs +222 -52
- package/web/app/boot.mjs +3 -0
- package/web/app/copy-mode.mjs +247 -0
- package/web/app/dist/boot.js +68 -2
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +1139 -222
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/sw-src.mjs +15 -53
- package/web/app/update.js +3 -2
- package/web/app/warm-start.mjs +115 -0
- package/web/app-signin/app-signin.mjs +78 -0
- package/web/app-signin/index.html +49 -0
- package/browser.svg +0 -72
- /package/lib/{device/admin → surface}/routes/connections.mjs +0 -0
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
FediPod gives you a Fediverse account whose data lives on a Solid pod. You
|
|
6
6
|
follow people on Mastodon, Bluesky, and other Fediverse or ATProto servers in
|
|
7
|
-
one timeline. Your posts, followers and settings
|
|
7
|
+
one timeline. Your posts, followers and settings are kept on your pod. While fedipod.net
|
|
8
|
+
keeps your account running, it works from a copy of them and writes it to your
|
|
9
|
+
pod every fifteen minutes; your signing key never leaves your pod.
|
|
8
10
|
|
|
9
11
|
The easiest way to run FediPod is to use it in any browser at https://fedipod.net. Nothing to install. Sign-up points you to a pod provider if you need a pod, then attaches a Fediverse identity to the pod you sign in with.
|
|
10
12
|
|
|
@@ -80,6 +82,11 @@ If your account turns a post down when it runs, you get a direct message from
|
|
|
80
82
|
yourself saying what was refused and why. Signed in, your outbox shows you
|
|
81
83
|
everything you've done, likes and follows included; everyone else sees only
|
|
82
84
|
your public posts.
|
|
85
|
+
While fedipod.net keeps your account running, a post from such an app goes out
|
|
86
|
+
in a moment, whether or not FediPod is open.
|
|
87
|
+
A comment you post from dokieli names nobody to send it to, so it goes to
|
|
88
|
+
nobody. It is kept on your pod at its own address, where the page you
|
|
89
|
+
commented on points to it. Your own timeline shows it as a private post.
|
|
83
90
|
|
|
84
91
|
**The manage page.** `manage`, in the bar's account group, opens it: your profile,
|
|
85
92
|
aliases, the gateway, key rotation, recovering posts, parking, moving to
|
|
@@ -92,14 +99,16 @@ there it takes over; the first drops back to reading.
|
|
|
92
99
|
|
|
93
100
|
## What the browser version does not do
|
|
94
101
|
|
|
95
|
-
- **Scheduled posts
|
|
102
|
+
- **Scheduled posts, if you stop fedipod.net keeping your account running.**
|
|
103
|
+
It does by default, and publishes them while FediPod is closed. Turned off
|
|
104
|
+
on the manage page, nothing runs between now and the time you picked, so
|
|
96
105
|
the composer refuses one rather than dropping it later.
|
|
97
|
-
- **Notifications
|
|
98
|
-
|
|
106
|
+
- **Notifications, and other clients, if you stop fedipod.net keeping your
|
|
107
|
+
account running.** While it does (the default), any Mastodon app, phone or
|
|
108
|
+
desktop, can use your account with `fedipod.net` as its server, and an app
|
|
109
|
+
that offers notifications gets them on your phone with everything closed.
|
|
99
110
|
- **Live updates.** The client refreshes by polling.
|
|
100
111
|
- **Hosting a group.** Joining one works.
|
|
101
|
-
- **Other clients.** A phone app or desktop client has nothing on the network
|
|
102
|
-
to connect to.
|
|
103
112
|
|
|
104
113
|
For these, see [Other ways to run FediPod](#other-ways-to-run-fedipod).
|
|
105
114
|
|
|
@@ -108,6 +117,10 @@ For these, see [Other ways to run FediPod](#other-ways-to-run-fedipod).
|
|
|
108
117
|
Everything you publish and everything you read is stored on your pod. Your
|
|
109
118
|
signing key is stored there too, in a container only you can read through
|
|
110
119
|
your pod's login.
|
|
120
|
+
While fedipod.net keeps your account running, its own pod identity can read
|
|
121
|
+
that container too, so it can act for you while FediPod is closed, and it
|
|
122
|
+
works from a copy of your account's data that it writes back to your pod every
|
|
123
|
+
fifteen minutes. Your key stays on your pod only.
|
|
111
124
|
It all lives in the `fedipod` container you chose at sign-up. Your pod's
|
|
112
125
|
public type index records it, as an ActivityStreams actor, which is how
|
|
113
126
|
FediPod and other Solid apps find your account again.
|
|
@@ -119,7 +132,7 @@ not change.
|
|
|
119
132
|
|
|
120
133
|
## Other ways to use FediPod
|
|
121
134
|
|
|
122
|
-
- [fediverse-
|
|
135
|
+
- [fediverse-session](lib/session/README.md) - an ESM library that supports both Solid-based and regular Fediverse accounts with methods to login, reply, boost, etc.
|
|
123
136
|
- [FediPod Server](packages/fedipod-server/README.md): a full ActivityPub
|
|
124
137
|
server as a Community Solid Server component, giving every pod on the server
|
|
125
138
|
the option of a Fediverse account.
|
package/architecture.md
CHANGED
|
@@ -16,13 +16,19 @@ and forwards the rest to the pod inbox with a receipt. It holds no key. The
|
|
|
16
16
|
browser version always has one; the DeviceAgent may use one. Any
|
|
17
17
|
lightweight host will do, Netlify included.
|
|
18
18
|
|
|
19
|
+
A gateway can also keep a browser account running. It then acts as that
|
|
20
|
+
account's agent whenever FediPod is closed, under a pod identity of its own
|
|
21
|
+
that the owner's rules name, reading the signing key from the pod when it
|
|
22
|
+
needs it. The account's state documents live in a working copy at the
|
|
23
|
+
gateway, which FediPod in the browser, the gateway and any Mastodon app all
|
|
24
|
+
work from, and which the gateway writes to the pod every fifteen minutes. See
|
|
25
|
+
[the gateway](gateway.md).
|
|
26
|
+
|
|
19
27
|
[FediPod Server](packages/fedipod-server/README.md) puts the agent inside a
|
|
20
28
|
Community Solid Server, so anyone with a pod on that server can opt in to a
|
|
21
29
|
Fediverse account fed by the server itself.
|
|
22
30
|
|
|
23
|
-

|
|
31
|
+

|
|
26
32
|
|
|
27
33
|
## Protocol conformance
|
|
28
34
|
|
package/architecture.svg
CHANGED
|
@@ -1,89 +1,97 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1300 680" font-family="Arial, Helvetica, sans-serif">
|
|
2
2
|
<defs>
|
|
3
|
-
<marker id="
|
|
4
|
-
<path d="M0,0 L10,5 L0,10 z" fill="#
|
|
3
|
+
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
4
|
+
<path d="M0,0 L10,5 L0,10 z" fill="#333"/>
|
|
5
5
|
</marker>
|
|
6
6
|
</defs>
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
7
|
+
<rect x="0" y="0" width="1300" height="680" fill="#ffffff"/>
|
|
8
|
+
|
|
9
|
+
<!-- boxes (draw.io palette, as browser.svg) -->
|
|
10
|
+
<g stroke-width="1.5">
|
|
11
|
+
<rect x="450" y="24" width="280" height="84" rx="6" fill="#dae8fc" stroke="#6c8ebf"/>
|
|
12
|
+
<rect x="260" y="175" width="650" height="215" rx="10" fill="#f5f5f5" stroke="#666666" stroke-dasharray="8 6"/>
|
|
13
|
+
<rect x="285" y="230" width="230" height="130" rx="6" fill="#d5e8d4" stroke="#82b366"/>
|
|
14
|
+
<rect x="665" y="230" width="230" height="130" rx="6" fill="#e1d5e7" stroke="#9673a6"/>
|
|
15
|
+
<rect x="40" y="500" width="300" height="110" rx="6" fill="#f8cecc" stroke="#b85450"/>
|
|
16
|
+
<rect x="780" y="500" width="300" height="110" rx="6" fill="#fff2cc" stroke="#d6b656"/>
|
|
17
|
+
<rect x="1040" y="250" width="220" height="95" rx="6" fill="#ffe6cc" stroke="#d79b00"/>
|
|
18
|
+
</g>
|
|
19
|
+
|
|
20
|
+
<!-- box labels -->
|
|
21
|
+
<g fill="#111" text-anchor="middle">
|
|
22
|
+
<text x="590" y="60" font-size="20" font-weight="bold">Open Social Web</text>
|
|
23
|
+
<text x="590" y="88" font-size="16">Mastodon · Pixelfed · Bluesky …</text>
|
|
24
|
+
|
|
25
|
+
<text x="280" y="206" font-size="22" font-weight="bold" text-anchor="start">FediPod</text>
|
|
26
|
+
|
|
27
|
+
<text x="400" y="266" font-size="20" font-weight="bold">BrowserAgent</text>
|
|
28
|
+
<text x="400" y="294" font-size="16">FediPod in any browser,</text>
|
|
29
|
+
<text x="400" y="318" font-size="16">with Sengi or Phanpy</text>
|
|
30
|
+
|
|
31
|
+
<text x="780" y="266" font-size="20" font-weight="bold">Gateway</text>
|
|
32
|
+
<text x="780" y="294" font-size="16">at fedipod.net, keeps</text>
|
|
33
|
+
<text x="780" y="318" font-size="16">your account running</text>
|
|
34
|
+
<text x="780" y="342" font-size="16">from a working copy</text>
|
|
35
|
+
|
|
36
|
+
<text x="190" y="536" font-size="20" font-weight="bold">Your Pod</text>
|
|
37
|
+
<text x="190" y="564" font-size="16">your key, your posts</text>
|
|
38
|
+
<text x="190" y="588" font-size="16">and your account's data</text>
|
|
39
|
+
|
|
40
|
+
<text x="930" y="536" font-size="20" font-weight="bold">Mastodon Apps</text>
|
|
41
|
+
<text x="930" y="564" font-size="16">Elk · Ivory · Tusky · Fedilab</text>
|
|
42
|
+
<text x="930" y="588" font-size="16">on your phone or desktop</text>
|
|
43
|
+
|
|
44
|
+
<text x="1150" y="289" font-size="20" font-weight="bold">C2S Apps</text>
|
|
45
|
+
<text x="1150" y="317" font-size="16">box, Dokieli</text>
|
|
46
|
+
</g>
|
|
47
|
+
|
|
48
|
+
<!-- arrows -->
|
|
49
|
+
<g stroke="#333" stroke-width="1.6" fill="none">
|
|
50
|
+
<!-- Open Social Web <-> BrowserAgent (ATProto, Bluesky) -->
|
|
51
|
+
<line x1="520" y1="108" x2="410" y2="230" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
|
52
|
+
<!-- Open Social Web <-> Gateway (ActivityPub mail) -->
|
|
53
|
+
<line x1="660" y1="108" x2="770" y2="230" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
|
54
|
+
<!-- BrowserAgent <-> Gateway -->
|
|
55
|
+
<line x1="515" y1="298" x2="665" y2="298" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
|
56
|
+
<!-- Your Pod -> Open Social Web (public posts and profile) -->
|
|
57
|
+
<path d="M100,500 C0,400 0,110 450,70" marker-end="url(#arrow)"/>
|
|
58
|
+
<!-- BrowserAgent <-> Your Pod (Solid) -->
|
|
59
|
+
<line x1="350" y1="360" x2="230" y2="500" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
|
60
|
+
<!-- Gateway <-> Your Pod (stores the account's data) -->
|
|
61
|
+
<line x1="720" y1="360" x2="340" y2="560" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
|
62
|
+
<!-- Mastodon Apps <-> Gateway -->
|
|
63
|
+
<line x1="900" y1="500" x2="840" y2="360" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
|
64
|
+
<!-- C2S Apps <-> Gateway -->
|
|
65
|
+
<line x1="1040" y1="298" x2="895" y2="298" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
|
66
|
+
</g>
|
|
67
|
+
|
|
68
|
+
<!-- arrow labels -->
|
|
69
|
+
<g fill="#333" font-size="16">
|
|
70
|
+
<text x="470" y="140" text-anchor="end" font-weight="bold" fill="#111">ATProto Protocol</text>
|
|
71
|
+
<text x="470" y="160" text-anchor="end">for Bluesky</text>
|
|
72
|
+
|
|
73
|
+
<text x="712" y="140" text-anchor="start" font-weight="bold" fill="#111">S2S Protocol</text>
|
|
74
|
+
<text x="712" y="160" text-anchor="start">mail in, posts out</text>
|
|
75
|
+
|
|
76
|
+
<text x="590" y="286" text-anchor="middle">serves the app</text>
|
|
77
|
+
<text x="590" y="320" text-anchor="middle">posts out</text>
|
|
78
|
+
|
|
79
|
+
<text x="968" y="286" text-anchor="middle" font-weight="bold" fill="#111">C2S Protocol</text>
|
|
80
|
+
|
|
81
|
+
<text x="85" y="250" text-anchor="start">other servers read</text>
|
|
82
|
+
<text x="85" y="270" text-anchor="start">your public posts</text>
|
|
83
|
+
<text x="85" y="290" text-anchor="start">and profile</text>
|
|
84
|
+
|
|
85
|
+
<text x="272" y="420" text-anchor="end" font-weight="bold" fill="#111">Solid Protocol</text>
|
|
86
|
+
<text x="272" y="440" text-anchor="end">reads & writes your pod</text>
|
|
87
|
+
|
|
88
|
+
<text x="520" y="490" text-anchor="start">stores the working copy</text>
|
|
89
|
+
<text x="520" y="510" text-anchor="start">on your pod</text>
|
|
90
|
+
|
|
91
|
+
<text x="894" y="415" text-anchor="start" font-weight="bold" fill="#111">Mastodon client API</text>
|
|
92
|
+
<text x="894" y="435" text-anchor="start">sign in at fedipod.net;</text>
|
|
93
|
+
<text x="894" y="455" text-anchor="start">notifications to your phone</text>
|
|
94
|
+
</g>
|
|
95
|
+
|
|
96
|
+
<text x="1260" y="660" text-anchor="end" font-size="16" fill="#555">© 2026 Jeff Zucker · CC BY-SA 4.0</text>
|
|
89
97
|
</svg>
|
package/gateway.md
CHANGED
|
@@ -6,13 +6,16 @@ each delivery's signature where the headers still exist, drops forgeries and
|
|
|
6
6
|
junk before they ever touch your pod, and passes the rest on with a receipt
|
|
7
7
|
saying it checked.
|
|
8
8
|
|
|
9
|
-
Your name, your signing key and your data
|
|
9
|
+
Your name, your signing key and your data are kept on your own pod. The gateway is
|
|
10
10
|
**keyless** — it never holds the key you sign with, so it cannot post as you,
|
|
11
11
|
read your private things, or be you anywhere. The worst a broken one can do is
|
|
12
12
|
push items into your inbox, and those still face your agent's own checks.
|
|
13
13
|
With the outbox door below, a gateway can also hand your agent a post marked as
|
|
14
14
|
yours, which your agent then signs and sends. It still holds no key, but what you
|
|
15
|
-
trust it with grows by that much.
|
|
15
|
+
trust it with grows by that much. A browser account is also kept running by
|
|
16
|
+
the gateway (below). That gives the gateway your key while it works, so it can
|
|
17
|
+
post as you, as any Fediverse server can, and your account's working data is
|
|
18
|
+
kept at the gateway too and written to your pod every fifteen minutes.
|
|
16
19
|
|
|
17
20
|
A FediPod install works without any gateway at all. Deliveries go straight to
|
|
18
21
|
your pod inbox, which holds them whether your agent is running or not.
|
|
@@ -114,6 +117,11 @@ there, signed in at your pod. The door checks that the token is yours, puts the
|
|
|
114
117
|
post in your inbox marked as yours, and answers with the address the post will
|
|
115
118
|
have. Your agent publishes it and sends it to your followers the next time it
|
|
116
119
|
runs: for a browser account, the next time you open the site.
|
|
120
|
+
While the gateway keeps a browser account running, it starts the account's
|
|
121
|
+
keeper as the post arrives, so the post goes out in a moment.
|
|
122
|
+
A comment from dokieli names nobody to send it to, so it is sent to nobody. It
|
|
123
|
+
is kept at its own address for the page it is on to point at, and only your
|
|
124
|
+
own outbox lists it.
|
|
117
125
|
|
|
118
126
|
A post with no audience of its own goes out as a public post. A post that is
|
|
119
127
|
not a note, an annotation say, is kept as the app sent it, under your name.
|
|
@@ -153,10 +161,61 @@ on the setup page). Setup reads the account on the pod, keeps its state and
|
|
|
153
161
|
key there, attaches at the new gateway, and completes the move when the
|
|
154
162
|
agent first acts. See [the DeviceAgent](device-agent.md).
|
|
155
163
|
|
|
164
|
+
## While your app is closed
|
|
165
|
+
|
|
166
|
+
A browser account runs only while FediPod is open somewhere. While it is
|
|
167
|
+
closed, fedipod.net does two things for it.
|
|
168
|
+
|
|
169
|
+
**Your mail waits at the gateway.** Deliveries are kept there, not written to
|
|
170
|
+
your pod one by one, and reach your pod in batches of up to a hundred: when you
|
|
171
|
+
open FediPod, or every fifteen minutes otherwise. While FediPod is open, mail
|
|
172
|
+
goes straight to your pod as before. A DeviceAgent's mail always goes straight
|
|
173
|
+
to its pod.
|
|
174
|
+
|
|
175
|
+
**The gateway keeps your account running.** Unless you turn it off, the
|
|
176
|
+
gateway accepts your follows, tries again what failed to go out, and publishes
|
|
177
|
+
the posts you scheduled, all while FediPod is closed. Other mail waits on your
|
|
178
|
+
pod for FediPod to open, as before. It works under its own pod identity, which your app
|
|
179
|
+
names in the access rules on your FediPod folder. It reads your signing key
|
|
180
|
+
from your pod when it needs it, so it can post as you, and keeps no copy of the
|
|
181
|
+
key. **Stop keeping it running** on the manage page takes it out of the rules.
|
|
182
|
+
If fedipod.net ever changes its own pod identity, your FediPod moves your
|
|
183
|
+
account over the next time you open it, with nothing for you to do.
|
|
184
|
+
Scheduling a post is offered only while this is on, because otherwise nothing
|
|
185
|
+
would be running when the time came.
|
|
186
|
+
|
|
187
|
+
**Your account's working data is kept at the gateway.** While the gateway
|
|
188
|
+
keeps your account running, your timeline, notifications, followers, settings
|
|
189
|
+
and the rest of your FediPod state are kept at fedipod.net, and FediPod in your
|
|
190
|
+
browser, the gateway and any app you use all work from that one copy, so they
|
|
191
|
+
never disagree. fedipod.net writes what changed to your pod every fifteen
|
|
192
|
+
minutes, and everything at once when you stop it keeping your account running,
|
|
193
|
+
move your address or close it. Your signing key and the passwords of accounts
|
|
194
|
+
you connected elsewhere are never in the copy; they stay on your pod only.
|
|
195
|
+
|
|
196
|
+
## Using any Mastodon app
|
|
197
|
+
|
|
198
|
+
While the gateway keeps your account running, any Mastodon app can use it:
|
|
199
|
+
elk.zone, Ivory, Tusky, Phanpy and the rest. In the app, give `fedipod.net` as
|
|
200
|
+
your server. The app sends you to a fedipod.net page that asks for your
|
|
201
|
+
Fediverse address or your WebID and signs you in at your own pod; no password
|
|
202
|
+
is typed on fedipod.net. The app then reads your timeline and notifications from your
|
|
203
|
+
account's copy at the gateway, and posts, boosts, likes and follows as you.
|
|
204
|
+
|
|
205
|
+
An app checks for new posts every minute or so while it is open; there are no
|
|
206
|
+
live updates. Accounts are made on the fedipod.net front page, with a pod, not
|
|
207
|
+
from an app.
|
|
208
|
+
|
|
209
|
+
**Notifications on your phone.** An app that offers notifications can sign up
|
|
210
|
+
for them here. A mention, a reply, a direct message, a follow, or a like, boost
|
|
211
|
+
or reaction to your own post then reaches your phone as it arrives, while
|
|
212
|
+
FediPod and the app are closed. While FediPod is open in a browser, it reads
|
|
213
|
+
your mail itself, and those arrivals are not pushed.
|
|
214
|
+
|
|
156
215
|
## Accounts that go quiet
|
|
157
216
|
|
|
158
|
-
|
|
159
|
-
|
|
217
|
+
Every delivery the gateway accepts ends up in your pod inbox, and your agent
|
|
218
|
+
reads it from there. A BrowserAgent reads only while
|
|
160
219
|
its page is open, so an account nobody opens grows on its pod without limit
|
|
161
220
|
and comes back to a drain of everything at once. fedipod.net keeps two facts
|
|
162
221
|
about each browser account — when its owner last signed in or posted, and
|
|
@@ -194,9 +253,16 @@ web address becomes a link.
|
|
|
194
253
|
|
|
195
254
|
It reads only public data to decide what concerns you: your published
|
|
196
255
|
followers and following, and a small public policy document your agent writes
|
|
197
|
-
with a mirror of your blocklist.
|
|
198
|
-
|
|
199
|
-
|
|
256
|
+
with a mirror of your blocklist. Publishing that mirror does make your
|
|
257
|
+
blocklist public, which is part of the bargain of running behind a door.
|
|
258
|
+
|
|
259
|
+
Three things go further, all for browser accounts only. Mail that waits at
|
|
260
|
+
the gateway while your app is closed, direct messages included, is stored there
|
|
261
|
+
until it reaches your pod. A gateway keeping your account running can read and
|
|
262
|
+
write everything in your FediPod folder, your signing key included, while it
|
|
263
|
+
works; nothing else on your pod is open to it. And it keeps your account's
|
|
264
|
+
working data — your timeline, notifications and direct messages among it — for
|
|
265
|
+
as long as it keeps your account running.
|
|
200
266
|
|
|
201
267
|
## Running a gateway
|
|
202
268
|
|
package/lib/client/c2s.mjs
CHANGED
|
@@ -48,6 +48,10 @@ const ACTIVITY_TYPES = new Set([
|
|
|
48
48
|
|
|
49
49
|
const arr = (v) => (v === undefined || v === null ? [] : Array.isArray(v) ? v : [v]);
|
|
50
50
|
const idOf = (v) => (typeof v === 'string' ? v : v?.id || null);
|
|
51
|
+
// An audience address. The public collection has three valid spellings
|
|
52
|
+
// (ActivityPub §5.6); reading a post as JSON-LD can yield the short one.
|
|
53
|
+
const PUBLIC_SPELLINGS = new Set(['Public', 'as:Public']);
|
|
54
|
+
const audienceOf = (v) => { const id = idOf(v); return PUBLIC_SPELLINGS.has(id) ? wire.PUBLIC : id; };
|
|
51
55
|
|
|
52
56
|
function readBody(req) {
|
|
53
57
|
return new Promise((resolve, reject) => {
|
|
@@ -63,6 +67,9 @@ function readBody(req) {
|
|
|
63
67
|
});
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
const isAnnotation = (o) => arr(o?.type).some((t) => t === 'Annotation' || t === 'oa:Annotation'
|
|
71
|
+
|| t === 'http://www.w3.org/ns/oa#Annotation');
|
|
72
|
+
|
|
66
73
|
export class C2S {
|
|
67
74
|
constructor({ agent, log = console.log, auth, scheme = null, mount = '' }) {
|
|
68
75
|
this.agent = agent;
|
|
@@ -345,8 +352,8 @@ export class C2S {
|
|
|
345
352
|
// every client means by a post with no audience chosen, and what a client
|
|
346
353
|
// that never addresses (dokieli's annotations) needs.
|
|
347
354
|
visibilityOf(activity, object) {
|
|
348
|
-
const to = arr(activity.to ?? object?.to).map(
|
|
349
|
-
const cc = arr(activity.cc ?? object?.cc).map(
|
|
355
|
+
const to = arr(activity.to ?? object?.to).map(audienceOf);
|
|
356
|
+
const cc = arr(activity.cc ?? object?.cc).map(audienceOf);
|
|
350
357
|
// Blind copies alone are still an audience: a post for them and nobody
|
|
351
358
|
// else is direct, never public.
|
|
352
359
|
const blind = arr(activity.bto ?? object?.bto).length + arr(activity.bcc ?? object?.bcc).length;
|
|
@@ -386,7 +393,7 @@ export class C2S {
|
|
|
386
393
|
// in to/cc are listed and delivered to; the ones in bto/bcc are delivered
|
|
387
394
|
// to and never listed (§6).
|
|
388
395
|
addressedActors(activity, object) {
|
|
389
|
-
const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(
|
|
396
|
+
const pick = (...fields) => [...new Set(fields.flatMap((f) => arr(activity[f] ?? object?.[f]).map(audienceOf)))]
|
|
390
397
|
.filter((a) => typeof a === 'string' && /^https?:\/\//u.test(a)
|
|
391
398
|
&& a !== wire.PUBLIC && a !== this.urls.followers && a !== this.urls.actor);
|
|
392
399
|
// `audience` is who the post is for, a group most often: delivered to and
|
|
@@ -417,12 +424,16 @@ export class C2S {
|
|
|
417
424
|
const visibility = this.visibilityOf(activity, object);
|
|
418
425
|
const { also, deliverTo } = this.addressedActors(activity, object);
|
|
419
426
|
// Not a Note and not a poll: stored as sent, under this actor, and
|
|
420
|
-
// the Create around it delivered — a Web Annotation, for one.
|
|
427
|
+
// the Create around it delivered — a Web Annotation, for one. An
|
|
428
|
+
// annotation addressed to nobody, as dokieli sends one, is delivered
|
|
429
|
+
// to nobody: kept at its address for the page it is on to point at.
|
|
421
430
|
if (makes !== 'Note' && makes !== 'Question') {
|
|
422
431
|
const asSent = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
423
432
|
? (ACTIVITY_TYPES.has(raw.type) ? (raw.object && typeof raw.object === 'object' ? raw.object : null) : raw)
|
|
424
433
|
: null;
|
|
425
|
-
const
|
|
434
|
+
const unaddressed = isAnnotation(asSent || object)
|
|
435
|
+
&& ['to', 'cc', 'bto', 'bcc', 'audience'].every((f) => !arr(activity[f] ?? object[f]).length);
|
|
436
|
+
const made = await agent.publisher.publishObject(asSent || object, { visibility, slug, also, deliverTo, unaddressed });
|
|
426
437
|
return reply(201, { id: made.createId, object: made.id }, { location: made.createId });
|
|
427
438
|
}
|
|
428
439
|
// `source.content` is the client's plain text when it sends one; bare
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// bridge.mjs — a fetch Request handed to code written for Node's request and
|
|
2
|
+
// response: the Mastodon facade and the admin surface. The browser worker
|
|
3
|
+
// answers its own client through it (web/app/sw-src.mjs), and the gateway
|
|
4
|
+
// answers Mastodon apps through it (lib/gateway/masto-gateway.mjs).
|
|
5
|
+
//
|
|
6
|
+
// Returns { req, res, bodyBytes, bodyText, response() }: `response()` is what
|
|
7
|
+
// the handler wrote, as a fetch Response.
|
|
8
|
+
export async function bridge(request, url, { sameOrigin = false } = {}) {
|
|
9
|
+
// Bytes, not text. A multipart upload is binary — read as text it comes back
|
|
10
|
+
// through a UTF-8 round trip that replaces every byte that is not valid UTF-8,
|
|
11
|
+
// which is most of a JPEG, so the boundary search found nothing and every
|
|
12
|
+
// media and avatar upload answered "422 file required". readBody() does
|
|
13
|
+
// `data += chunk`, which decodes a Buffer the same way it always did, so the
|
|
14
|
+
// JSON and form paths are unchanged.
|
|
15
|
+
const bodyBytes = (request.method === 'GET' || request.method === 'HEAD')
|
|
16
|
+
? null : Buffer.from(new Uint8Array(await request.arrayBuffer()));
|
|
17
|
+
const bodyText = bodyBytes ? new TextDecoder().decode(bodyBytes) : '';
|
|
18
|
+
const reqHeaders = {}; for (const [k, v] of request.headers) reqHeaders[k.toLowerCase()] = v;
|
|
19
|
+
// `host` is a forbidden header name, so a fetch Request never carries one and
|
|
20
|
+
// the loop above cannot produce it — but it is a header every real request
|
|
21
|
+
// arrives with, and the agent reads it to say where it lives. Without it the
|
|
22
|
+
// notifications `Link` header named `https://undefined/`, so a client that
|
|
23
|
+
// paged by following it (which is how a client is meant to page) walked off
|
|
24
|
+
// the origin and saw nothing past the first screen.
|
|
25
|
+
reqHeaders.host = url.host;
|
|
26
|
+
const listeners = {};
|
|
27
|
+
// The body's events fire on the next microtask. A route that reads the body
|
|
28
|
+
// may only register for them after an await or two — the facade dispatches
|
|
29
|
+
// through its area modules first — so a listener that arrives after the
|
|
30
|
+
// events have fired is given them at once, in the order it asks.
|
|
31
|
+
let fired = false;
|
|
32
|
+
const req = { method: request.method, url: url.pathname + url.search, headers: reqHeaders,
|
|
33
|
+
// Whether the caller is a page on the agent's own origin. The browser
|
|
34
|
+
// worker has established that before anything reaches the facade, and
|
|
35
|
+
// MastoApi asks (through its authorities) whether a request is the owner's
|
|
36
|
+
// own; a request at the gateway never is.
|
|
37
|
+
sameOrigin,
|
|
38
|
+
socket: { encrypted: url.protocol === 'https:' },
|
|
39
|
+
on(ev, cb) {
|
|
40
|
+
(listeners[ev] ||= []).push(cb);
|
|
41
|
+
if (fired) { if (ev === 'data' && bodyBytes?.length) cb(bodyBytes); else if (ev === 'end') cb(); }
|
|
42
|
+
return req;
|
|
43
|
+
},
|
|
44
|
+
destroy() {} };
|
|
45
|
+
queueMicrotask(() => {
|
|
46
|
+
fired = true;
|
|
47
|
+
if (bodyBytes?.length) (listeners.data || []).forEach((cb) => cb(bodyBytes));
|
|
48
|
+
(listeners.end || []).forEach((cb) => cb());
|
|
49
|
+
});
|
|
50
|
+
let status = 200; const outHeaders = {}; const chunks = [];
|
|
51
|
+
const res = {
|
|
52
|
+
writeHead(s, h) { status = s; if (h) Object.assign(outHeaders, h); return res; },
|
|
53
|
+
setHeader(k, v) { outHeaders[k] = v; }, getHeader(k) { return outHeaders[k]; },
|
|
54
|
+
write(c) { chunks.push(c); }, end(c) { if (c) chunks.push(c); },
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
req, res, bodyBytes, bodyText,
|
|
58
|
+
get status() { return status; },
|
|
59
|
+
response: () => new Response(chunks.join(''), { status, headers: { 'content-type': 'application/json', ...outHeaders } }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -149,7 +149,10 @@ export class MastoApi {
|
|
|
149
149
|
// A viewer-mode agent (another agent holds the drain lease) may not act —
|
|
150
150
|
// but a user acting HERE outranks the idle active agent elsewhere, so a
|
|
151
151
|
// write attempt claims the lease and proceeds. Only a failed claim 503s.
|
|
152
|
-
|
|
152
|
+
// An agent working from a copy its gateway shares (the browser build,
|
|
153
|
+
// web/app/copy-mode.mjs) asks every time, since an app at the gateway may
|
|
154
|
+
// have taken the lease without it hearing yet.
|
|
155
|
+
if ((this.agent.viewer || this.agent.copy) && req.method !== 'GET' && req.method !== 'HEAD') {
|
|
153
156
|
const took = await this.agent.requestTakeover?.();
|
|
154
157
|
if (!took) return send(503, { error: 'another agent is active for this pod — takeover failed, try again' });
|
|
155
158
|
}
|
|
@@ -45,7 +45,7 @@ const parseRedirects = (v) => (Array.isArray(v) ? v : String(v || '').split(/\s+
|
|
|
45
45
|
// The sign-in page for an account whose owner signs in at their pod: no
|
|
46
46
|
// password of ours exists, and none is asked for. The page carries the
|
|
47
47
|
// client's request in its address, signs the owner in at the pod with the
|
|
48
|
-
// fediverse-
|
|
48
|
+
// fediverse-session library, and proves that sign-in back to /oauth/authorize.
|
|
49
49
|
function sendPodSigninPage(res, client, webId, mount = '') {
|
|
50
50
|
let asking = '';
|
|
51
51
|
if (client && (client.name || client.redirect)) {
|
|
@@ -53,7 +53,7 @@ function sendPodSigninPage(res, client, webId, mount = '') {
|
|
|
53
53
|
try { where = client.redirect ? new URL(client.redirect).host : ''; } catch { /* oob or blank */ }
|
|
54
54
|
const who = client.name ? escapeHtml(client.name) : (where ? escapeHtml(where) : 'A client');
|
|
55
55
|
asking = `<p><strong>${who}</strong> is asking to use your account`
|
|
56
|
-
+ `${where ? `, and will be sent back to <code>${escapeHtml(where)}</code>` : ''}.</p>`;
|
|
56
|
+
+ `${where ? `, and you will be sent back to <code>${escapeHtml(where)}</code>` : ''}.</p>`;
|
|
57
57
|
}
|
|
58
58
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' });
|
|
59
59
|
res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -112,6 +112,9 @@ export async function handle(api, ctx) {
|
|
|
112
112
|
};
|
|
113
113
|
sched.push(entry);
|
|
114
114
|
api.store.setScheduled(sched);
|
|
115
|
+
// Whatever keeps the time for this agent hears of it (a browser app
|
|
116
|
+
// tells its gateway, which publishes it if the app has closed by then).
|
|
117
|
+
api.agent.onScheduled?.();
|
|
115
118
|
return send(200, api.scheduledJson(entry));
|
|
116
119
|
}
|
|
117
120
|
let note;
|
|
@@ -55,18 +55,22 @@ export class AcctFeed {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
start
|
|
58
|
+
// `lastSweptAt` and `onSwept`: see TagFeed.start — a restarted browser
|
|
59
|
+
// worker sweeps only when the interval is due.
|
|
60
|
+
start({ lastSweptAt = 0, onSwept = null } = {}) {
|
|
59
61
|
this.stopped = false;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}, Math.round(this.config().intervalMin * 60_000 * (0.85 + Math.random() * 0.3)));
|
|
62
|
+
const sweep = () => this.sweep()
|
|
63
|
+
.catch(e => this.log(`acctfeed: ${e.message}`))
|
|
64
|
+
.finally(() => onSwept?.(Date.now()));
|
|
65
|
+
const every = () => Math.round(this.config().intervalMin * 60_000 * (0.85 + Math.random() * 0.3));
|
|
66
|
+
const tick = (ms) => {
|
|
67
|
+
this.timer = setTimeout(() => { sweep().finally(() => { if (!this.stopped) tick(every()); }); }, ms);
|
|
67
68
|
this.timer.unref?.();
|
|
68
69
|
};
|
|
69
|
-
|
|
70
|
+
const due = lastSweptAt + this.config().intervalMin * 60_000 - Date.now();
|
|
71
|
+
if (due > 0) { tick(due); return; }
|
|
72
|
+
sweep();
|
|
73
|
+
tick(every());
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
stop() { this.stopped = true; clearTimeout(this.timer); }
|