An AT Protocol PDS
1import { Secp256k1Keypair, EcdsaKeypair, type ExportableKeypair } from '@atproto/crypto'
2import type { Db } from '../db/index.js'
3
4/**
5 * Manages per-DID signing keypairs stored in SQLite (as JWK).
6 * Replaces @atproto/pds's ActorStore.keypair() / reserveKeypair() interface.
7 */
8export class KeyStore {
9 constructor(private db: Db) {}
10
11 async getOrCreateKeypair(did: string): Promise<ExportableKeypair> {
12 const existing = this.db.prepare(
13 `SELECT privateKeyJwk FROM signing_key WHERE did = ?`
14 ).get(did) as { privateKeyJwk: string } | undefined
15
16 if (existing) {
17 return Secp256k1Keypair.import(JSON.parse(existing.privateKeyJwk), { exportable: true })
18 }
19
20 const keypair = await Secp256k1Keypair.create({ exportable: true })
21 const jwk = await keypair.export()
22 this.db.prepare(`
23 INSERT OR IGNORE INTO signing_key (did, privateKeyJwk, createdAt) VALUES (?, ?, ?)
24 `).run(did, JSON.stringify(jwk), new Date().toISOString())
25
26 return keypair
27 }
28
29 async getKeypair(did: string): Promise<ExportableKeypair | null> {
30 const row = this.db.prepare(
31 `SELECT privateKeyJwk FROM signing_key WHERE did = ?`
32 ).get(did) as { privateKeyJwk: string } | undefined
33 if (!row) return null
34 return Secp256k1Keypair.import(JSON.parse(row.privateKeyJwk), { exportable: true })
35 }
36
37 /** Reserve a signing key for a DID before account creation (migration flow). */
38 async reserveKeypair(did: string | undefined): Promise<ExportableKeypair> {
39 const keypair = await Secp256k1Keypair.create({ exportable: true })
40 const jwk = await keypair.export()
41 const key = did ?? keypair.did()
42 this.db.prepare(`
43 INSERT OR IGNORE INTO reserved_keypair (did, privateKeyJwk, createdAt) VALUES (?, ?, ?)
44 `).run(key, JSON.stringify(jwk), new Date().toISOString())
45 return keypair
46 }
47
48 async getReservedKeypair(did: string): Promise<ExportableKeypair | null> {
49 const row = this.db.prepare(
50 `SELECT privateKeyJwk FROM reserved_keypair WHERE did = ?`
51 ).get(did) as { privateKeyJwk: string } | undefined
52 if (!row) return null
53 return Secp256k1Keypair.import(JSON.parse(row.privateKeyJwk), { exportable: true })
54 }
55
56 clearReservedKeypair(did: string) {
57 this.db.prepare(`DELETE FROM reserved_keypair WHERE did = ?`).run(did)
58 }
59
60 promoteReservedKeypair(did: string): boolean {
61 const row = this.db.prepare(
62 `SELECT privateKeyJwk FROM reserved_keypair WHERE did = ?`
63 ).get(did) as { privateKeyJwk: string } | undefined
64 if (!row) return false
65 this.db.prepare(`
66 INSERT OR REPLACE INTO signing_key (did, privateKeyJwk, createdAt) VALUES (?, ?, ?)
67 `).run(did, row.privateKeyJwk, new Date().toISOString())
68 this.db.prepare(`DELETE FROM reserved_keypair WHERE did = ?`).run(did)
69 return true
70 }
71}