|
1 | | -import { exportJWK, generateKeyPair as joseGenerateKeyPair, importJWK, KeyLike } from 'jose'; |
| 1 | +import { importJWK, type JWK, type KeyLike } from 'jose'; |
2 | 2 |
|
3 | | -interface KeyPair { |
| 3 | +// For production, consider caching the imported key to avoid re-parsing on every request. |
| 4 | +export async function getKeyPair(): Promise<{ |
4 | 5 | privateKey: KeyLike; |
5 | | - publicJwk: JsonWebKey & { kid: string; alg: string }; |
6 | | -} |
7 | | - |
8 | | -/** |
9 | | - * Generates a key pair if none is available on the env. |
10 | | - * Uses globalThis to survive Next.js HMR — without this, dev-mode module |
11 | | - * reloads regenerate the keys while the PowerSync service still holds the |
12 | | - * old public key, causing "signature verification failed" errors. |
13 | | - */ |
14 | | -async function ensureKeyPair(): Promise<KeyPair> { |
15 | | - const g = globalThis as Record<string, unknown>; |
16 | | - if (g.__powersync_keypair) return g.__powersync_keypair as KeyPair; |
17 | | - |
| 6 | + publicJwk: JWK & { kid: string; alg: string }; |
| 7 | + alg: string; |
| 8 | + kid: string; |
| 9 | +}> { |
18 | 10 | const envPrivate = process.env.POWERSYNC_PRIVATE_KEY; |
19 | 11 | const envPublic = process.env.POWERSYNC_PUBLIC_KEY; |
20 | 12 |
|
21 | | - let privateKey: KeyLike; |
22 | | - let publicJwk: JsonWebKey & { kid: string; alg: string }; |
| 13 | + if (!envPrivate || !envPublic) { |
| 14 | + throw new Error( |
| 15 | + 'POWERSYNC_PRIVATE_KEY and POWERSYNC_PUBLIC_KEY are not set in .env.local. Run `pnpm generate-keys` and paste the output into .env.local, then restart the dev server.' |
| 16 | + ); |
| 17 | + } |
| 18 | + |
| 19 | + const privateJwk = parseJwk('POWERSYNC_PRIVATE_KEY', envPrivate); |
| 20 | + const publicJwk = parseJwk('POWERSYNC_PUBLIC_KEY', envPublic); |
23 | 21 |
|
24 | | - if (envPrivate && envPublic) { |
25 | | - const privateJwk = JSON.parse(Buffer.from(envPrivate, 'base64').toString()); |
26 | | - privateKey = (await importJWK(privateJwk)) as KeyLike; |
27 | | - publicJwk = JSON.parse(Buffer.from(envPublic, 'base64').toString()); |
28 | | - } else { |
29 | | - console.warn('POWERSYNC_PRIVATE_KEY not set. Generating a temporary key pair (will not survive restarts).'); |
30 | | - const generated = await joseGenerateKeyPair('RS256', { extractable: true }); |
31 | | - privateKey = generated.privateKey; |
32 | | - publicJwk = (await exportJWK(generated.publicKey)) as JsonWebKey & { kid: string; alg: string }; |
33 | | - publicJwk.alg = 'RS256'; |
34 | | - publicJwk.kid = 'powersync-anon-key'; |
| 22 | + if (privateJwk.kid !== publicJwk.kid) { |
| 23 | + throw new Error( |
| 24 | + `POWERSYNC_PRIVATE_KEY and POWERSYNC_PUBLIC_KEY have mismatched kids (${privateJwk.kid} vs ${publicJwk.kid}). Run \`pnpm generate-keys\` to create a matching pair.` |
| 25 | + ); |
35 | 26 | } |
36 | 27 |
|
37 | | - const pair: KeyPair = { privateKey, publicJwk }; |
38 | | - g.__powersync_keypair = pair; |
39 | | - return pair; |
| 28 | + const privateKey = (await importJWK(privateJwk)) as KeyLike; |
| 29 | + return { privateKey, publicJwk, alg: privateJwk.alg, kid: privateJwk.kid }; |
40 | 30 | } |
41 | 31 |
|
42 | | -export { ensureKeyPair as getKeyPair }; |
| 32 | +function parseJwk(name: string, base64: string): JWK & { kid: string; alg: string } { |
| 33 | + let parsed: unknown; |
| 34 | + try { |
| 35 | + parsed = JSON.parse(Buffer.from(base64, 'base64').toString()); |
| 36 | + } catch { |
| 37 | + throw new Error(`${name} could not be decoded. Run \`pnpm generate-keys\` and paste the output into .env.local.`); |
| 38 | + } |
| 39 | + |
| 40 | + const jwk = parsed as Partial<JWK> & { kid?: string; alg?: string }; |
| 41 | + if (!jwk || typeof jwk !== 'object' || !jwk.kty || !jwk.alg || !jwk.kid) { |
| 42 | + throw new Error( |
| 43 | + `${name} is missing required JWK fields (kty, alg, kid). Run \`pnpm generate-keys\` to create a fresh pair.` |
| 44 | + ); |
| 45 | + } |
| 46 | + return jwk as JWK & { kid: string; alg: string }; |
| 47 | +} |
0 commit comments