Something went wrong. Try again.
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
Something went wrong. Try again.
18 kB · 497 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498import { beforeEach, describe, expect, mock, test } from 'bun:test'import { PRIVATE_SHARE_QUERY_PARAM } from '@wispplace/constants'import { hashSecret, hashShareTokenSync, PRIVATE_GRANT_QUERY_PARAM, type PrivateSite, sessionCookieName,} from '@wispplace/private-sites'import { Hono } from 'hono'import type { AuthorizedPrivateSite, PrivateHandoffExchangeInput, PrivateHandoffExchangeResult, PrivateShareTokenExchangeInput, PrivateShareTokenExchangeResult,} from './private-sites-db'
const SITE_ID = 'bright-brook-fox-1234'const encoder = new TextEncoder()
const calls = { handoffs: [] as PrivateHandoffExchangeInput[], loads: [] as Array<{ siteId: string; secretHash: string }>, shares: [] as PrivateShareTokenExchangeInput[], storageKeys: [] as string[], touchedShares: [] as string[],}
let exchangeHandoff: (input: PrivateHandoffExchangeInput) => Promise<PrivateHandoffExchangeResult | null>let exchangeShare: (input: PrivateShareTokenExchangeInput) => Promise<PrivateShareTokenExchangeResult | null>let loadAuthorized: (siteId: string, secretHash: string) => Promise<AuthorizedPrivateSite | null>let readStorage: (key: string) => Promise<Uint8Array | null>let touchGrantedShare: (shareId: string) => Promise<void>
mock.module('./private-sites-db', () => ({ exchangePrivateHandoff: (input: PrivateHandoffExchangeInput) => { calls.handoffs.push(input) return exchangeHandoff(input) }, exchangePrivateShareToken: (input: PrivateShareTokenExchangeInput) => { calls.shares.push(input) return exchangeShare(input) }, loadAuthorizedPrivateSite: (siteId: string, secretHash: string) => { calls.loads.push({ siteId, secretHash }) return loadAuthorized(siteId, secretHash) }, touchShare: (shareId: string) => { calls.touchedShares.push(shareId) return touchGrantedShare(shareId) },}))
mock.module('./storage', () => ({ privateStorage: { get: (key: string) => { calls.storageKeys.push(key) return readStorage(key) }, },}))
const { privateNotFound, servePrivateSite } = await import('./private-serving')
const file = (path: string, mimeType: string | null = 'text/plain') => ({ path, size: 4, mimeType })const readyAuthorized = (files: AuthorizedPrivateSite['files']): AuthorizedPrivateSite => ({ site: { state: 'ready', siteId: SITE_ID, ownerDid: 'did:plc:owner', name: 'private-test', fileCount: files.length, totalBytes: files.reduce((total, current) => total + current.size, 0), expiresAt: null, createdAt: new Date('2026-01-01T00:00:00.000Z'), updatedAt: new Date('2026-01-01T00:00:00.000Z'), } satisfies PrivateSite, files,})
const requestFor = (path = '', query = '', init: RequestInit = {}): Request => { return new Request(`https://${SITE_ID}.priv.wisp.place/${path}${query}`, init)}
const secureCookie = (value: string): string => `${sessionCookieName(true)}=${value}`
const expectPrivateHeaders = (response: Response): void => { expect(response.headers.get('cache-control')).toBe('no-store, no-cache, must-revalidate, private') expect(response.headers.get('pragma')).toBe('no-cache') expect(response.headers.get('x-robots-tag')).toBe('noindex, nofollow, noarchive, nosnippet')}
const expectNotFound = async (response: Response): Promise<void> => { expect(response.status).toBe(404) expect(await response.text()).toBe('Not found') expect(response.headers.get('content-type')).toBe('text/plain; charset=utf-8') expectPrivateHeaders(response)}
beforeEach(() => { calls.handoffs.length = 0 calls.loads.length = 0 calls.shares.length = 0 calls.storageKeys.length = 0 calls.touchedShares.length = 0 exchangeHandoff = async () => null exchangeShare = async () => null loadAuthorized = async () => null readStorage = async () => null touchGrantedShare = async () => undefined})
describe('private request validation and anonymous responses', () => { for (const scenario of [ { name: 'backslash traversal', path: '..\\secret.txt' }, { name: 'dot traversal', path: '../secret.txt' }, { name: 'absolute path', path: '/secret.txt' }, { name: 'duplicate separator', path: 'assets//secret.txt' }, { name: 'encoded traversal marker', path: '%2e%2e/secret.txt' }, { name: 'trailing slash', path: 'assets/' }, ]) { test(`rejects ${scenario.name} before authorization or storage`, async () => { const response = await servePrivateSite(requestFor(), SITE_ID, scenario.path)
expect(response.status).toBe(400) expect(await response.text()).toBe('Invalid path') expectPrivateHeaders(response) expect(calls.handoffs).toHaveLength(0) expect(calls.shares).toHaveLength(0) expect(calls.loads).toHaveLength(0) expect(calls.storageKeys).toHaveLength(0) }) }
test('rejects an invalid site id with the generic private not-found response', async () => { const response = await servePrivateSite(requestFor(), 'not-a-private-site', 'index.html')
await expectNotFound(response) expect(calls.handoffs).toHaveLength(0) expect(calls.shares).toHaveLength(0) expect(calls.loads).toHaveLength(0) expect(calls.storageKeys).toHaveLength(0) })
for (const scenario of [ { name: 'no cookie', query: '', cookie: null }, { name: 'wrong cookie name', query: '', cookie: 'wsps=not-secure' }, { name: 'duplicate secure cookies', query: '', cookie: `${secureCookie('first')}; ${secureCookie('second')}`, }, { name: 'empty share parameter', query: `?${PRIVATE_SHARE_QUERY_PARAM}=`, cookie: null }, { name: 'empty handoff parameter', query: `?${PRIVATE_GRANT_QUERY_PARAM}=`, cookie: null }, ]) { test(`keeps ${scenario.name} database-independent and anonymous`, async () => { const headers = new Headers() if (scenario.cookie) headers.set('cookie', scenario.cookie) const response = await servePrivateSite(requestFor('', scenario.query, { headers }), SITE_ID, '')
expect(response.status).toBe(200) expect(await response.text()).toContain('this address needs a share link') expect(response.headers.get('referrer-policy')).toBe('no-referrer') expectPrivateHeaders(response) expect(calls.handoffs).toHaveLength(0) expect(calls.shares).toHaveLength(0) expect(calls.loads).toHaveLength(0) expect(calls.storageKeys).toHaveLength(0) }) }})
describe('private credential exchanges', () => { test('atomically exchanges an unscoped share and removes all credential parameters', async () => { exchangeShare = async () => ({ kind: 'share', shareId: 'share-1' }) const token = 'share-token' const response = await servePrivateSite( requestFor('docs', `?keep=1&${PRIVATE_SHARE_QUERY_PARAM}=${token}&${PRIVATE_GRANT_QUERY_PARAM}=`), SITE_ID, 'docs', )
expect(response.status).toBe(302) expect(response.headers.get('location')).toBe('/docs?keep=1') expect(response.headers.get('set-cookie')).toContain(`${sessionCookieName(true)}=wsx_`) expect(response.headers.get('set-cookie')).toContain('Secure') expect(response.headers.get('set-cookie')).toContain('HttpOnly') expectPrivateHeaders(response) expect(calls.shares).toHaveLength(1) expect(calls.shares[0]?.siteId).toBe(SITE_ID) expect(calls.shares[0]?.tokenHash).toBe(hashShareTokenSync(token)) expect(calls.handoffs).toHaveLength(0) expect(calls.loads).toHaveLength(0) expect(calls.touchedShares).toEqual(['share-1']) })
for (const scenario of [ { name: 'a scoped share', result: { kind: 'audienceMismatch', audienceDid: 'did:plc:only-this-account', } satisfies PrivateShareTokenExchangeResult, status: 200, }, { name: 'an empty scoped audience DID', result: { kind: 'audienceMismatch', audienceDid: '' } satisfies PrivateShareTokenExchangeResult, status: 200, }, { name: 'a revoked, expired, or unknown share', result: null, status: 404 }, ]) { test(`handles ${scenario.name} without loading a session`, async () => { exchangeShare = async () => scenario.result const token = 'token<&' const response = await servePrivateSite( requestFor('', `?${PRIVATE_SHARE_QUERY_PARAM}=${encodeURIComponent(token)}`), SITE_ID, '', )
expect(response.status).toBe(scenario.status) if (scenario.status === 200) { expect(await response.text()).toContain('this link is for a specific account') expect(response.headers.get('referrer-policy')).toBe('strict-origin') } else { await expectNotFound(response) } expect(calls.shares).toHaveLength(1) expect(calls.handoffs).toHaveLength(0) expect(calls.loads).toHaveLength(0) expect(calls.storageKeys).toHaveLength(0) expect(calls.touchedShares).toHaveLength(0) }) }
test('keeps a successful exchange redirecting when best-effort share tracking fails', async () => { exchangeShare = async () => ({ kind: 'share', shareId: 'share-1' }) touchGrantedShare = async () => { throw new Error('share tracking is unavailable') }
const response = await servePrivateSite(requestFor('', `?${PRIVATE_SHARE_QUERY_PARAM}=share-token`), SITE_ID, '')
expect(response.status).toBe(302) expect(calls.touchedShares).toEqual(['share-1']) })
test('fails closed when the share exchange dependency rejects', async () => { exchangeShare = async () => { throw new Error('database details must not reach the response') } const response = await servePrivateSite(requestFor('', `?${PRIVATE_SHARE_QUERY_PARAM}=share-token`), SITE_ID, '')
await expectNotFound(response) expect(calls.shares).toHaveLength(1) expect(calls.loads).toHaveLength(0) })
for (const scenario of [ { name: 'owner handoff', result: { kind: 'owner', ownerDid: 'did:plc:owner' } satisfies PrivateHandoffExchangeResult, touched: [] as string[], }, { name: 'share handoff', result: { kind: 'share', shareId: 'share-from-handoff' } satisfies PrivateHandoffExchangeResult, touched: ['share-from-handoff'], }, { name: 'used or invalid handoff', result: null, touched: [] as string[] }, ]) { test(`handles ${scenario.name} before every other credential`, async () => { exchangeHandoff = async () => scenario.result const handoff = 'one-time-handoff' const response = await servePrivateSite( requestFor('', `?${PRIVATE_SHARE_QUERY_PARAM}=share-token&${PRIVATE_GRANT_QUERY_PARAM}=${handoff}`), SITE_ID, '', )
if (scenario.result) { expect(response.status).toBe(302) expect(response.headers.get('location')).toBe('/') } else { await expectNotFound(response) } expect(calls.handoffs).toHaveLength(1) expect(calls.handoffs[0]?.secretHash).toBe(hashSecret(handoff)) expect(calls.shares).toHaveLength(0) expect(calls.loads).toHaveLength(0) expect(calls.touchedShares).toEqual(scenario.touched) }) }
test('fails closed when the handoff exchange dependency rejects', async () => { exchangeHandoff = async () => { throw new Error('handoff database unavailable') } const response = await servePrivateSite(requestFor('', `?${PRIVATE_GRANT_QUERY_PARAM}=handoff`), SITE_ID, '')
await expectNotFound(response) expect(calls.handoffs).toHaveLength(1) expect(calls.shares).toHaveLength(0) })
test('allows exactly one concurrent atomic handoff exchange', async () => { let available = true exchangeHandoff = async () => { if (!available) return null available = false return { kind: 'owner', ownerDid: 'did:plc:owner' } }
const responses = await Promise.all([ servePrivateSite(requestFor('', `?${PRIVATE_GRANT_QUERY_PARAM}=single-use`), SITE_ID, ''), servePrivateSite(requestFor('', `?${PRIVATE_GRANT_QUERY_PARAM}=single-use`), SITE_ID, ''), ])
expect(responses.map((response) => response.status).sort()).toEqual([302, 404]) expect(calls.handoffs).toHaveLength(2) expect(calls.shares).toHaveLength(0) })})
describe('private cookie sessions and files', () => { test('uses one authorized aggregate lookup and streams the selected file response', async () => { const rawCookie = 'session-secret' loadAuthorized = async () => readyAuthorized([file('index.html', 'text/html; charset=utf-8')]) readStorage = async () => encoder.encode('hello')
const response = await servePrivateSite( requestFor('', '', { headers: { cookie: secureCookie(rawCookie) } }), SITE_ID, '', )
expect(response.status).toBe(200) expect(response.body).not.toBeNull() expect(await response.text()).toBe('hello') expect(response.headers.get('content-type')).toBe('text/html; charset=utf-8') expect(response.headers.get('content-length')).toBe('5') expectPrivateHeaders(response) expect(calls.loads).toEqual([{ siteId: SITE_ID, secretHash: hashSecret(rawCookie) }]) expect(calls.storageKeys).toEqual([`private/${SITE_ID}/index.html`]) expect(calls.handoffs).toHaveLength(0) expect(calls.shares).toHaveLength(0) })
test('keeps server HEAD and Range response semantics after authorization', async () => { loadAuthorized = async () => readyAuthorized([file('index.html')]) readStorage = async () => encoder.encode('hello') const app = new Hono() app.get('/*', (context) => servePrivateSite(context.req.raw, SITE_ID, 'index.html')) const headers = { cookie: secureCookie('session-secret') }
const headResponse = await app.request( new Request(`https://${SITE_ID}.priv.wisp.place/`, { method: 'HEAD', headers }), ) expect(headResponse.status).toBe(200) expect(headResponse.headers.get('content-length')).toBe('5') expect(await headResponse.text()).toBe('')
const rangeResponse = await app.request( new Request(`https://${SITE_ID}.priv.wisp.place/`, { headers: { ...headers, range: 'bytes=1-3' }, }), ) expect(rangeResponse.status).toBe(200) expect(rangeResponse.headers.get('content-range')).toBeNull() expect(await rangeResponse.text()).toBe('hello') })
for (const scenario of [ { name: 'an unknown or expired session', load: async () => null, status: 200 }, { name: 'an unavailable authorized-site dependency', load: async () => { throw new Error('database failure') }, status: 404, }, ]) { test(`handles ${scenario.name} without reading storage`, async () => { loadAuthorized = scenario.load const response = await servePrivateSite( requestFor('', '', { headers: { cookie: secureCookie('session-secret') } }), SITE_ID, '', )
if (scenario.status === 200) { expect(await response.text()).toContain('this address needs a share link') } else { await expectNotFound(response) } expect(calls.loads).toHaveLength(1) expect(calls.storageKeys).toHaveLength(0) }) }
for (const scenario of [ { name: 'an exact file before its directory index', requested: 'docs', files: [file('docs'), file('docs/index.html')], target: 'docs', }, { name: 'the root index preference', requested: '', files: [file('index.htm'), file('index.html')], target: 'index.html', }, { name: 'a directory index', requested: 'docs', files: [file('docs/index.htm')], target: 'docs/index.htm' }, { name: 'a legacy root exact file', requested: 'guide.txt', files: [file('dist/guide.txt')], target: 'dist/guide.txt', }, { name: 'a legacy root directory index', requested: 'docs', files: [file('dist/docs/index.html')], target: 'dist/docs/index.html', }, { name: 'a missing target', requested: 'missing', files: [file('index.html')], target: null }, ]) { test(`resolves ${scenario.name}`, async () => { loadAuthorized = async () => readyAuthorized(scenario.files) readStorage = async () => encoder.encode(`body:${scenario.target}`) const response = await servePrivateSite( requestFor(scenario.requested, '', { headers: { cookie: secureCookie('session-secret') } }), SITE_ID, scenario.requested, )
if (scenario.target === null) { await expectNotFound(response) expect(calls.storageKeys).toHaveLength(0) return } expect(response.status).toBe(200) expect(await response.text()).toBe(`body:${scenario.target}`) expect(calls.storageKeys).toEqual([`private/${SITE_ID}/${scenario.target}`]) }) }
for (const scenario of [ { name: 'an empty stored path', files: [file('')] }, { name: 'a traversal stored path', files: [file('../secret.txt')] }, { name: 'a trailing-slash stored path', files: [file('assets/')] }, { name: 'a backslash stored path', files: [file('assets\\secret.txt')] }, ]) { test(`fails closed for ${scenario.name}`, async () => { loadAuthorized = async () => readyAuthorized(scenario.files) const response = await servePrivateSite( requestFor('', '', { headers: { cookie: secureCookie('session-secret') } }), SITE_ID, '', )
await expectNotFound(response) expect(calls.loads).toHaveLength(1) expect(calls.storageKeys).toHaveLength(0) }) }
for (const scenario of [ { name: 'a missing stored object', read: async () => null }, { name: 'a storage dependency failure', read: async () => { throw new Error('storage failure') }, }, ]) { test(`returns generic not-found for ${scenario.name}`, async () => { loadAuthorized = async () => readyAuthorized([file('index.html')]) readStorage = scenario.read const response = await servePrivateSite( requestFor('', '', { headers: { cookie: secureCookie('session-secret') } }), SITE_ID, '', )
await expectNotFound(response) expect(calls.storageKeys).toEqual([`private/${SITE_ID}/index.html`]) }) }})
test('privateNotFound always uses the generic no-store response', async () => { await expectNotFound(privateNotFound())})