Something went wrong. Try again.
An AT Protocol Personal Data Server written in JavaScript pdsjs.dev
pds atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155// @pdsjs/core/http - Request and response mechanics every handler shares:// CORS, cookies, HTML responses and draining a body a handler will not read.
// CORS headers for cross-origin requestsexport const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', // Range lets a browser page read single objects out of a git bundle blob // with com.atproto.sync.getBlob. 'Access-Control-Allow-Headers': 'Content-Type, Authorization, DPoP, atproto-accept-labelers, atproto-proxy, x-bsky-topics, x-bsky-is-beta-user, Range', // Browser clients must be able to READ these response headers. Without // exposing DPoP-Nonce, a browser OAuth client cannot see the nonce the server // hands back with `use_dpop_nonce` and so cannot retry — login fails on it. 'Access-Control-Expose-Headers': 'DPoP-Nonce, WWW-Authenticate',};
/** * The CORS headers answering one preflight. A browser names the headers it * wants in Access-Control-Request-Headers, and the reply allows exactly those. * Thus a client sending a header this server has never heard of still gets * through, and the Bluesky app does not break each time it adds one. The * allowance grants nothing on its own: the origin is `*`, so the browser sends * no credentials, and the request that follows still has to authenticate. * @param {Request} request * @returns {Record<string, string>} */export function preflightHeaders(request) { const requested = request.headers.get('Access-Control-Request-Headers'); if (!requested) return corsHeaders; return { ...corsHeaders, 'Access-Control-Allow-Headers': requested };}
/** * Parse a single-range `Range` header against a known length. Returns null * when there is no range to honour, and the string 'unsatisfiable' for a * range that falls outside the file, which is a 416 rather than an error. * @param {string|null} header * @param {number} size * @returns {{start: number, end: number}|null|'unsatisfiable'} */export function parseByteRange(header, size) { if (!header) return null; const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim()); if (!match) return null; const [, rawStart, rawEnd] = match; if (rawStart === '' && rawEnd === '') return null; if (size === 0) return 'unsatisfiable';
let start; let end; if (rawStart === '') { // A suffix range asks for the last N bytes. const length = Number(rawEnd); if (length === 0) return 'unsatisfiable'; start = Math.max(0, size - length); end = size - 1; } else { start = Number(rawStart); end = rawEnd === '' ? size - 1 : Number(rawEnd); } if (!Number.isFinite(start) || !Number.isFinite(end)) return null; if (start >= size || start > end) return 'unsatisfiable'; return { start, end: Math.min(end, size - 1) };}
/** * Add CORS headers to response * @param {Response} response * @returns {Response} */export function addCorsHeaders(response) { // WebSocket upgrade responses (101) can't be reconstructed // webSocket is a Cloudflare-specific property if (response.status === 101 || /** @type {any} */ (response).webSocket) { return response; }
const newHeaders = new Headers(response.headers); for (const [key, value] of Object.entries(corsHeaders)) { newHeaders.set(key, value); } return new Response(response.body, { status: response.status, statusText: response.statusText, headers: newHeaders, });}
/** * Consume a request body that a handler is about to reject unread. * Workers runtimes raise an uncaught error when a response is sent while * the request stream is still readable. * @param {Request} request * @returns {Promise<void>} */export async function drainRequestBody(request) { if (!request.body) return; try { await request.arrayBuffer(); } catch { // Body already consumed or aborted }}
/** * Form posts are only accepted from this server's own pages. `SameSite=Lax` * already keeps the session cookie off cross-site posts; this rejects the * request outright rather than treating it as signed out. * @param {Request} request * @param {URL} url * @returns {boolean} */export function isSameOriginPost(request, url) { const origin = request.headers.get('origin'); return !origin || origin === `${url.protocol}//${url.host}`;}
/** * Read one cookie from a request's Cookie header. * @param {Request} request * @param {string} name * @returns {string|null} */export function readCookie(request, name) { const header = request.headers.get('cookie'); if (!header) return null; for (const pair of header.split(';')) { const eq = pair.indexOf('='); if (eq === -1) continue; if (pair.slice(0, eq).trim() === name) { return decodeURIComponent(pair.slice(eq + 1).trim()); } } return null;}
/** * An HTML response for the account pages. These carry per-account data and a * session cookie, so they must never be cached. * @param {string} html * @param {number} [status=200] * @param {string|null} [setCookie=null] * @returns {Response} */export function htmlResponse(html, status = 200, setCookie = null) { const headers = new Headers({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', 'Referrer-Policy': 'same-origin', }); if (setCookie) headers.set('Set-Cookie', setCookie); return new Response(html, { status, headers });}