fedipod-server 0.12.1 → 0.13.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.
@@ -19,6 +19,7 @@ import { createRequire } from 'node:module';
19
19
  import { Agent } from '../../run-agent.mjs';
20
20
  import { RemotePod } from '../device/remote.mjs';
21
21
  import { apUrls, DEFAULT_ROOT } from '../core/wire.mjs';
22
+ import { handleDelivery } from '../gateway/gateway-core.mjs';
22
23
  import { writeJsonAtomic } from '../device/home.mjs';
23
24
  import { buildAdminSurface } from '../device/admin/index.mjs';
24
25
  import { FixedAuthorities } from '../shared/guard.mjs';
@@ -201,6 +202,62 @@ async function connectionsIntoPod(agent, home, log) {
201
202
  }
202
203
  }
203
204
 
205
+ /**
206
+ * The pod's own inbox is a verifying door here.
207
+ *
208
+ * The server that stores the inbox is the server the delivery arrives at, so
209
+ * the signature is checked while the headers still exist and the receipt is
210
+ * written beside the activity — the same door code a standalone gateway runs,
211
+ * with nothing renamed and nothing advertised differently. The identity's
212
+ * config names its own inbox as the door and carries the receipt secret, which
213
+ * is what makes the drain read receipts at all; `trust` because a verified
214
+ * sender is one the identity may act for.
215
+ *
216
+ * An identity attached to an outside door keeps that door: only the secret
217
+ * is ensured, so receipts from either door verify against the one value.
218
+ */
219
+ export async function ensureInboxDoor(agent, urls, log = () => {}) {
220
+ const cfg = agent.store.getConfig();
221
+ if (!cfg) return;
222
+ const g = { ...(cfg.gateway || {}) };
223
+ const fresh = !g.url || !g.mode || g.mode === 'off';
224
+ if (fresh) Object.assign(g, { url: urls.inbox, mode: 'trust' });
225
+ if (!g.hmacSecret) g.hmacSecret = crypto.randomBytes(32).toString('base64');
226
+ if (fresh || !cfg.gateway?.hmacSecret) {
227
+ agent.store.setConfig({ ...cfg, gateway: g });
228
+ await agent.store.flush();
229
+ log(fresh ? 'the pod inbox verifies deliveries at the door' : 'receipt secret added for the inbox door');
230
+ }
231
+ }
232
+
233
+ /**
234
+ * One delivery to a running identity's inbox, verified at the door.
235
+ *
236
+ * `request` is the WHATWG form of the POST. `podPut` writes through the
237
+ * server's store. Returns { status, reason } for the caller to answer with.
238
+ * The policy is read from the identity's live state rather than its
239
+ * published policy document, because both are in this process.
240
+ */
241
+ export async function deliverToInbox(agent, request, { podPut, gatewayWebId = null, fetchImpl = fetch } = {}) {
242
+ const cfg = agent.store.getConfig() || {};
243
+ const contacts = agent.store.getContacts();
244
+ const bl = agent.store.getBlocklist();
245
+ const u = agent.urls;
246
+ const toPod = (x) => (u.toPod ? u.toPod(x) : x);
247
+ const ident = {
248
+ inboxUrl: toPod(u.inbox),
249
+ actorUrl: u.actor,
250
+ followersUrl: u.followers,
251
+ notesPrefix: u.notes,
252
+ following: contacts.following.filter((f) => f.accepted && !f.bsky).map((f) => f.actor),
253
+ blocklist: { domains: bl.domains || [], actors: bl.actors || [] },
254
+ kind: cfg.kind || 'person',
255
+ gatewayWebId,
256
+ hmacSecret: cfg.gateway?.hmacSecret || null,
257
+ };
258
+ return handleDelivery(request, ident, { podPut, fetchImpl });
259
+ }
260
+
204
261
  /**
205
262
  * Bring one identity up inside the server.
206
263
  *
@@ -277,6 +334,7 @@ export async function startEmbeddedAgent({
277
334
  // Before connect(), which is what looks the key and the connections up.
278
335
  await keyIntoPod(agent, home, log);
279
336
  await connectionsIntoPod(agent, home, log);
337
+ await ensureInboxDoor(agent, urls, log);
280
338
 
281
339
  await agent.connect();
282
340
 
@@ -342,6 +400,6 @@ export async function startEmbeddedAgent({
342
400
  // returned rather than rebuilt by the caller so the root name lives here.
343
401
  return {
344
402
  agent, handle, home, surface, host: authorities.host,
345
- podHome: urls.home, actorUrl: urls.actor, stop,
403
+ podHome: urls.home, actorUrl: urls.actor, inboxUrl: urls.inbox, stop,
346
404
  };
347
405
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fedipod-server",
3
- "version": "0.12.1",
3
+ "version": "0.13.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",
package/web/app/agent.mjs CHANGED
@@ -17,9 +17,8 @@ import { MastoApi } from '../../lib/client/masto/index.mjs';
17
17
  import { TagFeed } from '../../lib/connections/tagfeed.mjs';
18
18
  import { makeDpopSession } from './pod-auth.mjs';
19
19
  import { BrowserRemotePod } from './pod-remote.mjs';
20
- import { importSigningKey, loadKeysFromPod, keyCacheKey } from './keys-browser.mjs';
20
+ import { importSigningKey, loadKeysFromPod, cacheOpenedKeys } from './keys-browser.mjs';
21
21
  import { generateKeys, wrapKeys } from './keystore.mjs';
22
- import { kvPut } from './idb-kv.mjs';
23
22
  import { RelayDeliverer } from './deliver-relay.mjs';
24
23
  import { AdminFacade } from './admin-facade.mjs';
25
24
  import { BrowserAtproto } from './atproto-browser.mjs';
@@ -364,8 +363,7 @@ export class BrowserAgent {
364
363
  const rec = await generateKeys();
365
364
  rec.mintedFor = this.urls.actor; // one key, one actor (lib/keys.mjs)
366
365
  await podState.writeWrappedKeys(this.remote, this.urls, await wrapKeys(rec, password));
367
- await kvPut(keyCacheKey(this.urls.actor), rec).catch(() => {});
368
- const keys = await importSigningKey(rec);
366
+ const keys = await cacheOpenedKeys(this.urls.actor, rec);
369
367
  this.publisher.publicKeyPem = keys.rsaPublicPem;
370
368
  this.deliverer.rsaPrivate = keys.rsaPrivate;
371
369
  await this.publisher.publishProfile();
package/web/app/boot.mjs CHANGED
@@ -15,8 +15,7 @@ import * as podState from '../../lib/pod/state.mjs';
15
15
  import { BrowserRemotePod } from './pod-remote.mjs';
16
16
  import { beginLogin, completeLogin, getSession, signOut } from './oidc-session.mjs';
17
17
  import { unwrapKeys, isKeyEnvelope } from './keystore.mjs';
18
- import { kvPut } from './idb-kv.mjs';
19
- import { keyCacheKey } from './keys-browser.mjs';
18
+ import { cacheOpenedKeys } from './keys-browser.mjs';
20
19
 
21
20
  const REDIRECT = `${location.origin}/`; // the app root doubles as the OIDC callback
22
21
 
@@ -83,7 +82,7 @@ window.fedipodUnlock = async (password) => {
83
82
  if (!isKeyEnvelope(doc)) throw new Error('this account\'s key is not locked — nothing to unlock');
84
83
  const rec = await unwrapKeys(doc, password); // throws 'wrong password'
85
84
  const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
86
- await kvPut(keyCacheKey(actorUrl), rec);
85
+ await cacheOpenedKeys(actorUrl, rec);
87
86
  await bootWorker();
88
87
  };
89
88
 
@@ -33231,6 +33231,26 @@ async function kvPut(key, val) {
33231
33231
  }
33232
33232
 
33233
33233
  // web/app/keys-browser.mjs
33234
+ var pemToDer = (pem) => Uint8Array.from(
33235
+ atob(pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "")),
33236
+ (c) => c.charCodeAt(0)
33237
+ );
33238
+ async function importSigningKey(keysRecord) {
33239
+ const rsaPrivate = await crypto.subtle.importKey(
33240
+ "pkcs8",
33241
+ pemToDer(keysRecord.rsa.privatePem),
33242
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
33243
+ false,
33244
+ ["sign"]
33245
+ );
33246
+ return { rsaPrivate, rsaPublicPem: keysRecord.rsa.publicPem, edPrivate: null, edPublicMultibase: null };
33247
+ }
33248
+ async function cacheOpenedKeys(actorUrl, keysRecord) {
33249
+ const keys = await importSigningKey(keysRecord);
33250
+ await kvPut(keyCacheKey(actorUrl), { rsaPrivate: keys.rsaPrivate, rsaPublicPem: keys.rsaPublicPem }).catch(() => {
33251
+ });
33252
+ return keys;
33253
+ }
33234
33254
  var keyCacheKey = (actorUrl) => `signing-keys:${actorUrl}`;
33235
33255
 
33236
33256
  // web/app/signup.mjs
@@ -33321,8 +33341,7 @@ async function signUp(answers, { onStep = () => {
33321
33341
  } catch (e) {
33322
33342
  throw new Error(`could not store the signing key on the pod (${e.message}). The credential is for ${credential.webId} \u2014 that WebID must own ${pod} and its ${AP_ROOT} must be writable by it.`);
33323
33343
  }
33324
- await kvPut(keyCacheKey(actorUrl), keys).catch(() => {
33325
- });
33344
+ await cacheOpenedKeys(actorUrl, keys);
33326
33345
  prog.keys = keys;
33327
33346
  prog.keysStored = true;
33328
33347
  keysStep.ok();
@@ -33668,7 +33687,7 @@ window.fedipodUnlock = async (password) => {
33668
33687
  if (!isKeyEnvelope(doc)) throw new Error("this account's key is not locked \u2014 nothing to unlock");
33669
33688
  const rec = await unwrapKeys(doc, password);
33670
33689
  const actorUrl = `${cfg.remotePod}${cfg.root || AP_ROOT}ap/actor`;
33671
- await kvPut(keyCacheKey(actorUrl), rec);
33690
+ await cacheOpenedKeys(actorUrl, rec);
33672
33691
  await bootWorker();
33673
33692
  };
33674
33693
  window.fedipodSignup = async ({ onStep, ...answers }) => {