podcast manager
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 111 lines 2.9 kB view raw
1import * as jose from 'jose' 2import {z} from 'zod/v4' 3import {CryptoError} from './errors' 4 5export type JWK = jose.JWK 6 7const subtleSignAlgo = {name: 'ECDSA', namedCurve: 'P-256'} 8const joseSignAlgo = {name: 'ES256'} 9 10const jwkEcPublicSchema = z.object({ 11 kty: z.literal('EC'), 12 crv: z.string(), 13 x: z.string(), 14 y: z.string(), 15}) 16 17const jwkEcPrivateSchema = z.object({ 18 ...jwkEcPublicSchema.shape, 19 d: z.string(), 20}) 21 22/** 23 * a zod schema describing a JWK from jose 24 * we only support EC keys, to make life easier 25 * 26 * @see https://www.rfc-editor.org/rfc/rfc7517 27 * @see https://github.com/panva/jose/blob/main/src/types.d.ts#L2 28 */ 29export const jwkSchema: z.ZodType<jose.JWK> = z.union([jwkEcPublicSchema, jwkEcPrivateSchema]) 30 31/** 32 * zod transform from JWK to CryptoKey 33 */ 34export const jwkImport: z.ZodTransform<CryptoKey, jose.JWK> = z.transform(async (val, ctx) => { 35 try { 36 if (typeof val === 'object') { 37 const key = await jose.importJWK(val, joseSignAlgo.name) 38 if (key instanceof CryptoKey) { 39 return key 40 } 41 42 ctx.issues.push({ 43 code: 'custom', 44 message: 'symmetric keys unsupported', 45 input: val, 46 }) 47 } else { 48 ctx.issues.push({ 49 code: 'custom', 50 message: 'not a valid JWK object', 51 input: val, 52 }) 53 } 54 } catch (e) { 55 ctx.issues.push({ 56 code: 'custom', 57 message: `could not import JWK object: ${e}`, 58 input: val, 59 }) 60 } 61 62 return z.NEVER 63}) 64 65/** zod transform from exportable CryptoKey to JWK */ 66export const jwkExport: z.ZodTransform<jose.JWK, CryptoKey> = z.transform(async (val, ctx) => { 67 try { 68 if (val.extractable) { 69 return await jose.exportJWK(val) 70 } 71 72 ctx.issues.push({ 73 code: 'custom', 74 message: 'non-extractable key!', 75 input: val, 76 }) 77 } catch (e) { 78 ctx.issues.push({ 79 code: 'custom', 80 message: `could not export JWK object: ${e}`, 81 input: val, 82 }) 83 } 84 85 return z.NEVER 86}) 87 88/** 89 * @returns a newly generated, signing compatible keypair 90 */ 91export async function generateSigningJwkPair(): Promise<CryptoKeyPair> { 92 const pair = await crypto.subtle.generateKey(subtleSignAlgo, false, ['sign', 'verify']) 93 if (!('publicKey' in pair)) throw new CryptoError('keypair returned a single key!?') 94 95 return pair 96} 97 98/** 99 * @param payload - the payload to sign 100 * @returns a properly configured jwt signer, with the payload provided 101 */ 102declare const _INFERRED: unique symbol 103type Inferred = typeof _INFERRED 104 105export function generateSignableJwt(payload: jose.JWTPayload): jose.SignJWT 106export function generateSignableJwt<T>(payload: jose.JWTPayload & {payload: T}): jose.SignJWT 107export function generateSignableJwt<T = Inferred>( 108 payload: T extends Inferred ? jose.JWTPayload : jose.JWTPayload & {payload: T}, 109): jose.SignJWT { 110 return new jose.SignJWT(payload).setProtectedHeader({alg: joseSignAlgo.name}) 111}