// An image host: drop a file in, get a link to share. Deploy it to your own // PDS with PDS_URL, PDS_DID and PDS_APP_PASSWORD set; see docs/apps.md. // // Images are com.chadtmiller.drop.image records in the owner's repo and the // bytes are ordinary blobs. The shared link is the site's /.blobs/ // route, which serves the original bytes immutably. Reads are public through // dev.pdsjs.query.run; uploads need an app password, which the app exchanges // for a session against the owner's own PDS. // // Usage: PDS_APP_PASSWORD=... node drop.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 = 'drop-app'; const SITES = 'dev.pdsjs.site.deploy'; const DEFS = 'dev.pdsjs.query.def'; // The collection images are written to. An NSID's authority is every segment // but the last, reversed, so this names a domain you control. const IMAGES = process.env.DROP_COLLECTION || 'com.chadtmiller.drop.image'; 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, 'drop-images', { collection: IMAGES, sort: { field: 'createdAt', direction: 'desc' }, createdAt: new Date().toISOString(), }); // ---- the app ---- const HTML = ` Drop

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', 'drop', { name: 'Drop', description: 'An image host in your own repo: drag, paste or choose a file and the link copies itself. Images are blobs served immutably from your site, so the link works anywhere an image URL does.', icon, site: { uri: deployed.uri, cid: deployed.cid }, suggestedSite: 'drop', primitives: ['serve', 'query'], external: false, queries: [ { name: 'drop-images', collection: IMAGES, sort: { field: 'createdAt', direction: 'desc' }, }, ], settings: [ { key: 'gallery', label: 'Show every image to visitors', type: 'boolean', default: true, }, ], createdAt: new Date().toISOString(), }); console.error('Published dev.pdsjs.app.manifest/drop.');