fedipod-server 0.30.0 → 0.31.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 CHANGED
@@ -156,6 +156,11 @@ that document's address and registers nothing here. Once in, it posts to the
156
156
  outbox the actor names, and reads what the identity received at `/ap/inbox`,
157
157
  which nobody but the owner may read.
158
158
 
159
+ A followers-only or direct post of the identity's is given, at its own
160
+ address, to a server it was sent to when that server signs its request, the
161
+ way Mastodon gives its own: a follower's server gets a followers-only post,
162
+ the named person's server gets a direct one, and nobody else gets either.
163
+
159
164
  A subdomained pod gets an origin of its own and answers at its root; a
160
165
  suffixed pod answers under its own path (`/aisha/ap/actor`, `/aisha/api/…`,
161
166
  `/aisha/oauth/…`, its door at `/aisha/fp/`). So a host carries at most one
package/dist/claims.d.ts CHANGED
@@ -5,6 +5,7 @@ export declare function claims(input: {
5
5
  pathname: string;
6
6
  }, frontHost: string, runPath?: string): boolean;
7
7
  export declare const isInboxPath: (pathname: string) => boolean;
8
+ export declare const isPrivateReadPath: (pathname: string) => boolean;
8
9
  /**
9
10
  * True when this path belongs to an identity's client surface.
10
11
  *
@@ -18,4 +19,5 @@ export declare const isInboxPath: (pathname: string) => boolean;
18
19
  export declare function agentClaims(input: {
19
20
  pathname: string;
20
21
  method?: string;
22
+ signed?: boolean;
21
23
  }, 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.isInboxPath = exports.DEFAULT_RUN_PATH = void 0;
7
+ exports.isPrivateReadPath = exports.isInboxPath = exports.DEFAULT_RUN_PATH = void 0;
8
8
  exports.claims = claims;
9
9
  exports.agentClaims = agentClaims;
10
10
  // Signing up is CSS's, untouched: a person makes an account and a pod the way
@@ -18,8 +18,8 @@ const FRONT_PATHS = new Set(['/roster',
18
18
  '/.well-known/webfinger', '/api/handle', '/api/attach', '/api/agent',
19
19
  '/api/roster', '/api/revoke',
20
20
  // What the pages that remain load: the sign-in library the opt-in and
21
- // roster pages use, and the installer, which is FediPod's own to hand out.
22
- '/solid-oidc-client.js', '/install',
21
+ // roster pages use.
22
+ '/solid-oidc-client.js',
23
23
  // Each page's own script, served beside it; a page without its script has
24
24
  // buttons that never wake up.
25
25
  '/new-account.js', '/run.js', '/admin.js']);
@@ -58,6 +58,10 @@ const AGENT_PREFIXES = ['/api/', '/oauth/', '/@']; // /@handle: the short profil
58
58
  // `/aisha/fedipod/ap/inbox/` arrives here already stripped to `/fedipod/ap/inbox/`.
59
59
  const isInboxPath = (pathname) => /^\/(?:[^/]+\/)+ap\/inbox\/$/u.test(pathname);
60
60
  exports.isInboxPath = isInboxPath;
61
+ // A document in the identity's private folder — a followers-only or direct
62
+ // post, or one of the owner's own lists — matched by shape like the inbox.
63
+ const isPrivateReadPath = (pathname) => /^\/(?:[^/]+\/)+ap\/private\/[^/]+$/u.test(pathname);
64
+ exports.isPrivateReadPath = isPrivateReadPath;
61
65
  /**
62
66
  * True when this path belongs to an identity's client surface.
63
67
  *
@@ -70,8 +74,13 @@ exports.isInboxPath = isInboxPath;
70
74
  */
71
75
  function agentClaims(input, uiPath = '/fp/') {
72
76
  const { pathname } = input;
77
+ const method = String(input.method ?? '').toUpperCase();
73
78
  if ((0, exports.isInboxPath)(pathname))
74
- return String(input.method ?? '').toUpperCase() === 'POST';
79
+ return method === 'POST';
80
+ // A signed read of a private post is answered at the door, to a server the
81
+ // post was sent to (private-read.ts). Unsigned, the document is the pod's.
82
+ if ((0, exports.isPrivateReadPath)(pathname))
83
+ return !!input.signed && (method === 'GET' || method === 'HEAD');
75
84
  if (AGENT_PATHS.has(pathname))
76
85
  return true;
77
86
  if (AGENT_PREFIXES.some((prefix) => pathname.startsWith(prefix)))
package/dist/fedipod.d.ts CHANGED
@@ -12,5 +12,4 @@ export interface FrontPages {
12
12
  noticesPage: string | null;
13
13
  authBundle: string | null;
14
14
  pageScripts: Record<string, string | null>;
15
- installScript: string | null;
16
15
  }
package/dist/front.js CHANGED
@@ -39,14 +39,12 @@ async function serveFront(h, request, response) {
39
39
  // The notices page renders here too, and says the server keeps none:
40
40
  // a notices store is the Netlify front's, not this component's.
41
41
  noticesPage: pages.noticesPage,
42
- // The opt-in and roster pages load the sign-in library, and the installer
43
- // command is handed out with it; without these the pages render but
44
- // cannot be used.
42
+ // The opt-in and roster pages load the sign-in library; without it the
43
+ // pages render but cannot be used.
45
44
  authBundle: pages.authBundle,
46
45
  // Each page's own script, so the pages can be served under
47
46
  // `script-src 'self'` (see fedipod/front).
48
47
  pageScripts: pages.pageScripts,
49
- installScript: pages.installScript,
50
48
  lookup: (handle) => h.dir.lookup(handle),
51
49
  putDirectory: (handle, rec) => h.dir.putDirectory(handle, rec),
52
50
  podPut: (_handle, url, body, ct) => s.podPut(url, body, ct),
package/dist/handler.js CHANGED
@@ -52,6 +52,7 @@ const mounts_1 = require("./mounts");
52
52
  const identities = __importStar(require("./identities"));
53
53
  const optIn = __importStar(require("./opt-in"));
54
54
  const door_1 = require("./door");
55
+ const private_read_1 = require("./private-read");
55
56
  const front_1 = require("./front");
56
57
  class FediPodServerHandler extends community_server_1.HttpHandler {
57
58
  args;
@@ -125,7 +126,8 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
125
126
  // relative to the claim's mount, so a suffix pod's `/aisha/ap/actor` is
126
127
  // judged as `/ap/actor`.
127
128
  const c = (0, mounts_1.resolveClaim)(this, host, pathname);
128
- if (c && (0, claims_1.agentClaims)({ pathname: (0, mounts_1.stripMount)(pathname, c.mount), method: request.method }, this.uiPath))
129
+ if (c && (0, claims_1.agentClaims)({ pathname: (0, mounts_1.stripMount)(pathname, c.mount), method: request.method,
130
+ signed: !!request.headers.signature }, this.uiPath))
129
131
  return;
130
132
  throw new Error('not a gateway route'); // reject → CSS's LDP handler takes it
131
133
  }
@@ -185,6 +187,13 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
185
187
  if (pathname === inboxPath && String(request.method).toUpperCase() === 'POST') {
186
188
  return (0, door_1.deliverAtDoor)(this, identity, request, response);
187
189
  }
190
+ // A signed read of a private post — a followers-only or direct one, fetched
191
+ // at its address by a server it was sent to — is answered here (§3.2).
192
+ const privatePrefix = inboxPath.replace(/ap\/inbox\/$/u, 'ap/private/');
193
+ const method = String(request.method).toUpperCase();
194
+ if (pathname.startsWith(privatePrefix) && (method === 'GET' || method === 'HEAD') && request.headers.signature) {
195
+ return (0, private_read_1.readPrivateAtDoor)(this, identity, request, response);
196
+ }
188
197
  await identity.surface.handler(request, response);
189
198
  }
190
199
  }
@@ -16,6 +16,15 @@ export interface EmbeddedIdentity {
16
16
  getConfig?: () => {
17
17
  kind?: string;
18
18
  } | null | undefined;
19
+ getContacts?: () => {
20
+ followers?: {
21
+ actor?: string;
22
+ }[];
23
+ };
24
+ };
25
+ urls?: {
26
+ followers?: string;
27
+ toPod?: (u: string) => string;
19
28
  };
20
29
  };
21
30
  }
@@ -0,0 +1,4 @@
1
+ import type { HttpHandlerInput } from '@solid/community-server';
2
+ import type { EmbeddedIdentity } from './identities';
3
+ import type { FediPodServerHandler } from './handler';
4
+ export declare function readPrivateAtDoor(h: FediPodServerHandler, identity: EmbeddedIdentity, request: HttpHandlerInput['request'], response: HttpHandlerInput['response']): Promise<void>;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ // private-read.ts — a signed read of a followers-only or direct post at its
3
+ // address on this pod, answered to a server the post was sent to (ActivityPub
4
+ // §3.2). The same rule the front applies (lib/gateway/private-read.mjs); the
5
+ // post is read through the server's own store and the followers come from the
6
+ // identity's live state. Anyone the post was not sent to gets 404.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.readPrivateAtDoor = readPrivateAtDoor;
9
+ const adapt_1 = require("./adapt");
10
+ const fedipod_1 = require("./fedipod");
11
+ async function readPrivateAtDoor(h, identity, request, response) {
12
+ const s = h.internals();
13
+ const { readPrivateForSigner } = await (0, fedipod_1.esmImport)(fedipod_1.EMBED);
14
+ let whatwg;
15
+ try {
16
+ // The pod's own origin: the signature covers the path and the Host header, and both are the pod's.
17
+ whatwg = await (0, adapt_1.nodeToWhatwg)(request, new URL(identity.podHome).origin);
18
+ }
19
+ catch (e) {
20
+ response.writeHead(400, { 'content-type': 'application/json' });
21
+ response.end(JSON.stringify({ error: e.message }));
22
+ return;
23
+ }
24
+ const out = await readPrivateForSigner(identity.agent, whatwg, { read: (url) => s.io.read(url) });
25
+ s.logger.info(`FediPod: private read for @${identity.handle} — ${out.reason} (${out.status})`);
26
+ const noStore = { 'cache-control': 'no-store', vary: 'Signature' };
27
+ if (out.status !== 200) {
28
+ response.writeHead(out.status, noStore);
29
+ response.end();
30
+ return;
31
+ }
32
+ response.writeHead(200, { ...noStore, 'content-type': 'application/activity+json' });
33
+ response.end(String(request.method).toUpperCase() === 'HEAD' ? '' : out.body);
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fedipod-server",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "The FediPod Server: a full ActivityPub server as a Community Solid Server component.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -15,16 +15,16 @@
15
15
  "@solid/community-server": "^7.0.0"
16
16
  },
17
17
  "dependencies": {
18
- "fedipod": "^1.42.0"
18
+ "fedipod": "^1.45.0"
19
19
  },
20
20
  "devDependencies": {
21
- "typescript": "^5.5.3",
22
- "componentsjs-generator": "^3.1.2",
23
- "@types/node": "^20.0.0",
24
- "@solid/community-server": "^7.2.0",
25
21
  "@fedify/fedify": "^2.3.4",
22
+ "@solid/community-server": "^7.2.0",
23
+ "@types/node": "^20.0.0",
24
+ "componentsjs-generator": "^3.1.2",
26
25
  "n3": "^2.1.1",
27
- "rdflib": "^2.4.0"
26
+ "rdflib": "^2.4.0",
27
+ "typescript": "^5.5.3"
28
28
  },
29
29
  "overrides": {
30
30
  "@opentelemetry/core": "^2.8.0",