demos for spacedust

subscriptions almost working??

+41 -18
+12 -4
atproto-notifications/src/App.tsx
··· 11 11 </div> 12 12 ); 13 13 14 - function requestPermission(setAsking) { 14 + function requestPermission(host, setAsking) { 15 15 return async () => { 16 16 setAsking(true); 17 17 let err; 18 18 try { 19 19 await Notification.requestPermission(); 20 - await subscribeToPush(); 20 + const sub = await subscribeToPush(); 21 + const res = await fetch(`${host}/subscribe`, { 22 + method: 'POST', 23 + headers: {'Content-Type': 'application/json'}, 24 + body: JSON.stringify({ sub }), 25 + credentials: 'include', 26 + }); 27 + if (!res.ok) throw res; 21 28 } catch (e) { 22 29 err = e; 23 30 } 24 31 setAsking(false); 25 32 if (err) throw err; 26 - 27 33 } 28 34 } 29 35 ··· 35 41 }; 36 42 const pushSubscription = await registration.pushManager.subscribe(subscribeOptions); 37 43 console.log({ pushSubscription }); 44 + return pushSubscription; 38 45 } 39 46 40 47 async function verifyUser(host, token) { ··· 42 49 method: 'POST', 43 50 headers: {'Content-Type': 'applicaiton/json'}, 44 51 body: JSON.stringify({ token }), 52 + credentials: 'include', 45 53 }); 46 54 if (!res.ok) throw res; 47 55 } ··· 92 100 <p>To show atproto notifications we need permission:</p> 93 101 <p> 94 102 <button 95 - onClick={requestPermission(setAsking)} 103 + onClick={requestPermission(host, setAsking)} 96 104 disabled={asking} 97 105 > 98 106 {asking ? <>Requesting&hellip;</> : <>Request permission</>}
+29 -14
server/index.js
··· 10 10 11 11 const DUMMY_DID = 'did:plc:zzzzzzzzzzzzzzzzzzzzzzzz'; 12 12 13 - const CORS_PERMISSIVE = { 14 - 'Access-Control-Allow-Origin': '*', 13 + const CORS_PERMISSIVE = req => ({ 14 + 'Access-Control-Allow-Origin': req.headers.origin, // DANGERRRRR 15 15 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST', 16 16 'Access-Control-Allow-Headers': 'Content-Type', 17 - }; 17 + 'Access-Control-Allow-Credentials': 'true', // TODO: *def* want to restrict allowed origin, probably 18 + }); 18 19 19 20 let spacedust; 20 21 let spacedustEverStarted = false; ··· 166 167 const handleIndex = handleFile('index.html', 'text/html'); 167 168 const handleServiceWorker = handleFile('service-worker.js', 'application/javascript'); 168 169 169 - const handleVerify = async (req, res, jwks, app_secret) => { 170 + const handleVerify = async (req, res, jwks, appSecret) => { 170 171 const body = await getRequesBody(req); 171 172 const { token } = JSON.parse(body); 172 173 let did; ··· 177 178 res.setHeader('Set-Cookie', cookie.serialize('verified-did', '', { expires: new Date(0) })); 178 179 return res.writeHead(400).end(JSON.stringify({ reason: 'verification failed' })); 179 180 } 180 - const signed = cookieSig.sign(did, app_secret); 181 + const signed = cookieSig.sign(did, appSecret); 181 182 res.setHeader('Set-Cookie', cookie.serialize('verified-did', signed, { 182 183 httpOnly: true, 183 184 secure: true, ··· 186 187 return res.writeHead(200).end('okayyyy'); 187 188 }; 188 189 189 - const handleSubscribe = async (req, res) => { 190 + const handleSubscribe = async (req, res, appSecret) => { 191 + const rawCookies = req.headers.cookie; 192 + const cookies = cookie.parse(req.headers.cookie ?? ''); 193 + const untrusted = cookies['verified-did'] ?? ''; 194 + const did = cookieSig.unsign(untrusted, appSecret); 195 + if (!did) { 196 + res.setHeader('Set-Cookie', cookie.serialize('verified-did', '', { expires: new Date(0) })); 197 + return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' })); 198 + } 190 199 const body = await getRequesBody(req); 191 - const { did, sub } = JSON.parse(body); 200 + const { sub } = JSON.parse(body); 201 + addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG) 192 202 addSub(did, sub); 193 203 res.setHeader('Content-Type', 'application/json'); 194 204 res.writeHead(201); 195 205 res.end('{"oh": "hi"}'); 196 206 }; 197 207 198 - const requestListener = (pubkey, jwks, app_secret) => (req, res) => { 208 + const requestListener = (pubkey, jwks, appSecret) => (req, res) => { 199 209 if (req.method === 'GET' && req.url === '/') { 200 210 return handleIndex(req, res, { PUBKEY: pubkey }); 201 211 } ··· 205 215 206 216 if (req.method === 'OPTIONS' && req.url === '/verify') { 207 217 // TODO: probably restrict the origin 208 - return res.writeHead(204, CORS_PERMISSIVE).end(); 218 + return res.writeHead(204, CORS_PERMISSIVE(req)).end(); 209 219 } 210 220 if (req.method === 'POST' && req.url === '/verify') { 211 - res.setHeaders(new Headers(CORS_PERMISSIVE)); 212 - return handleVerify(req, res, jwks, app_secret); 221 + res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 222 + return handleVerify(req, res, jwks, appSecret); 213 223 } 214 224 225 + if (req.method === 'OPTIONS' && req.url === '/subscribe') { 226 + // TODO: probably restrict the origin 227 + return res.writeHead(204, CORS_PERMISSIVE(req)).end(); 228 + } 215 229 if (req.method === 'POST' && req.url === '/subscribe') { 216 - return handleSubscribe(req, res); 230 + res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 231 + return handleSubscribe(req, res, appSecret); 217 232 } 218 233 219 234 res.writeHead(200); ··· 230 245 ); 231 246 232 247 if (!env.APP_SECRET) throw new Error('APP_SECRET is required to run'); 233 - const app_secret = env.APP_SECRET; 248 + const appSecret = env.APP_SECRET; 234 249 235 250 const whoamiHost = env.WHOAMI_HOST ?? 'https://who-am-i.microcosm.blue'; 236 251 const jwks = jose.createRemoteJWKSet(new URL(`${whoamiHost}/.well-known/jwks.json`)); ··· 242 257 const port = parseInt(env.PORT ?? 8000, 10); 243 258 244 259 http 245 - .createServer(requestListener(keys.publicKey, jwks, app_secret)) 260 + .createServer(requestListener(keys.publicKey, jwks, appSecret)) 246 261 .listen(port, host, () => console.log(`listening at http://${host}:${port}`)); 247 262 }; 248 263