AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

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

at main 99 lines 3.0 kB view raw
1// DPoP key management (IndexedDB) and proof creation 2 3import { randomString, sha256Base64Url, signJwt } from './crypto.js' 4 5const DB_VERSION = 1 6const KEY_STORE = 'dpop-keys' 7const KEY_ID = 'dpop-key' 8 9const dbPromises = new Map() 10 11function openDatabase(namespace) { 12 const existing = dbPromises.get(namespace) 13 if (existing) return existing 14 15 const promise = new Promise((resolve, reject) => { 16 const req = indexedDB.open(`appview-oauth-${namespace}`, DB_VERSION) 17 req.onerror = () => reject(req.error) 18 req.onsuccess = () => resolve(req.result) 19 req.onupgradeneeded = (e) => { 20 const db = e.target.result 21 if (!db.objectStoreNames.contains(KEY_STORE)) { 22 db.createObjectStore(KEY_STORE, { keyPath: 'id' }) 23 } 24 } 25 }) 26 27 dbPromises.set(namespace, promise) 28 return promise 29} 30 31async function getStoredKey(namespace) { 32 const db = await openDatabase(namespace) 33 return new Promise((resolve, reject) => { 34 const tx = db.transaction(KEY_STORE, 'readonly') 35 const req = tx.objectStore(KEY_STORE).get(KEY_ID) 36 req.onsuccess = () => resolve(req.result || null) 37 req.onerror = () => reject(req.error) 38 }) 39} 40 41async function storeKey(namespace, privateKey, publicJwk) { 42 const db = await openDatabase(namespace) 43 return new Promise((resolve, reject) => { 44 const tx = db.transaction(KEY_STORE, 'readwrite') 45 const req = tx.objectStore(KEY_STORE).put({ 46 id: KEY_ID, 47 privateKey, 48 publicJwk, 49 createdAt: Date.now(), 50 }) 51 req.onsuccess = () => resolve() 52 req.onerror = () => reject(req.error) 53 }) 54} 55 56export async function getOrCreateDPoPKey(namespace) { 57 const existing = await getStoredKey(namespace) 58 if (existing) return existing 59 60 const keyPair = await crypto.subtle.generateKey( 61 { name: 'ECDSA', namedCurve: 'P-256' }, 62 false, // non-extractable private key 63 ['sign'], 64 ) 65 66 const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey) 67 await storeKey(namespace, keyPair.privateKey, publicJwk) 68 69 return { id: KEY_ID, privateKey: keyPair.privateKey, publicJwk, createdAt: Date.now() } 70} 71 72export async function clearDPoPKey(namespace) { 73 const db = await openDatabase(namespace) 74 return new Promise((resolve, reject) => { 75 const tx = db.transaction(KEY_STORE, 'readwrite') 76 const req = tx.objectStore(KEY_STORE).delete(KEY_ID) 77 req.onsuccess = () => resolve() 78 req.onerror = () => reject(req.error) 79 }) 80} 81 82export async function createDPoPProof(namespace, method, url, accessToken) { 83 const keyData = await getOrCreateDPoPKey(namespace) 84 const { kty, crv, x, y } = keyData.publicJwk 85 86 const header = { alg: 'ES256', typ: 'dpop+jwt', jwk: { kty, crv, x, y } } 87 const payload = { 88 jti: randomString(16), 89 htm: method, 90 htu: url.split('?')[0], 91 iat: Math.floor(Date.now() / 1000), 92 } 93 94 if (accessToken) { 95 payload.ath = await sha256Base64Url(accessToken) 96 } 97 98 return signJwt(header, payload, keyData.privateKey) 99}