import { CID, cborEncodeDagCbor, cidToString, createBlobCid, createCid, } from '@pdsjs/core/repo'; import { beforeEach, describe, expect, it } from 'vitest'; import { createSiteHandler } from '../src/handler.js'; import { clearManifestCache } from '../src/manifest.js'; const DID = 'did:plc:handlertest'; const DOMAIN = 'example.com'; /** * Build a handler over an in-memory site. Returns the handler plus call * counters so tests can assert which storage reads happened. * @param {Object} [opts] * @param {Record} [opts.files] * @param {string} [opts.fallback] * @param {string} [opts.site] */ async function setup(opts = {}) { const siteName = opts.site || 'home'; /** @type {Map} */ const blobStore = new Map(); const fileEntries = []; const cids = /** @type {Record} */ ({}); for (const [path, spec] of Object.entries(opts.files || {})) { const data = new TextEncoder().encode(spec.content); const cidBytes = await createBlobCid(data); const cid = cidToString(cidBytes); cids[path] = cid; const mimeType = spec.mimeType ?? 'application/octet-stream'; blobStore.set(cid, { data, mimeType }); const entry = /** @type {Record} */ ({ path, blob: { $type: 'blob', ref: new CID(cidBytes), mimeType, size: data.length, }, }); if (spec.contentType) entry.contentType = spec.contentType; if (spec.cacheControl) entry.cacheControl = spec.cacheControl; if (spec.originTemplate) entry.originTemplate = true; fileEntries.push(entry); } const record = /** @type {Record} */ ({ $type: 'dev.pdsjs.site.deploy', files: fileEntries, createdAt: '2026-08-06T00:00:00.000Z', }); if (opts.fallback) record.fallback = opts.fallback; const bytes = cborEncodeDagCbor(record); const recordCid = cidToString(await createCid(bytes)); const calls = { getRecord: 0, blobGet: 0 }; const handler = createSiteHandler({ actorStorage: /** @type {any} */ ({ getRecord: async (/** @type {string} */ uri) => { calls.getRecord++; return uri === `at://${DID}/dev.pdsjs.site.deploy/${siteName}` ? { cid: recordCid, value: bytes } : null; }, }), blobs: /** @type {any} */ ({ get: async (/** @type {string} */ _did, /** @type {string} */ cid) => { calls.blobGet++; return blobStore.get(cid) || null; }, }), getDid: async () => DID, siteDomain: DOMAIN, }); /** @param {string} urlStr @param {RequestInit} [init] */ const fetch = (urlStr, init) => { const request = new Request(urlStr, init); return handler(request, new URL(urlStr)); }; return { fetch, calls, cids }; } beforeEach(() => clearManifestCache()); describe('createSiteHandler', () => { it('throws on missing ctx entries', () => { expect(() => createSiteHandler(/** @type {any} */ ({}))).toThrow( /actorStorage/, ); }); it('returns null for hostnames outside the site domain', async () => { const { fetch } = await setup({ files: { 'index.html': { content: 'hi', mimeType: 'text/html' } }, }); expect(await fetch('https://pds.other.dev/index.html')).toBeNull(); expect(await fetch('https://other.org/')).toBeNull(); }); it('serves the apex site at the apex and by subdomain rkey', async () => { const { fetch } = await setup({ files: { 'index.html': { content: '

hi

', mimeType: 'text/html' }, }, }); const apex = await fetch('https://example.com/'); expect(apex?.status).toBe(200); expect(await apex?.text()).toBe('

hi

'); expect(apex?.headers.get('content-type')).toBe('text/html'); const sub = await fetch('https://home.example.com/index.html'); expect(sub?.status).toBe(200); }); it('404s an unknown site and multi-label subdomains', async () => { const { fetch } = await setup({ files: { 'index.html': { content: 'hi', mimeType: 'text/html' } }, }); expect((await fetch('https://nope.example.com/'))?.status).toBe(404); expect((await fetch('https://a.b.example.com/'))?.status).toBe(404); }); it('redirects www to the apex when no www site is deployed', async () => { const { fetch } = await setup({ files: { 'index.html': { content: 'hi', mimeType: 'text/html' } }, }); const response = await fetch('https://www.example.com/a/b?q=1'); expect(response?.status).toBe(301); expect(response?.headers.get('location')).toBe( 'https://example.com/a/b?q=1', ); }); it('serves a deployed www site instead of redirecting', async () => { const { fetch } = await setup({ site: 'www', files: { 'index.html': { content: 'the www site', mimeType: 'text/html' }, }, }); const response = await fetch('https://www.example.com/'); expect(response?.status).toBe(200); expect(await response?.text()).toBe('the www site'); }); it('fills the origin token in an origin-templated file', async () => { const { fetch } = await setup({ files: { 'client-metadata.json': { content: '{"client_id":"__PDSJS_ORIGIN__/client-metadata.json","redirect_uris":["__PDSJS_ORIGIN__/callback"]}', mimeType: 'application/json', originTemplate: true, }, }, }); const res = await fetch(`https://${DOMAIN}/client-metadata.json`); expect(res?.status).toBe(200); // The blob cid no longer names the transformed bytes. expect(res?.headers.get('ETag')).toBeNull(); const body = await res?.json(); expect(body.client_id).toBe(`https://${DOMAIN}/client-metadata.json`); expect(body.redirect_uris).toEqual([`https://${DOMAIN}/callback`]); const head = await fetch(`https://${DOMAIN}/client-metadata.json`, { method: 'HEAD', }); expect(Number(head?.headers.get('Content-Length'))).toBeGreaterThan(0); }); it('answers handle verification on site hostnames', async () => { const { fetch } = await setup({ files: {} }); const response = await fetch('https://example.com/.well-known/atproto-did'); expect(response?.status).toBe(200); expect(await response?.text()).toBe(DID); // A browser app reads this from the handle's own hostname. expect(response?.headers.get('access-control-allow-origin')).toBe('*'); }); it('answers a preflight for handle verification', async () => { const { fetch } = await setup({ files: {} }); const response = await fetch( 'https://example.com/.well-known/atproto-did', { method: 'OPTIONS' }, ); expect(response?.status).toBe(204); expect(response?.headers.get('access-control-allow-origin')).toBe('*'); }); it('405s non-GET/HEAD including OPTIONS, with no CORS headers', async () => { const { fetch } = await setup({ files: { 'index.html': { content: 'hi', mimeType: 'text/html' } }, }); for (const method of ['POST', 'PUT', 'DELETE', 'OPTIONS']) { const response = await fetch('https://example.com/', { method }); expect(response?.status).toBe(405); expect(response?.headers.get('allow')).toBe('GET, HEAD'); expect(response?.headers.get('access-control-allow-origin')).toBeNull(); } }); it('rejects traversal and undecodable paths', async () => { const { fetch } = await setup({ files: { 'index.html': { content: 'hi', mimeType: 'text/html' } }, }); // Dot segments, literal or percent-encoded, never reach the handler: // WHATWG URL parsing collapses both, so these are ordinary misses. The // resolvePath guard behind them is defense-in-depth for non-URL callers. expect((await fetch('https://example.com/../secrets'))?.status).toBe(404); expect((await fetch('https://example.com/a/%2e%2e/b'))?.status).toBe(404); expect((await fetch('https://example.com/%zz'))?.status).toBe(400); expect((await fetch('https://example.com/a%5cb'))?.status).toBe(400); }); it('serves directory indexes and redirects slashless directories', async () => { const { fetch } = await setup({ files: { 'index.html': { content: 'root', mimeType: 'text/html' }, 'docs/index.html': { content: 'docs', mimeType: 'text/html' }, }, }); expect(await (await fetch('https://example.com/docs/'))?.text()).toBe( 'docs', ); const redirect = await fetch('https://example.com/docs?q=1'); expect(redirect?.status).toBe(301); expect(redirect?.headers.get('location')).toBe('/docs/?q=1'); }); it('serves the SPA fallback with status 200', async () => { const { fetch } = await setup({ files: { 'index.html': { content: 'app', mimeType: 'text/html' } }, fallback: 'index.html', }); const response = await fetch('https://example.com/some/deep/route'); expect(response?.status).toBe(200); expect(await response?.text()).toBe('app'); }); it('404s unmatched paths without a fallback', async () => { const { fetch } = await setup({ files: { 'index.html': { content: 'hi', mimeType: 'text/html' } }, }); const response = await fetch('https://example.com/missing.png'); expect(response?.status).toBe(404); expect(response?.headers.get('cache-control')).toBe('no-store'); }); it('serves files with an open CORS origin', async () => { const { fetch } = await setup({ files: { 'index.html': { content: 'hi', mimeType: 'text/html' } }, }); const response = await fetch('https://example.com/'); expect(response?.headers.get('access-control-allow-origin')).toBe('*'); }); it('sends a strong ETag and answers If-None-Match with a blobless 304', async () => { const { fetch, calls, cids } = await setup({ files: { 'index.html': { content: 'hi', mimeType: 'text/html' } }, }); const first = await fetch('https://example.com/'); const etag = first?.headers.get('etag'); expect(etag).toBe(`"${cids['index.html']}"`); expect(calls.blobGet).toBe(1); const conditional = await fetch('https://example.com/', { headers: { 'If-None-Match': /** @type {string} */ (etag) }, }); expect(conditional?.status).toBe(304); expect(conditional?.headers.get('etag')).toBe(etag); expect(calls.blobGet).toBe(1); const weak = await fetch('https://example.com/', { headers: { 'If-None-Match': `W/${etag}, "other"` }, }); expect(weak?.status).toBe(304); expect(calls.blobGet).toBe(1); }); it('answers HEAD from the manifest without reading the blob', async () => { const { fetch, calls } = await setup({ files: { 'index.html': { content: 'hello', mimeType: 'text/html' } }, }); const response = await fetch('https://example.com/', { method: 'HEAD' }); expect(response?.status).toBe(200); expect(response?.headers.get('content-length')).toBe('5'); expect(await response?.text()).toBe(''); expect(calls.blobGet).toBe(0); }); it('applies MIME precedence: manifest override, stored mime, extension map', async () => { const { fetch } = await setup({ files: { 'styled.css': { content: 'a{}', mimeType: 'text/css', contentType: 'text/css; charset=iso-8859-1', }, 'plain.css': { content: 'b{}', mimeType: 'text/css' }, 'sniffed.js': { content: 'let x', mimeType: 'application/octet-stream', }, 'mystery.bin': { content: '?', mimeType: 'application/octet-stream' }, }, }); const styled = await fetch('https://example.com/styled.css'); expect(styled?.headers.get('content-type')).toBe( 'text/css; charset=iso-8859-1', ); const plain = await fetch('https://example.com/plain.css'); expect(plain?.headers.get('content-type')).toBe('text/css'); const sniffed = await fetch('https://example.com/sniffed.js'); expect(sniffed?.headers.get('content-type')).toBe( 'text/javascript; charset=utf-8', ); const mystery = await fetch('https://example.com/mystery.bin'); expect(mystery?.headers.get('content-type')).toBe( 'application/octet-stream', ); }); it('applies cacheControl overrides and the revalidating default', async () => { const { fetch } = await setup({ files: { 'app.hash123.js': { content: 'x', mimeType: 'text/javascript', cacheControl: 'public, max-age=31536000, immutable', }, 'index.html': { content: 'hi', mimeType: 'text/html' }, }, }); const hashed = await fetch('https://example.com/app.hash123.js'); expect(hashed?.headers.get('cache-control')).toBe( 'public, max-age=31536000, immutable', ); const index = await fetch('https://example.com/'); expect(index?.headers.get('cache-control')).toBe( 'public, max-age=60, must-revalidate', ); expect(index?.headers.get('x-content-type-options')).toBe('nosniff'); expect(index?.headers.get('strict-transport-security')).toBe( 'max-age=31536000', ); expect(index?.headers.get('content-security-policy')).toBeNull(); expect(index?.headers.get('content-disposition')).toBeNull(); }); it('500s in plain text when serving throws', async () => { const handler = createSiteHandler({ actorStorage: /** @type {any} */ ({ getRecord: async () => { throw new Error('storage exploded'); }, }), blobs: /** @type {any} */ ({ get: async () => null }), getDid: async () => DID, siteDomain: DOMAIN, }); const url = 'https://example.com/'; const response = await handler(new Request(url), new URL(url)); expect(response?.status).toBe(500); expect(response?.headers.get('content-type')).toContain('text/plain'); }); }); describe('repo blobs by CID', () => { it('serves media blobs under their stored mime, immutable and sandboxed', async () => { const { fetch, cids } = await setup({ files: { 'photo.webp': { content: 'img-bytes', mimeType: 'image/webp' } }, }); const response = await fetch( `https://example.com/.blobs/${cids['photo.webp']}`, ); expect(response?.status).toBe(200); expect(response?.headers.get('content-type')).toBe('image/webp'); expect(response?.headers.get('cache-control')).toBe( 'public, max-age=31536000, immutable', ); expect(response?.headers.get('content-security-policy')).toContain( 'sandbox', ); expect(await response?.text()).toBe('img-bytes'); }); it('forces non-media mimes to octet-stream', async () => { const { fetch, cids } = await setup({ files: { 'page.html': { content: '', mimeType: 'text/html' }, }, }); const response = await fetch( `https://example.com/.blobs/${cids['page.html']}`, ); expect(response?.headers.get('content-type')).toBe( 'application/octet-stream', ); }); it('answers If-None-Match with 304 and no blob read', async () => { const { fetch, calls, cids } = await setup({ files: { 'photo.webp': { content: 'img', mimeType: 'image/webp' } }, }); const cid = cids['photo.webp']; const response = await fetch(`https://example.com/.blobs/${cid}`, { headers: { 'If-None-Match': `"${cid}"` }, }); expect(response?.status).toBe(304); expect(response?.headers.get('content-type')).toBeNull(); expect(calls.blobGet).toBe(0); }); it('404s an unknown or malformed CID', async () => { const { fetch } = await setup({ files: { 'photo.webp': { content: 'img', mimeType: 'image/webp' } }, }); const missing = await fetch( 'https://example.com/.blobs/bafkreiaaaaaaaaaaaaaaaaaaaaaaaa', ); expect(missing?.status).toBe(404); const malformed = await fetch('https://example.com/.blobs/NOT-A-CID'); expect(malformed?.status).toBe(404); }); });