fedipod-server 0.16.0 → 0.18.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.
Files changed (55) hide show
  1. package/README.md +37 -15
  2. package/dist/claims.d.ts +10 -11
  3. package/dist/claims.js +19 -17
  4. package/dist/directory.d.ts +11 -3
  5. package/dist/directory.js +28 -14
  6. package/dist/handler.d.ts +30 -7
  7. package/dist/handler.js +133 -42
  8. package/dist/handler.jsonld +14 -6
  9. package/dist/streaming-handler.js +7 -2
  10. package/lib/client/c2s.mjs +95 -61
  11. package/lib/client/masto/index.mjs +7 -1
  12. package/lib/client/masto/timelines.mjs +1 -1
  13. package/lib/client/oidc-auth.mjs +5 -3
  14. package/lib/core/contexts/anno.json +126 -0
  15. package/lib/core/contexts/index.mjs +4 -0
  16. package/lib/core/contexts/map.json +2 -1
  17. package/lib/core/intake/index.mjs +32 -5
  18. package/lib/core/publisher/index.mjs +15 -1
  19. package/lib/core/publisher/notes.mjs +84 -2
  20. package/lib/core/publisher/questions.mjs +1 -0
  21. package/lib/core/publisher/restore.mjs +27 -2
  22. package/lib/core/social.mjs +1 -0
  23. package/lib/core/store.mjs +4 -0
  24. package/lib/core/wire.mjs +20 -13
  25. package/lib/device/admin/routes/gateway.mjs +1 -1
  26. package/lib/device/admin/surface.mjs +25 -17
  27. package/lib/device/cli/commands/setup.mjs +9 -1
  28. package/lib/device/setup.mjs +6 -0
  29. package/lib/gateway/front-core.mjs +66 -12
  30. package/lib/gateway/gateway-core.mjs +40 -0
  31. package/lib/pod/actor.mjs +2 -2
  32. package/lib/pod/root.mjs +11 -0
  33. package/lib/pod/transport.mjs +9 -3
  34. package/lib/server/embed.mjs +23 -6
  35. package/package.json +2 -1
  36. package/run-agent.mjs +10 -1
  37. package/vendor/gate.cjs +5 -2
  38. package/web/admin/index.html +2 -0
  39. package/web/admin/upkeep.js +9 -1
  40. package/web/app/README.md +1 -1
  41. package/web/app/agent.mjs +11 -1
  42. package/web/app/boot.mjs +49 -10
  43. package/web/app/dist/boot.js +89 -37
  44. package/web/app/dist/boot.js.map +3 -3
  45. package/web/app/dist/sw.js +1326 -662
  46. package/web/app/dist/sw.js.map +4 -4
  47. package/web/app/index.html +16 -6
  48. package/web/app/signup.mjs +3 -1
  49. package/web/app/site/admin/index.html +2 -0
  50. package/web/app/site/admin/upkeep.js +9 -1
  51. package/web/app/site/boot.js +89 -37
  52. package/web/app/site/index.html +16 -6
  53. package/web/app/site/sw.js +1326 -662
  54. package/web/front/#new-account.html# +0 -43
  55. package/web/front/new-account.html~ +0 -50
package/README.md CHANGED
@@ -30,9 +30,13 @@ before installing: a server that does not meet them cannot offer accounts, and
30
30
  sign-up refuses rather than half-working.
31
31
 
32
32
  - **Community Solid Server 7.**
33
- - **Pods on subdomains.** Each account answers on its own pod's address, so
34
- every pod needs a host to itself. A server that puts its pods on paths of
35
- one suffix-based host cannot offer accounts at all.
33
+ - **Pods on subdomains or on paths.** A subdomain pod answers on its own
34
+ address (`@mei@mei.example.org`); a suffix pod on a path of the server's own
35
+ host answers under that path, and the server fronts it so its address is the
36
+ server's host (`@aisha@server.example`, for a pod at
37
+ `https://server.example/aisha/`). Either works — a suffix pod cannot resolve
38
+ its own handle, so it is followable only through the server's apex, which the
39
+ server also runs.
36
40
  - **A single worker.** Run the server as one copy of itself — `--workers 1`,
37
41
  which is the default. With more, it still serves pods normally, but no pod
38
42
  can be an account: sign-up is refused, and an account set up earlier goes on
@@ -66,8 +70,8 @@ Add the package context to your CSS config and import the shipped snippet:
66
70
 
67
71
  Then set what you need on the `urn:fedipod:server:Handler` node and restart
68
72
  the server.
69
- Use a subdomain-pods CSS config: identities need an origin of their own, and
70
- a path-pod config cannot opt anyone in.
73
+ A subdomain-pods CSS config gives each identity an origin of its own; a
74
+ suffix (path) config hosts each on a path of the shared host. Both opt in.
71
75
  The snippet places the component in the routing waterfall ahead of
72
76
  the LDP catch-all, on the initializer and finalizer lists so identities start
73
77
  and stop with the server, and on the websocket handler list for the live feed.
@@ -84,8 +88,9 @@ and stop with the server, and on the websocket handler list for the live feed.
84
88
  ```
85
89
 
86
90
  An identity is provisioned when its owner opts in: its name is the pod's
87
- subdomain label, and it publishes an actor, a signing key and WebFinger on the
88
- pod itself. Everything it is made of lives on its pod its state, its private
91
+ subdomain label, or its last path segment on a suffix pod, and it publishes an
92
+ actor and a signing key on the pod itself. Its handle resolves on the pod for a
93
+ subdomain, and through the server's apex for a suffix pod. Everything it is made of lives on its pod — its state, its private
89
94
  signing key, the secret guarding its own pages, and the credentials for any
90
95
  accounts its owner connects on other servers. Each identity has its own
91
96
  secret; one owner's opens nobody else's door. The opt-in reply is where the
@@ -103,7 +108,7 @@ naming the pod the identity runs on. Nothing private is in it.
103
108
  |---|---|
104
109
  | `agentRuntimeOptIn` | Whether pod owners can sign up. With it off, nothing runs. |
105
110
  | `agentDataDir` | Where each identity keeps the file naming its pod; its log lines go to the server's own log. Required whenever sign-up is on. |
106
- | `agentUiPath` | Where the owner's pages live on the pod's origin. `/fedipod/` by default; empty serves no pages. |
111
+ | `agentUiPath` | Where the owner's pages live on the pod's origin. `/fp/` by default; empty serves no pages. On a suffix pod it is under the pod's path, e.g. `/aisha/fp/`. |
107
112
  | `agentRegistryContainer` | The internal container holding the sign-up rows. |
108
113
  | `runPage` | The HTML served at `/run`: the page where a pod owner opts in or out. The package's own `web/front/run.html` is served unless you set this. |
109
114
  | `agentWebIdSuffix` | Path from a pod's base to its owner's WebID. Defaults to `profile/card#me`. |
@@ -149,9 +154,12 @@ that document's address and registers nothing here. Once in, it posts to the
149
154
  outbox the actor names, and reads what the identity received at `/ap/inbox`,
150
155
  which nobody but the owner may read.
151
156
 
152
- A client API is rooted at an origin, so each identity needs an origin of its
153
- own: subdomain pods, one per identity. A second identity on a host is
154
- refused at opt-in rather than half-working.
157
+ A subdomain pod gets an origin of its own and answers at its root; a suffix
158
+ pod shares the host and answers under its own path (`/aisha/ap/actor`,
159
+ `/aisha/api/…`, `/aisha/oauth/…`, its door at `/aisha/fp/`). So a host carries
160
+ at most one root identity, but any number of path pods beside it; a second
161
+ root identity on a host, or a path pod nesting under another, is refused at
162
+ opt-in rather than half-working.
155
163
 
156
164
  ### Before you turn it on
157
165
 
@@ -164,16 +172,24 @@ for anyone hosting other people, it is a promise being made to them.
164
172
  owner's door, with that identity's own door secret:
165
173
 
166
174
  ```
167
- curl -X POST https://mei.example.org/fedipod/config \
175
+ # subdomain pod:
176
+ curl -X POST https://mei.example.org/fp/config \
177
+ -H 'x-dk-token: THE_DOOR_SECRET_FROM_THE_OPT_IN_REPLY' -H 'content-type: application/json' \
178
+ -d '{"password":"the one you will type into your phone"}'
179
+ # suffix pod — the door is under the pod's path:
180
+ curl -X POST https://server.example/aisha/fp/config \
168
181
  -H 'x-dk-token: THE_DOOR_SECRET_FROM_THE_OPT_IN_REPLY' -H 'content-type: application/json' \
169
182
  -d '{"password":"the one you will type into your phone"}'
170
183
  ```
171
184
 
172
- **Some pod paths stop being served.** On an identity's origin the paths above
185
+ **Some pod paths stop being served.** On a subdomain pod the paths above
173
186
  belong to the identity, so pod resources at those names — a container called
174
- `api`, `oauth` or `fedipod`, or documents at `ap/actor`, `ap/outbox`,
187
+ `api`, `oauth` or `fp`, or documents at `ap/actor`, `ap/outbox`,
175
188
  `ap/inbox`, `.well-known/oauth-authorization-server`,
176
189
  `.well-known/nodeinfo` and `nodeinfo/2.0` — are not served over HTTP there.
190
+ On a suffix pod the same names are taken under the pod's own path
191
+ (`/aisha/api/…`, `/aisha/ap/actor`, `/aisha/fp/`, and so on); the rest of the
192
+ path, and every other pod on the host, is untouched.
177
193
  They stay in the pod and in its listings. Every other path is the pod, exactly
178
194
  as before.
179
195
 
@@ -228,7 +244,8 @@ TypeScript, built the way CSS builds its own components.
228
244
  npm install
229
245
  npm run build # tsc → dist, then componentsjs-generator → dist/components
230
246
  npm test # unit tests, plus the transport against a real CSS store
231
- npm run test:e2e # boots a real server and uses it as a client would
247
+ npm run test:e2e # boots a real server (subdomain pods) and uses it as a client would
248
+ npm run test:e2e:suffix # the same, for pods on paths of one host (mode d)
232
249
  ```
233
250
 
234
251
  `npm test` drives a genuine CSS store stack — ETags, conditional writes,
@@ -241,6 +258,11 @@ lease protocol and the deletion deny-list still hold across it.
241
258
  become identities up front; the third opts in and out at runtime), then signs in as a phone app does, posts,
242
259
  receives a follow from another server, and watches the live feed. It takes
243
260
  about a minute. `FEDIPOD_E2E_LOG=info` shows the server's log while it runs.
261
+ `npm run test:e2e:suffix` does the same on a server whose pods live on paths of
262
+ one host (`@aisha@server`): it proves the apex WebFinger resolves straight to
263
+ the pod actor, that the actor, inbox, client API, OAuth and door all answer
264
+ under the pod's path, that two path pods on one origin stay separate, and that
265
+ a signed delivery is verified at the pod's own door.
244
266
 
245
267
  `dist/` and `node_modules/` are gitignored; `src/`, `config/` and this file are
246
268
  the sources.
package/dist/claims.d.ts CHANGED
@@ -2,19 +2,18 @@ export declare function claims(input: {
2
2
  host?: string;
3
3
  pathname: string;
4
4
  }, frontHost: string): boolean;
5
+ export declare const isInboxPath: (pathname: string) => boolean;
5
6
  /**
6
- * The inbox container of an identity this server runs, under the root the
7
- * embedded credential uses (lib/server/embed.mjs). A delivery POSTed here is
8
- * verified at the door before it is written, so the request is claimed from
9
- * the LDP handler; every other method on the container is the pod's.
10
- */
11
- export declare const INBOX_PATH = "/activitypods-js/ap/inbox/";
12
- /**
13
- * True when this request belongs to an identity's client surface.
14
- * `agentHosts` is keyed by host including port, as the Host header carries it.
7
+ * True when this path belongs to an identity's client surface.
8
+ *
9
+ * `pathname` is RELATIVE to the identity's mount: the handler has already
10
+ * matched the request's host and mount to one identity (see resolveClaim) and
11
+ * stripped the mount, so a host-root/subdomain pod passes its path unchanged
12
+ * and a suffix pod on `/aisha/` passes the part after `/aisha`. That keeps this
13
+ * a pure statement about which routes an identity owns, with no notion of host
14
+ * or of where on the origin it lives.
15
15
  */
16
16
  export declare function agentClaims(input: {
17
- host?: string;
18
17
  pathname: string;
19
18
  method?: string;
20
- }, agentHosts: Set<string>, uiPath?: string): boolean;
19
+ }, uiPath?: string): boolean;
package/dist/claims.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // front's apex the gateway answers the fediverse routes; a pod subdomain is a
5
5
  // real Solid pod and is never claimed.
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.INBOX_PATH = void 0;
7
+ exports.isInboxPath = void 0;
8
8
  exports.claims = claims;
9
9
  exports.agentClaims = agentClaims;
10
10
  const FRONT_PATHS = new Set(['/', '/signup', '/new-account', '/run', '/roster',
@@ -36,27 +36,29 @@ const AGENT_PATHS = new Set([
36
36
  '/.well-known/oauth-authorization-server',
37
37
  ]);
38
38
  const AGENT_PREFIXES = ['/api/', '/oauth/'];
39
+ // An identity's inbox is the container `/<root>/ap/inbox/`, matched by shape
40
+ // rather than a fixed root. A POST here is a delivery, verified at the door
41
+ // before it is written; the handler checks the exact path per identity. The
42
+ // path is relative to the identity's mount (see agentClaims), so a suffix pod's
43
+ // `/aisha/fedipod/ap/inbox/` arrives here already stripped to `/fedipod/ap/inbox/`.
44
+ const isInboxPath = (pathname) => /^\/[^/]+\/ap\/inbox\/$/u.test(pathname);
45
+ exports.isInboxPath = isInboxPath;
39
46
  /**
40
- * The inbox container of an identity this server runs, under the root the
41
- * embedded credential uses (lib/server/embed.mjs). A delivery POSTed here is
42
- * verified at the door before it is written, so the request is claimed from
43
- * the LDP handler; every other method on the container is the pod's.
47
+ * True when this path belongs to an identity's client surface.
48
+ *
49
+ * `pathname` is RELATIVE to the identity's mount: the handler has already
50
+ * matched the request's host and mount to one identity (see resolveClaim) and
51
+ * stripped the mount, so a host-root/subdomain pod passes its path unchanged
52
+ * and a suffix pod on `/aisha/` passes the part after `/aisha`. That keeps this
53
+ * a pure statement about which routes an identity owns, with no notion of host
54
+ * or of where on the origin it lives.
44
55
  */
45
- exports.INBOX_PATH = '/activitypods-js/ap/inbox/';
46
- /**
47
- * True when this request belongs to an identity's client surface.
48
- * `agentHosts` is keyed by host including port, as the Host header carries it.
49
- */
50
- function agentClaims(input, agentHosts, uiPath = '/fedipod/') {
51
- if (!input.host || agentHosts.size === 0)
52
- return false;
53
- if (!agentHosts.has(String(input.host).toLowerCase()))
54
- return false;
56
+ function agentClaims(input, uiPath = '/fp/') {
55
57
  const { pathname } = input;
58
+ if ((0, exports.isInboxPath)(pathname))
59
+ return String(input.method ?? '').toUpperCase() === 'POST';
56
60
  if (AGENT_PATHS.has(pathname))
57
61
  return true;
58
- if (pathname === exports.INBOX_PATH)
59
- return String(input.method ?? '').toUpperCase() === 'POST';
60
62
  if (AGENT_PREFIXES.some((prefix) => pathname.startsWith(prefix)))
61
63
  return true;
62
64
  // The owner's door, when there is one: '' turns the pages off entirely.
@@ -39,11 +39,19 @@ export interface AgentRegistryRecord {
39
39
  webId: string;
40
40
  optedInAt: string;
41
41
  }
42
+ /**
43
+ * The registry key for an opted-in pod: its host plus its path. Several pods
44
+ * can share a host (suffix pods on one origin), so the host alone is not
45
+ * unique. A host-root or subdomain pod has an empty path, so its key is just
46
+ * its host — exactly the shape rows were keyed by before suffix pods existed,
47
+ * which is why old rows still resolve with no migration.
48
+ */
49
+ export declare function agentKey(host: string, podBase: string): string;
42
50
  export interface AgentRegistry {
43
- listHosts(): Promise<string[]>;
44
- get(host: string): Promise<AgentRegistryRecord | null>;
51
+ listKeys(): Promise<string[]>;
52
+ get(key: string): Promise<AgentRegistryRecord | null>;
45
53
  add(record: AgentRegistryRecord): Promise<void>;
46
- remove(host: string): Promise<void>;
54
+ remove(key: string): Promise<void>;
47
55
  }
48
56
  export declare function makeAgentRegistry(io: IO, containerUrl: string): AgentRegistry;
49
57
  export declare function makeStorePodPut(io: IO): (url: string, body: string, contentType: string) => Promise<boolean>;
package/dist/directory.js CHANGED
@@ -5,6 +5,7 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.frontRow = frontRow;
7
7
  exports.makeDirectory = makeDirectory;
8
+ exports.agentKey = agentKey;
8
9
  exports.makeAgentRegistry = makeAgentRegistry;
9
10
  exports.makeStorePodPut = makeStorePodPut;
10
11
  // One JSON document per key in a container — the shape both registries share.
@@ -57,28 +58,41 @@ function makeDirectory(io, containerUrl) {
57
58
  putDirectory: (handle, record) => table.put(handle, record),
58
59
  };
59
60
  }
60
- // The registry of runtime-opted-in pods, keyed by host (the claim key). The
61
- // IO layer cannot enumerate a container, so index.json carries the host list;
61
+ /**
62
+ * The registry key for an opted-in pod: its host plus its path. Several pods
63
+ * can share a host (suffix pods on one origin), so the host alone is not
64
+ * unique. A host-root or subdomain pod has an empty path, so its key is just
65
+ * its host — exactly the shape rows were keyed by before suffix pods existed,
66
+ * which is why old rows still resolve with no migration.
67
+ */
68
+ function agentKey(host, podBase) {
69
+ return host + new URL(podBase).pathname.replace(/\/+$/u, '');
70
+ }
71
+ // The registry of runtime-opted-in pods, keyed by agentKey (host+path). The
72
+ // IO layer cannot enumerate a container, so index.json carries the key list;
62
73
  // the row is written FIRST, so a crash between the two writes still leaves a
63
- // row the next boot claims once the index catches up on the next change.
74
+ // row the next boot claims once the index catches up on the next change. The
75
+ // index field stays named `hosts`: for a host-root pod a key IS its host, so
76
+ // documents an older server wrote are read back unchanged.
64
77
  function makeAgentRegistry(io, containerUrl) {
65
78
  const table = jsonTable(io, containerUrl);
66
79
  const index = jsonTable(io, containerUrl);
67
80
  const INDEX = 'index';
68
- const hosts = async () => (await index.get(INDEX))?.hosts ?? [];
81
+ const keys = async () => (await index.get(INDEX))?.hosts ?? [];
69
82
  return {
70
- listHosts: hosts,
71
- get: (host) => table.get(host),
83
+ listKeys: keys,
84
+ get: (key) => table.get(key),
72
85
  async add(record) {
73
- await table.put(record.host, record);
74
- const list = await hosts();
75
- if (!list.includes(record.host))
76
- await index.put(INDEX, { hosts: [...list, record.host] });
86
+ const key = agentKey(record.host, record.podBase);
87
+ await table.put(key, record);
88
+ const list = await keys();
89
+ if (!list.includes(key))
90
+ await index.put(INDEX, { hosts: [...list, key] });
77
91
  },
78
- async remove(host) {
79
- const list = await hosts();
80
- await index.put(INDEX, { hosts: list.filter((h) => h !== host) });
81
- await table.remove(host);
92
+ async remove(key) {
93
+ const list = await keys();
94
+ await index.put(INDEX, { hosts: list.filter((h) => h !== key) });
95
+ await table.remove(key);
82
96
  },
83
97
  };
84
98
  }
package/dist/handler.d.ts CHANGED
@@ -65,7 +65,7 @@ export declare class FediPodServerHandler extends HttpHandler implements Initial
65
65
  readonly dir: Directory;
66
66
  private readonly podPut;
67
67
  private readonly logger;
68
- private readonly agentHosts;
68
+ private readonly claimed;
69
69
  private readonly agentHandles;
70
70
  private readonly frontHost;
71
71
  private readonly uiPath;
@@ -79,11 +79,26 @@ export declare class FediPodServerHandler extends HttpHandler implements Initial
79
79
  private onSignal;
80
80
  constructor(args: FediPodServerArgs);
81
81
  /**
82
- * Whether this pod may become an identity here: a real URL, an origin of its
83
- * own, not the front's host, and a handle no other identity already uses —
84
- * two pods must never share <agentDataDir>/<handle>/.
82
+ * Whether this pod may become an identity here, and the claim it earns: a
83
+ * real URL, a place no other identity already sits, and a handle no other
84
+ * identity uses — two pods must never share <agentDataDir>/<handle>/.
85
+ *
86
+ * A HOST-ROOT or subdomain pod needs an origin of its own, and it may not be
87
+ * the front's host: the whole surface answers at the origin root, so two of
88
+ * them, or one sharing the front, would collide. A SUFFIX pod lives on a path
89
+ * (`server/aisha/`), so it may share its host — with the front and with other
90
+ * suffix pods — provided no claim already contains or nests under its path.
85
91
  */
86
- private validateAgentHost;
92
+ private validateAgentPod;
93
+ /**
94
+ * The claim a request belongs to, or null. A request matches when its host is
95
+ * the claim's host and its path is at or under the claim's mount; the deepest
96
+ * mount wins, so a suffix pod's own routes are never swallowed by a shallower
97
+ * claim on the same host.
98
+ */
99
+ private resolveClaim;
100
+ /** A request path relative to a mount: `/aisha/ap/actor` under `/aisha` → `/ap/actor`. */
101
+ private stripMount;
87
102
  /**
88
103
  * Start an agent for each opted-in pod. Runs before the server listens, so
89
104
  * the identities come up in the background and boot is never held on a pod.
@@ -118,8 +133,16 @@ export declare class FediPodServerHandler extends HttpHandler implements Initial
118
133
  * of them can answer for it.
119
134
  */
120
135
  private runsIdentities;
121
- /** The running identity answering on a host, if it has finished starting. */
122
- surfaceFor(host?: string): EmbeddedIdentity | undefined;
136
+ /**
137
+ * The identity a request belongs to (matched by host and mount) and the
138
+ * request path relative to that identity's mount, or null. The identity is
139
+ * undefined when the pod is claimed but still starting. Used by the streaming
140
+ * upgrade, which has only the request to go on.
141
+ */
142
+ matchIdentity(host?: string, pathname?: string): {
143
+ identity: EmbeddedIdentity | undefined;
144
+ rel: string;
145
+ } | null;
123
146
  /**
124
147
  * A pod owner, already proven to control podBase, asks this server to run
125
148
  * their identity. Returns { httpStatus, ...body }; the secret appears in the