// A coffee roast log kept as atproto records. Deploy it to your own PDS // with PDS_URL, PDS_DID and PDS_APP_PASSWORD set; see docs/apps.md. // // Records are com.chadtmiller.coffee.roast in the owner's repo. Reads are // public through dev.pdsjs.query.run; writes need an app password, which the // app exchanges for a session against the owner's own PDS. Photos are // ordinary blobs, shown through the site's /.blobs/ route. // // Usage: PDS_APP_PASSWORD=... node build-roasts-site.mjs const PDS = process.env.PDS_URL; const DID = process.env.PDS_DID; if (!PDS || !DID) throw new Error('Set PDS_URL and PDS_DID.'); const SITE = 'roasts-app'; const SITES = 'dev.pdsjs.site.deploy'; const DEFS = 'dev.pdsjs.query.def'; // The collection roasts are written to. An NSID's authority is every // segment but the last, reversed, so this names a domain you control. const ROASTS = process.env.ROAST_COLLECTION || 'com.chadtmiller.coffee.roast'; async function xrpc(path, init = {}) { const response = await fetch(`${PDS}/xrpc/${path}`, init); if (!response.ok) { throw new Error( `${path.split('?')[0]} ${response.status}: ${(await response.text()).slice(0, 200)}`, ); } return response.json(); } const password = process.env.PDS_APP_PASSWORD; if (!password) throw new Error('Set PDS_APP_PASSWORD.'); const session = await xrpc('com.atproto.server.createSession', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier: DID, password }), }); const auth = { Authorization: `Bearer ${session.accessJwt}` }; /** @param {string} collection @param {string} rkey @param {Record} record */ async function put(collection, rkey, record) { await xrpc('com.atproto.repo.putRecord', { method: 'POST', headers: { ...auth, 'Content-Type': 'application/json' }, body: JSON.stringify({ repo: DID, collection, rkey, record: { $type: collection, ...record }, }), }); console.error(`put ${collection}/${rkey}`); } // ---- the query the app reads ---- await put(DEFS, 'coffee-roasts', { collection: ROASTS, sort: { field: 'roastedAt', direction: 'desc' }, createdAt: new Date().toISOString(), }); // ---- the app ---- const HTML = ` Roasts

Loading…

`; // ---- deploy ---- const upload = await fetch(`${PDS}/xrpc/com.atproto.repo.uploadBlob`, { method: 'POST', headers: { ...auth, 'Content-Type': 'text/html; charset=utf-8' }, body: HTML, }); const blob = (await upload.json()).blob; await put(SITES, SITE, { files: [ { path: 'index.html', blob, contentType: 'text/html; charset=utf-8', cacheControl: 'public, max-age=300', }, ], fallback: 'index.html', createdAt: new Date().toISOString(), }); console.error( `Deployed ${SITE}: ${Math.round(HTML.length / 1024)}KB, one file.`, ); // ---- publish it as an installable app ---- const ICON = ` `; const iconUp = await fetch(`${PDS}/xrpc/com.atproto.repo.uploadBlob`, { method: 'POST', headers: { ...auth, 'Content-Type': 'image/svg+xml' }, body: ICON, }); const icon = (await iconUp.json()).blob; const deployed = await xrpc( `com.atproto.repo.getRecord?repo=${DID}&collection=${SITES}&rkey=${SITE}`, ); await put('dev.pdsjs.app.manifest', 'roasts', { name: 'Roasts', description: 'A coffee roast log kept in your own repo: a live roast timer that marks dry end, first crack and drop, weight-loss and development-time metrics, Behmor profile and batch settings, notes and photos.', icon, site: { uri: deployed.uri, cid: deployed.cid }, suggestedSite: 'roasts', primitives: ['serve', 'query'], external: false, queries: [ { name: 'coffee-roasts', collection: ROASTS, sort: { field: 'roastedAt', direction: 'desc' }, }, ], createdAt: new Date().toISOString(), }); console.error('Published dev.pdsjs.app.manifest/roasts.');