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
|
|
@ -13,6 +13,9 @@ import type {
|
|||
} from "@simplewebauthn/types";
|
||||
import { getDb } from "./db.ts";
|
||||
import { getOrigin } from "./config.server.ts";
|
||||
import { federationEnabled } from "./federation.server.ts";
|
||||
import { ensureUserKeypair } from "./federation-keys.server.ts";
|
||||
import { logger } from "./logger.server.ts";
|
||||
import { users, credentials, magicTokens } from "@trails-cool/db/schema/journal";
|
||||
import type { Visibility } from "@trails-cool/db/schema/journal";
|
||||
|
||||
|
|
@ -92,9 +95,27 @@ export async function finishRegistration(
|
|||
transports: response.response.transports,
|
||||
});
|
||||
|
||||
await generateFederationKeysBestEffort(userId);
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec (social-federation): new users get federation signing keys at
|
||||
* registration. Best-effort — a keygen failure must never fail the
|
||||
* registration itself; the `backfill-user-keypairs` job is the safety
|
||||
* net for any user this skips. No-op while federation is off (the
|
||||
* backfill covers everyone when the flag first flips on).
|
||||
*/
|
||||
async function generateFederationKeysBestEffort(userId: string): Promise<void> {
|
||||
if (!federationEnabled()) return;
|
||||
try {
|
||||
await ensureUserKeypair(userId);
|
||||
} catch (err) {
|
||||
logger.warn({ err, userId }, "federation keypair generation failed at registration; backfill will retry");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Add Passkey to Existing Account ---
|
||||
|
||||
export async function addPasskeyStart(userId: string) {
|
||||
|
|
@ -176,6 +197,8 @@ export async function registerWithMagicLink(
|
|||
termsVersion,
|
||||
});
|
||||
|
||||
await generateFederationKeysBestEffort(userId);
|
||||
|
||||
// Same shape as login's createMagicToken — token for the click-through
|
||||
// link, 6-digit code for paste-from-email/SMS flows (mobile).
|
||||
const token = randomBytes(32).toString("base64url");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue