feat(journal): federation schema + per-user signing keypairs
social-federation tasks 2.1–2.5: - users.public_key / users.private_key_encrypted (TEXT NULL): RSA 2048 keypairs as JWK JSON; private key AES-256-GCM encrypted at rest with FEDERATION_KEY_ENCRYPTION_KEY. - remote_actors table: cache of remote AP actors (display fields, inbox/outbox URLs, public key, software discovery field, poll cursor). - activities.remote_origin_iri (UNIQUE) / remote_actor_iri / audience: provenance + audience tagging for rows ingested from remote outboxes. Replay-safe ingestion keys off the unique origin IRI. - crypto.server.ts: generalized into createAesCipher(envVar, salt) factory; existing INTEGRATION_SECRET surface unchanged. - federation-keys.server.ts: generate/ensure/load keypairs. RSASSA-PKCS1-v1_5 because that's what Mastodon interops on. - backfill-user-keypairs pg-boss job: enqueued once per server startup when FEDERATION_ENABLED=true; only touches users with NULL keys, so re-runs are no-ops. New users get keys at registration (both passkey and magic-link paths), best-effort with the backfill as safety net. - design.md: noted open question — activities.owner_id is NOT NULL but remote-ingested rows have no local owner; decide in task 7.2. Schema is additive; zero behavior change while FEDERATION_ENABLED is off. db:push verified clean against local Postgres. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4ef86e4dc2
commit
9a90b6db55
9 changed files with 322 additions and 34 deletions
|
|
@ -108,6 +108,14 @@ Inbound signature verification uses the actor's public key from their actor obje
|
|||
|
||||
## Open Questions
|
||||
|
||||
- **`activities.owner_id` is NOT NULL but remote-ingested rows have no local
|
||||
owner** (surfaced during task 2.3, 2026-06-06). Options: make `owner_id`
|
||||
nullable with a check constraint (`owner_id IS NOT NULL OR remote_actor_iri
|
||||
IS NOT NULL`), or key remote rows purely off `remote_actor_iri` in a way
|
||||
that never touches owner-joined queries. Decide in task 7.2 (ingestion)
|
||||
before any remote row is written; the columns landed in 2.3 don't prejudge
|
||||
either option.
|
||||
|
||||
- Custom AS extension for activity-type vs. plain `Create(Note)`? Mastodon will only render Notes; an extension type means non-trails clients see nothing useful. Lean toward `Create(Note)` with structured metadata in `attachment` so Mastodon shows the text + GPX link, and trails clients consuming the outbox can read the structured fields.
|
||||
- Per-instance inbox vs per-user inbox? Mastodon supports a "shared inbox" optimization. Worth doing for delivery efficiency once we have any volume; v1 can use per-user inboxes only.
|
||||
- How does the trails-to-trails check interact with self-hosters who fork or rebrand? Probably the `software` field is `"trails.cool"` (or a derivative we list) and the `/.well-known/trails-cool` endpoint is the authoritative discovery. Tasks phase decides the exact shape.
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
## 2. Schema
|
||||
|
||||
- [ ] 2.1 Add `users.public_key TEXT NULL`, `users.private_key_encrypted TEXT NULL` (encrypted with `FEDERATION_KEY_ENCRYPTION_KEY`)
|
||||
- [ ] 2.2 Add `remote_actors` table: `actor_iri PK`, `display_name`, `username`, `domain`, `avatar_url`, `inbox_url`, `outbox_url`, `public_key`, `software` (the discovery field), `last_polled_at`, `created_at`
|
||||
- [ ] 2.3 Extend `journal.activities` with `remote_origin_iri TEXT NULL UNIQUE`, `remote_actor_iri TEXT NULL`, `audience TEXT NOT NULL DEFAULT 'public'`
|
||||
- [ ] 2.4 Run `pnpm db:push` locally and confirm migration is clean
|
||||
- [ ] 2.5 One-shot pg-boss job `backfill-user-keypairs` that generates a keypair for every existing user lacking one. Runs once at deploy when feature flag flips on
|
||||
- [x] 2.1 Add `users.public_key TEXT NULL`, `users.private_key_encrypted TEXT NULL` (encrypted with `FEDERATION_KEY_ENCRYPTION_KEY`)
|
||||
- [x] 2.2 Add `remote_actors` table: `actor_iri PK`, `display_name`, `username`, `domain`, `avatar_url`, `inbox_url`, `outbox_url`, `public_key`, `software` (the discovery field), `last_polled_at`, `created_at`
|
||||
- [x] 2.3 Extend `journal.activities` with `remote_origin_iri TEXT NULL UNIQUE`, `remote_actor_iri TEXT NULL`, `audience TEXT NOT NULL DEFAULT 'public'`
|
||||
- [x] 2.4 Run `pnpm db:push` locally and confirm migration is clean
|
||||
- [x] 2.5 One-shot pg-boss job `backfill-user-keypairs` that generates a keypair for every existing user lacking one. Runs once at deploy when feature flag flips on
|
||||
|
||||
## 3. Identity surface
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue