Something went wrong. Try again.
Coves frontend - a photon fork
Something went wrong. Try again.
74 kB · 2076 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077import { describe, it, expect, vi, beforeEach } from 'vitest'import type { RequestEvent, ResolveOptions } from '@sveltejs/kit'import { createMockCookies, createMockEvent, isRedirect,} from '$lib/test-utils/request-event'
// Variable to control the mocked instance URLlet mockPublicInternalInstance: string | undefined = 'http://localhost:4000'let mockPublicInstanceUrl: string | undefined = undefined
// Variable to control dev mode (default false to avoid hostname redirects in most tests)let mockDev = falselet mockBuilding = false
// Variable to control the mocked LOG_STACKS private env varlet mockLogStacks: string | undefined = undefined
// Mock $app/environmentvi.mock('$app/environment', () => ({ get dev() { return mockDev }, browser: false, get building() { return mockBuilding }, version: 'test',}))
// Mock environment variablesvi.mock('$env/dynamic/public', () => ({ env: { get PUBLIC_INTERNAL_INSTANCE() { return mockPublicInternalInstance }, get PUBLIC_INSTANCE_URL() { return mockPublicInstanceUrl }, },}))
let mockOrigin: string | undefined
let mockCspVideoOrigins: string | undefined = 'https://pds.coves.me'
// Mock private environment variables (LOG_STACKS gates stack inclusion)vi.mock('$env/dynamic/private', () => ({ env: { get ORIGIN() { return mockOrigin }, get LOG_STACKS() { return mockLogStacks }, get CSP_VIDEO_ORIGINS() { return mockCspVideoOrigins }, },}))
// Controllable stand-in for SvelteKit's per-request event accessor. Declared// via vi.hoisted so the hoisted vi.mock factory below can close over it, and// kept as one stable vi.fn so tests can assert it was actually consulted.const { mockGetRequestEvent } = vi.hoisted(() => ({ mockGetRequestEvent: vi.fn<() => RequestEvent>(),}))
// Mock $app/server (getRequestEvent throws outside a request in real SvelteKit)vi.mock('$app/server', () => ({ getRequestEvent: mockGetRequestEvent,}))
// Import handle and handleError after mockingconst { handle, handleError, handleFetch } = await import('./hooks.server')
// Mock global fetchconst mockFetch = vi.fn()vi.stubGlobal('fetch', mockFetch)
/** * Creates a mock resolve function that returns a Response */function createMockResolve() { return vi.fn().mockResolvedValue(new Response('OK'))}
/** * Mimics Kit's page renderer: emits the nonce'd CSP and runs the * `transformPageChunk` option the wrapper passes through resolve(). Module * scope because both the security-header and the cache-policy suites need a * response that Kit itself rendered. */function kitPageResolve( html = '<html></html>', extraHeaders: Record<string, string> = {},) { return vi.fn(async (_event: unknown, opts?: ResolveOptions) => { const body = (await opts?.transformPageChunk?.({ html, done: true })) ?? html return new Response(body, { headers: { 'content-type': 'text/html', 'content-security-policy': "script-src 'self' 'nonce-kit123'", ...extraHeaders, }, }) })}
/** * Asserts a console spy received exactly one call carrying exactly one string * argument — the structured log line — and returns it raw and parsed. The raw * form is what secret-leak assertions must run against. */function singleJsonLine(calls: unknown[][]): { raw: string line: Record<string, unknown>} { expect(calls).toHaveLength(1) expect(calls[0]).toHaveLength(1) const raw = calls[0][0] expect(typeof raw).toBe('string') // Assert JSON shape before parsing so a legacy plain-text log line fails // with a readable diff rather than an opaque JSON.parse SyntaxError. expect(raw).toMatch(/^\{[\s\S]*\}$/) return { raw: raw as string, line: JSON.parse(raw as string) as Record<string, unknown>, }}
/** Narrows the `err` member of a parsed log line to an object. */function errOf(line: Record<string, unknown>): Record<string, unknown> { expect(line.err).toBeTypeOf('object') expect(line.err).not.toBeNull() return line.err as Record<string, unknown>}
function spyOnWarn() { return vi.spyOn(console, 'warn').mockImplementation(() => {})}
function spyOnError() { return vi.spyOn(console, 'error').mockImplementation(() => {})}
/** Asserts no console call in these paths ever received a raw Error object. */function expectOnlyStringArgs(calls: unknown[][]): void { for (const call of calls) { for (const arg of call) { expect(typeof arg).toBe('string') expect(arg).not.toBeInstanceOf(Error) } }}
/** Gives a synthetic Response the final URL a real network fetch carries. */function responseAt( url: string, body: BodyInit | null, init?: ResponseInit,): Response { const response = new Response(body, init) Object.defineProperty(response, 'url', { value: url }) return response}
describe('hooks.server handleFetch', () => { beforeEach(() => { mockPublicInternalInstance = 'http://localhost:4000' })
it('makes CORS-free internal responses readable to universal SSR loads', async () => { const event = createMockEvent({ url: 'https://frontend.example/feed' }) const request = new Request( 'http://localhost:4000/xrpc/social.coves.feed.getDiscover', ) const upstreamResponse = responseAt(request.url, '{"feed":[]}', { status: 206, statusText: 'Partial Content', headers: { 'content-type': 'application/json', etag: 'feed-v1' }, }) const fetchFn = vi.fn<typeof fetch>().mockResolvedValue(upstreamResponse)
const response = await handleFetch({ event, request, fetch: fetchFn })
expect(fetchFn).toHaveBeenCalledOnce() const forwardedRequest = fetchFn.mock.calls[0]?.[0] if (!(forwardedRequest instanceof Request)) { throw new Error('Expected a Request at the receiving transport') } expect.soft(forwardedRequest.url).toBe(request.url) expect.soft(forwardedRequest.headers.get('x-real-ip')).toBe('127.0.0.1') expect .soft(forwardedRequest.headers.get('x-forwarded-for')) .toBe('127.0.0.1') expect(response.status).toBe(206) expect(response.statusText).toBe('Partial Content') expect(response.headers.get('content-type')).toBe('application/json') expect(response.headers.get('etag')).toBe('feed-v1') expect(response.headers.get('access-control-allow-origin')).toBe( 'https://frontend.example', ) expect(await response.text()).toBe('{"feed":[]}') })
it('leaves a request to another cross-origin server unchanged', async () => { const event = createMockEvent({ url: 'https://frontend.example/feed' }) const request = new Request('https://untrusted.example/data') const upstreamResponse = new Response('private') const fetchFn = vi.fn<typeof fetch>().mockResolvedValue(upstreamResponse)
const response = await handleFetch({ event, request, fetch: fetchFn })
expect(response).toBe(upstreamResponse) expect(response.headers.has('access-control-allow-origin')).toBe(false) })
it('preserves a valid CORS policy supplied by the internal backend', async () => { const event = createMockEvent({ url: 'https://frontend.example/feed' }) const request = new Request('http://localhost:4000/xrpc/example') const upstreamResponse = responseAt(request.url, 'ok', { headers: { 'access-control-allow-origin': '*' }, }) const fetchFn = vi.fn<typeof fetch>().mockResolvedValue(upstreamResponse)
const response = await handleFetch({ event, request, fetch: fetchFn })
expect(response).toBe(upstreamResponse) expect(response.headers.get('access-control-allow-origin')).toBe('*') })
it('preserves an explicit CORS policy that denies the page origin', async () => { const event = createMockEvent({ url: 'https://frontend.example/feed' }) const request = new Request('http://localhost:4000/xrpc/example') const upstreamResponse = responseAt(request.url, 'denied', { headers: { 'access-control-allow-origin': 'https://different.example', }, }) const fetchFn = vi.fn<typeof fetch>().mockResolvedValue(upstreamResponse)
const response = await handleFetch({ event, request, fetch: fetchFn })
expect(response).toBe(upstreamResponse) expect(response.headers.get('access-control-allow-origin')).toBe( 'https://different.example', ) })
it('does not trust the destination of a cross-origin redirect', async () => { const event = createMockEvent({ url: 'https://frontend.example/feed' }) const request = new Request('http://localhost:4000/xrpc/redirect') const upstreamResponse = responseAt( 'http://metadata.internal/latest', 'secret', ) Object.defineProperty(upstreamResponse, 'redirected', { value: true }) const fetchFn = vi.fn<typeof fetch>().mockResolvedValue(upstreamResponse)
const response = await handleFetch({ event, request, fetch: fetchFn })
expect(response).toBe(upstreamResponse) expect(response.headers.has('access-control-allow-origin')).toBe(false) })
it('leaves same-origin requests unchanged', async () => { mockPublicInternalInstance = 'https://frontend.example' const event = createMockEvent({ url: 'https://frontend.example/feed' }) const request = new Request('https://frontend.example/xrpc/example') const upstreamResponse = new Response('same origin') const fetchFn = vi.fn<typeof fetch>().mockResolvedValue(upstreamResponse)
const response = await handleFetch({ event, request, fetch: fetchFn })
expect(response).toBe(upstreamResponse) expect(response.headers.has('access-control-allow-origin')).toBe(false) })
it('stamps same-origin sub-requests with the page request id', async () => { mockPublicInternalInstance = 'http://localhost:4000' const event = createMockEvent({ url: 'https://frontend.example/feed' }) const request = new Request('https://frontend.example/api/proxy/xrpc/x') const fetchFn = vi.fn<typeof fetch>().mockResolvedValue(new Response(''))
await handleFetch({ event, request, fetch: fetchFn })
const forwarded = fetchFn.mock.calls[0]?.[0] if (!(forwarded instanceof Request)) { throw new Error('Expected a Request at the receiving transport') } expect(forwarded.headers.get('x-request-id')).toBe(event.locals.requestId) })
it('does not send the request id to another origin', async () => { const event = createMockEvent({ url: 'https://frontend.example/feed' }) const request = new Request('https://untrusted.example/data') const fetchFn = vi.fn<typeof fetch>().mockResolvedValue(new Response(''))
await handleFetch({ event, request, fetch: fetchFn })
const forwarded = fetchFn.mock.calls[0]?.[0] expect(forwarded).toBe(request) expect(request.headers.has('x-request-id')).toBe(false) })
it('does not infer a trusted origin when the internal instance is unset', async () => { mockPublicInternalInstance = undefined const event = createMockEvent({ url: 'https://frontend.example/feed' }) const request = new Request('http://localhost:4000/xrpc/example') const upstreamResponse = new Response('unconfigured') const fetchFn = vi.fn<typeof fetch>().mockResolvedValue(upstreamResponse)
const response = await handleFetch({ event, request, fetch: fetchFn })
expect(response).toBe(upstreamResponse) expect(response.headers.has('access-control-allow-origin')).toBe(false) })})
describe('hooks.server handle', () => { // vitest's `restoreMocks` detaches these after each test, so no test needs // its own restore — and a failing assertion can no longer leak spy calls // into the next test by skipping one. let warnSpy: ReturnType<typeof spyOnWarn>
beforeEach(() => { vi.clearAllMocks() mockPublicInternalInstance = 'http://localhost:4000' mockPublicInstanceUrl = undefined mockDev = false warnSpy = spyOnWarn() })
it('skips /api/me for the exact proxy route without authenticating locals', async () => { mockFetch.mockResolvedValue(Response.json({}, { status: 429 })) const event = createMockEvent({ url: 'http://localhost:5173/api/proxy/xrpc/social.coves.feed.get', routeId: '/api/proxy/[...path]', cookies: createMockCookies({ coves_session: 'sealed-token' }), }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(mockFetch).not.toHaveBeenCalled() expect(event.locals.auth).toEqual({ authenticated: false }) expect(event.cookies.delete).not.toHaveBeenCalled() expect(resolve).toHaveBeenCalledOnce() })
it('skips /api/me for the expire route, which reads the cookie itself', async () => { mockFetch.mockResolvedValue(Response.json({}, { status: 401 })) const event = createMockEvent({ url: 'http://localhost:5173/api/auth/expire', routeId: '/api/auth/expire', method: 'POST', cookies: createMockCookies({ coves_session: 'sealed-token' }), }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(mockFetch).not.toHaveBeenCalled() expect(event.locals.auth).toEqual({ authenticated: false }) expect(event.locals.sessionExpired).toBeUndefined() expect(event.cookies.delete).not.toHaveBeenCalled() expect(resolve).toHaveBeenCalledOnce() })
it('skips /api/me for site stats, which read no session', async () => { const event = createMockEvent({ url: 'http://localhost:5173/api/site-stats', routeId: '/api/site-stats', cookies: createMockCookies({ coves_session: 'sealed-token' }), }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(mockFetch).not.toHaveBeenCalled() expect(event.locals.auth).toEqual({ authenticated: false }) expect(resolve).toHaveBeenCalledOnce() })
it.each([ ['/api/proxy-extra', '/api/proxy-extra', false], ['/api/auth/logout', '/api/auth/logout', false], ['/api/proxy/status', '/api/proxy/status', false], ['/c/general', '/c/[handle]', false], ['/c/general/__data.json', '/c/[handle]', true], ])( 'still validates a session on %s', async (path, routeId, isDataRequest) => { mockFetch.mockResolvedValue(Response.json({}, { status: 401 })) const event = Object.assign( createMockEvent({ url: `http://localhost:5173${path}`, routeId, cookies: createMockCookies({ coves_session: 'sealed-token' }), }), { isDataRequest }, )
await handle({ event, resolve: createMockResolve() })
expect(mockFetch).toHaveBeenCalledOnce() expect(mockFetch).toHaveBeenCalledWith( 'http://localhost:4000/api/me', expect.any(Object), ) expect(event.cookies.delete).not.toHaveBeenCalled() expect(event.locals.sessionExpired).toBe(true) }, )
describe('no coves_session cookie', () => { it('results in unauthenticated state and no fetch called', async () => { const cookies = createMockCookies({}) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(mockFetch).not.toHaveBeenCalled() expect(event.locals.auth.authenticated).toBe(false) expect(event.locals.authError).toBeUndefined() expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) }) })
describe('empty string coves_session cookie', () => { it('treats empty string as no cookie and returns unauthenticated', async () => { const cookies = createMockCookies({ coves_session: '' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
// Empty string is falsy, so it's treated the same as no cookie expect(event.locals.auth.authenticated).toBe(false) expect(mockFetch).not.toHaveBeenCalled() expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) }) })
describe('valid cookie and /api/me returns 200', () => { it('populates authenticated state with correct account and authToken', async () => { mockFetch.mockResolvedValue( new Response( JSON.stringify({ did: 'did:plc:user1', handle: 'user1.example.com', avatar: 'https://example.com/avatar.png', }), { status: 200 }, ), )
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(mockFetch).toHaveBeenCalledWith( 'http://localhost:4000/api/me', expect.objectContaining({ signal: expect.any(AbortSignal) }), ) const upstreamHeaders = new Headers(mockFetch.mock.calls[0][1].headers) expect(upstreamHeaders.get('cookie')).toBe( 'coves_session=sealed-token-value', ) expect(event.locals.auth.authenticated).toBe(true) if (event.locals.auth.authenticated) { expect(event.locals.auth.account.did).toBe('did:plc:user1') expect(event.locals.auth.account.handle).toBe('user1.example.com') expect(event.locals.auth.account.instance).toBe('http://localhost:4000') expect(event.locals.auth.account.sealedToken).toBe('sealed-token-value') expect(event.locals.auth.account.avatar).toBe( 'https://example.com/avatar.png', ) expect(event.locals.auth.authToken).toBe('sealed-token-value') } expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) }) })
describe('/api/me carries the observed client address', () => { // The backend's global rate limiter keys on X-Real-IP. This hop runs once // per authenticated page view, so an unstamped request would put every // user on the site into this container's single bucket — and past the // limit, the 429 is treated as "unauthenticated" and everyone is silently // logged out. The stamp is the whole cutover's rate-limit story. const ok = () => new Response( JSON.stringify({ did: 'did:plc:user1', handle: 'user1.example.com' }), { status: 200 }, )
it('stamps X-Real-IP and X-Forwarded-For from getClientAddress()', async () => { mockFetch.mockResolvedValue(ok()) const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) event.getClientAddress = () => '203.0.113.7'
await handle({ event, resolve: createMockResolve() })
const upstreamHeaders = new Headers(mockFetch.mock.calls[0][1].headers) expect(upstreamHeaders.get('x-real-ip')).toBe('203.0.113.7') expect(upstreamHeaders.get('x-forwarded-for')).toBe('203.0.113.7') })
it('never forwards a client-supplied address header', async () => { mockFetch.mockResolvedValue(ok()) const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies, headers: { 'x-real-ip': '6.6.6.6', 'x-forwarded-for': '6.6.6.6', forwarded: 'for=6.6.6.6', }, }) event.getClientAddress = () => '203.0.113.7'
await handle({ event, resolve: createMockResolve() })
const upstreamHeaders = new Headers(mockFetch.mock.calls[0][1].headers) expect(upstreamHeaders.get('x-real-ip')).toBe('203.0.113.7') expect(upstreamHeaders.get('x-forwarded-for')).toBe('203.0.113.7') expect(upstreamHeaders.has('forwarded')).toBe(false) })
it('still validates the session, with no address stamp, when the address is unavailable', async () => { // adapter-node throws when ADDRESS_HEADER is configured but the header // is absent — a proxy misconfiguration. The session check must go // ahead without an address claim rather than fail the page or fall // back to anything the client sent. mockFetch.mockResolvedValue(ok()) const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies, headers: { 'x-real-ip': '6.6.6.6' }, }) event.getClientAddress = () => { throw new Error( 'Address header was specified with ADDRESS_HEADER=x-real-ip but is absent from request', ) }
await handle({ event, resolve: createMockResolve() })
expect(mockFetch).toHaveBeenCalledTimes(1) const upstreamHeaders = new Headers(mockFetch.mock.calls[0][1].headers) expect(upstreamHeaders.get('cookie')).toBe( 'coves_session=sealed-token-value', ) expect(upstreamHeaders.has('x-real-ip')).toBe(false) expect(upstreamHeaders.has('x-forwarded-for')).toBe(false) expect(event.locals.auth.authenticated).toBe(true) }) })
describe('session generation identifies the cookie, including expired sessions', () => { it('is stable for the same cookie, changes after same-DID re-login, and never exposes the cookie', async () => { const generations: unknown[] = [] for (const [cookie, status] of [ ['first-opaque-cookie', 200], ['first-opaque-cookie', 401], ['replacement-opaque-cookie', 200], ] as const) { mockFetch.mockResolvedValue( new Response( JSON.stringify({ did: 'did:plc:user1', handle: 'user1.example.com', }), { status }, ), ) const event = createMockEvent({ cookies: createMockCookies({ coves_session: cookie }), }) await handle({ event, resolve: createMockResolve() }) const locals: Record<string, unknown> = { ...event.locals } expect(locals.sessionGeneration).toEqual(expect.any(String)) expect(locals.sessionGeneration).not.toBe(cookie) generations.push(locals.sessionGeneration) } expect(generations[0]).toBe(generations[1]) expect(generations[0]).not.toBe(generations[2]) }) })
describe('valid cookie and /api/me returns 401', () => { it('results in unauthenticated state without console.warn', async () => { mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }))
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(false) // Should NOT log a warning for 401 (expected case) expect(warnSpy).not.toHaveBeenCalled() expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) })
it('does not let a passive 401 delete a cookie belonging to a later login', async () => { mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }))
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(cookies.delete).not.toHaveBeenCalled() })
it('sets sessionExpired flag on 401', async () => { mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }))
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.sessionExpired).toBe(true) })
it('does not set authError on 401 (session expiry is expected)', async () => { mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }))
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.authError).toBeUndefined() }) })
it('exposes a distinct rate-limit warning through page data while retaining the session cookie', async () => { const { load } = await import('./routes/+layout.server') mockFetch.mockResolvedValue( Response.json({ error: 'RateLimitExceeded' }, { status: 429 }), ) const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ url: 'http://localhost:5173/c/general/__data.json', routeId: '/c/[handle=handle]', cookies, }) event.isDataRequest = true
const response = await handle({ event, resolve: async (resolvedEvent) => Response.json( await load({ ...resolvedEvent, route: { id: '/c/[handle=handle]' }, parent: async () => ({}), depends: () => {}, untrack: (callback) => callback(), }), ), })
expect(mockFetch).toHaveBeenCalledOnce() expect(mockFetch).toHaveBeenCalledWith( 'http://localhost:4000/api/me', expect.any(Object), ) expect(event.locals.authError).toBe('rate_limited') expect(event.locals.auth).toEqual({ authenticated: false }) expect(event.locals.sessionExpired).toBeUndefined() expect(cookies.get('coves_session')).toBe('sealed-token-value') expect(cookies.delete).not.toHaveBeenCalled() expect(cookies.set).not.toHaveBeenCalled() expect(response.status).toBe(200) expect(response.headers.get('cache-control')).toBe('private, no-store') expect(await response.json()).toEqual({ lang: 'en', session: null, sessionExpired: false, sessionGeneration: expect.any(String), authError: 'rate_limited', }) })
describe('valid cookie and /api/me returns 500', () => { it('results in unauthenticated state and logs warning', async () => { mockFetch.mockResolvedValue( new Response('Internal Server Error', { status: 500 }), )
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(false) expect(event.locals.authError).toBeUndefined() const { line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('/api/me returned 500') expect(line.requestId).toBe(event.locals.requestId) // The upstream status belongs in a queryable field, not only in prose. expect(line.status).toBe(500) expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) }) })
describe('valid cookie and fetch throws network error', () => { it('sets authError to network_error for connection refused', async () => { mockFetch.mockRejectedValue( new Error('Network error: connection refused'), )
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(false) expect(event.locals.authError).toBe('network_error') const { line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('Network error calling /api/me') expect(line.requestId).toBe(event.locals.requestId) expect(errOf(line).name).toBe('Error') expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) })
it('sets authError to network_error for TypeError (fetch failure)', async () => { mockFetch.mockRejectedValue(new TypeError('fetch failed'))
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(false) expect(event.locals.authError).toBe('network_error') const { line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('Network error calling /api/me') expect(line.requestId).toBe(event.locals.requestId) expect(errOf(line).name).toBe('TypeError') expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) })
it('classifies TimeoutError and AbortError as network errors by name', async () => { for (const name of ['TimeoutError', 'AbortError']) { warnSpy.mockClear() mockFetch.mockRejectedValue(new DOMException('operation failed', name))
const cookies = createMockCookies({ coves_session: 'sealed-token-value', }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(false) expect(event.locals.authError).toBe('network_error') const { line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('Network error calling /api/me') expect(line.requestId).toBe(event.locals.requestId) // DOMException carries the discriminating name, so the log line must // preserve it — that is what distinguishes a timeout from an abort. expect(errOf(line).name).toBe(name) expect(cookies.delete).not.toHaveBeenCalled() } })
it('does not delete the coves_session cookie on network error', async () => { mockFetch.mockRejectedValue(new TypeError('fetch failed'))
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(cookies.delete).not.toHaveBeenCalled() })
it('sets authError to network_error for unexpected non-network errors', async () => { mockFetch.mockRejectedValue(new Error('some completely unexpected error'))
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(false) expect(event.locals.authError).toBe('network_error') const { line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('Unexpected error calling /api/me') expect(line.requestId).toBe(event.locals.requestId) expect(errOf(line).name).toBe('Error') expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) }) })
describe('valid cookie and /api/me returns invalid JSON', () => { it('sets authError to validation_error and logs warning', async () => { mockFetch.mockResolvedValue( new Response('not json', { status: 200, headers: { 'Content-Type': 'text/plain' }, }), )
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(false) expect(event.locals.authError).toBe('validation_error') // Invalid JSON triggers response.json() to throw as SyntaxError, // which is categorized as a validation error const { line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('/api/me returned invalid JSON') expect(line.requestId).toBe(event.locals.requestId) expect(errOf(line).name).toBe('SyntaxError') expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) }) })
describe('valid cookie and /api/me returns incomplete data', () => { it('sets authError to validation_error when did is missing', async () => { mockFetch.mockResolvedValue( new Response(JSON.stringify({ handle: 'user1.example.com' }), { status: 200, }), )
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(false) expect(event.locals.authError).toBe('validation_error') const { line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('/api/me response failed validation') expect(line.requestId).toBe(event.locals.requestId) expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) })
it('sets authError to validation_error when handle is missing', async () => { mockFetch.mockResolvedValue( new Response(JSON.stringify({ did: 'did:plc:user1' }), { status: 200 }), )
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(false) expect(event.locals.authError).toBe('validation_error') expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) }) })
describe('no instance URL configured', () => { it('throws a fatal error when instance URL is missing', async () => { mockPublicInternalInstance = undefined mockPublicInstanceUrl = undefined
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await expect(handle({ event, resolve })).rejects.toThrow( 'No instance URL configured', )
expect(mockFetch).not.toHaveBeenCalled() }) })
describe('authToken equals cookie value', () => { it('authToken is the coves_session cookie value', async () => { mockFetch.mockResolvedValue( new Response( JSON.stringify({ did: 'did:plc:user1', handle: 'user1.example.com', }), { status: 200 }, ), )
const cookieValue = 'my-specific-sealed-token-value' const cookies = createMockCookies({ coves_session: cookieValue }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(event.locals.auth.authenticated).toBe(true) if (event.locals.auth.authenticated) { expect(event.locals.auth.authToken).toBe(cookieValue) } }) })
describe('resolve function behavior', () => { it('always calls resolve with the event', async () => { const cookies = createMockCookies({}) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(resolve).toHaveBeenCalledTimes(1) expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) })
it('returns the resolve response', async () => { const expectedResponse = new Response('Test Response') const cookies = createMockCookies({}) const event = createMockEvent({ cookies }) const resolve = vi.fn().mockResolvedValue(expectedResponse)
const result = await handle({ event, resolve })
expect(result).toBe(expectedResponse) }) })
describe('instance URL fallback', () => { it('uses PUBLIC_INSTANCE_URL when PUBLIC_INTERNAL_INSTANCE is not set', async () => { mockPublicInternalInstance = undefined mockPublicInstanceUrl = 'https://coves.example.com'
mockFetch.mockResolvedValue( new Response( JSON.stringify({ did: 'did:plc:user1', handle: 'user1.example.com', }), { status: 200 }, ), )
const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(mockFetch).toHaveBeenCalledWith( 'https://coves.example.com/api/me', expect.objectContaining({ signal: expect.any(AbortSignal) }), ) expect( new Headers(mockFetch.mock.calls[0][1].headers).get('cookie'), ).toBe('coves_session=sealed-token-value') expect(event.locals.auth.authenticated).toBe(true) }) })
describe('dev mode hostname normalization', () => { it('redirects localhost to 127.0.0.1 when dev=true and PUBLIC_INSTANCE_URL uses 127.0.0.1', async () => { mockDev = true mockPublicInstanceUrl = 'http://127.0.0.1:8080'
const cookies = createMockCookies({}) const event = createMockEvent({ cookies, url: 'http://localhost:8080/some/page?q=test', }) const resolve = createMockResolve()
try { await handle({ event, resolve }) // Should have thrown a redirect expect.fail('Expected a redirect to be thrown') } catch (err) { expect(isRedirect(err)).toBe(true) if (isRedirect(err)) { expect(err.status).toBe(302) expect(err.location).toBe('http://127.0.0.1:8080/some/page?q=test') } } })
it('does not redirect when hostname already matches canonical host', async () => { mockDev = true mockPublicInstanceUrl = 'http://127.0.0.1:8080'
const cookies = createMockCookies({}) const event = createMockEvent({ cookies, url: 'http://127.0.0.1:8080/some/page', }) const resolve = createMockResolve()
await handle({ event, resolve })
// Should proceed normally without redirect expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) })
it('does not redirect when dev=false even if hostname mismatches', async () => { mockDev = false mockPublicInstanceUrl = 'http://127.0.0.1:8080'
const cookies = createMockCookies({}) const event = createMockEvent({ cookies, url: 'http://localhost:8080/', }) const resolve = createMockResolve()
await handle({ event, resolve })
// Should proceed normally without redirect expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) })
it('does not redirect when PUBLIC_INSTANCE_URL is not set', async () => { mockDev = true mockPublicInstanceUrl = undefined
const cookies = createMockCookies({}) const event = createMockEvent({ cookies, url: 'http://localhost:8080/', }) const resolve = createMockResolve()
await handle({ event, resolve })
expect(resolve).toHaveBeenCalledWith( event, expect.objectContaining({ transformPageChunk: expect.any(Function) }), ) }) })})
describe('hooks.server request id', () => { // crypto.randomUUID() emits a v4 UUID; anything else (including the // non-uuid default in createMockEvent) fails this. const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
beforeEach(() => { vi.clearAllMocks() mockPublicInternalInstance = 'http://localhost:4000' mockPublicInstanceUrl = undefined mockDev = false })
it('assigns a uuid request id to locals on every request', async () => { const event = createMockEvent({ cookies: createMockCookies() })
await handle({ event, resolve: createMockResolve() })
expect(typeof event.locals.requestId).toBe('string') expect(event.locals.requestId).toMatch(UUID_V4) })
it('sets the x-request-id response header to that same id', async () => { const event = createMockEvent({ cookies: createMockCookies() })
await handle({ event, resolve: createMockResolve() })
expect(event.setHeaders).toHaveBeenCalledWith({ 'x-request-id': event.locals.requestId, }) })
it('gives two requests different ids', async () => { const first = createMockEvent({ cookies: createMockCookies() }) const second = createMockEvent({ cookies: createMockCookies() })
await handle({ event: first, resolve: createMockResolve() }) await handle({ event: second, resolve: createMockResolve() })
expect(first.locals.requestId).toMatch(UUID_V4) expect(second.locals.requestId).toMatch(UUID_V4) expect(first.locals.requestId).not.toBe(second.locals.requestId) })
it("reuses the page's id on an in-process sub-request", async () => { const parentId = '0b6e2c1a-5d3f-4a8e-9c7b-2f1e0d9c8b7a' const event = Object.assign( createMockEvent({ cookies: createMockCookies(), headers: { 'x-request-id': parentId }, }), { isSubRequest: true }, )
await handle({ event, resolve: createMockResolve() })
expect(event.locals.requestId).toBe(parentId) })
it('never trusts an x-request-id sent by an external client', async () => { const claimed = '0b6e2c1a-5d3f-4a8e-9c7b-2f1e0d9c8b7a' const event = createMockEvent({ cookies: createMockCookies(), headers: { 'x-request-id': claimed }, })
await handle({ event, resolve: createMockResolve() })
expect(event.locals.requestId).toMatch(UUID_V4) expect(event.locals.requestId).not.toBe(claimed) })
it('mints a fresh id for a sub-request whose header is not a uuid', async () => { const event = Object.assign( createMockEvent({ cookies: createMockCookies(), headers: { 'x-request-id': 'not-a-uuid' }, }), { isSubRequest: true }, )
await handle({ event, resolve: createMockResolve() })
expect(event.locals.requestId).toMatch(UUID_V4) })
it('carries the request id on the production /util 404 early return', async () => { const event = createMockEvent({ cookies: createMockCookies(), url: 'http://localhost:5173/util/instance', })
const response = await handle({ event, resolve: createMockResolve() })
expect(response.status).toBe(404) expect(event.locals.requestId).toMatch(UUID_V4) expect(response.headers.get('x-request-id')).toBe(event.locals.requestId) })})
describe('hooks.server handleError', () => { beforeEach(() => { vi.clearAllMocks() mockDev = false mockLogStacks = undefined })
it('returns "Not found" for 404 errors', async () => { const result = await handleError({ error: new Error('Page not found'), event: createMockEvent({ cookies: createMockCookies() }), status: 404, message: 'Not Found', })
expect(result).toEqual({ message: 'Not found' }) })
it('marks backend connection failures for the recoverable error page', async () => { const result = await handleError({ error: new TypeError('fetch failed'), event: createMockEvent({ cookies: createMockCookies() }), status: 500, message: 'Internal Server Error', })
expect(result).toEqual({ message: 'The server is temporarily unreachable. Please try again.', code: 'BackendUnavailable', }) })
it('returns generic error message for non-404 errors', async () => { const result = await handleError({ error: new Error( 'Internal database connection failed with password xyz123', ), event: createMockEvent({ cookies: createMockCookies() }), status: 500, message: 'Internal Server Error', })
expect(result).toEqual({ message: 'An unexpected error occurred' }) })
it('does not expose internal error details in response', async () => { const sensitiveError = new Error( 'Database password: secret123, API key: abc-def-ghi', ) const result = await handleError({ error: sensitiveError, event: createMockEvent({ cookies: createMockCookies() }), status: 500, message: 'Internal Server Error', })
const appError = result as App.Error expect(appError.message).not.toContain('secret123') expect(appError.message).not.toContain('abc-def-ghi') expect(appError.message).not.toContain('password') expect(appError.message).toBe('An unexpected error occurred') })
it('handles errors without message property', async () => { const result = await handleError({ error: 'String error without message property', event: createMockEvent({ cookies: createMockCookies() }), status: 500, message: 'Internal Server Error', })
expect(result).toEqual({ message: 'An unexpected error occurred' }) })
it('returns generic message for 400 errors', async () => { const result = await handleError({ error: new Error('Bad request: invalid JSON'), event: createMockEvent({ cookies: createMockCookies() }), status: 400, message: 'Bad Request', })
expect(result).toEqual({ message: 'An unexpected error occurred' }) })
it('returns generic message for 403 errors', async () => { const result = await handleError({ error: new Error('User not authorized for resource /admin/secrets'), event: createMockEvent({ cookies: createMockCookies() }), status: 403, message: 'Forbidden', })
expect(result).toEqual({ message: 'An unexpected error occurred' }) })
describe('log sanitization', () => { const sealedToken = 'sealed-session-token-v1.super-secret-value'
let errorSpy: ReturnType<typeof spyOnError>
beforeEach(() => { errorSpy = spyOnError() })
/** * Creates an event carrying the sealed session token in both places it * lives on a real authenticated request: the Cookie header and locals.auth. */ function createEventWithToken(): RequestEvent { const cookies = createMockCookies({ coves_session: sealedToken }) const event = createMockEvent({ cookies, locals: { auth: { authenticated: true, account: { did: 'did:plc:test123', handle: 'test.example.com', pdsUrl: 'https://pds.example.com', sealedToken, }, authToken: sealedToken, }, requestId: 'req-sanitize-1', } as unknown as App.Locals, }) // Replace the bare request with one that includes the session cookie // header, as the real server would receive it. Object.assign(event, { request: new Request(event.url, { headers: { Cookie: `coves_session=${sealedToken}` }, }), }) return event }
/** Best-effort string form of a logged argument for content assertions. */ function stringifyLoggedArg(arg: unknown): string { if (typeof arg === 'string') return arg try { return JSON.stringify(arg) ?? String(arg) } catch { return String(arg) } }
it('never logs the event object or the session token for non-404 errors', async () => { const event = createEventWithToken() await handleError({ error: new Error('Internal database connection failed'), event, status: 500, message: 'Internal Server Error', })
expect(errorSpy).toHaveBeenCalled() for (const call of errorSpy.mock.calls) { for (const arg of call) { expect(arg).not.toBe(event) expect(stringifyLoggedArg(arg)).not.toContain(sealedToken) } } })
it('does not call console.error for 404 errors', async () => { await handleError({ error: new Error('Page not found'), event: createEventWithToken(), status: 404, message: 'Not Found', })
expect(errorSpy).not.toHaveBeenCalled() })
it('logs the error stack for diagnostics in dev', async () => { mockDev = true
const error = new Error('boom') await handleError({ error, event: createEventWithToken(), status: 500, message: 'Internal Server Error', })
// The stack now rides inside the single structured line rather than in // a second, raw console.error call. expect(error.stack).toBeDefined() const { raw, line } = singleJsonLine(errorSpy.mock.calls) const err = errOf(line) expect(typeof err.stack).toBe('string') expect(err.stack).toContain('Error: boom') expect(raw).not.toContain(sealedToken) }) }) describe('structured logging', () => { let errorSpy: ReturnType<typeof spyOnError> let warnSpy: ReturnType<typeof spyOnWarn>
beforeEach(() => { errorSpy = spyOnError() warnSpy = spyOnWarn() })
it('writes exactly one JSON line carrying the request context', async () => { const event = createMockEvent({ cookies: createMockCookies(), url: 'http://localhost:5173/c/gardening', })
await handleError({ error: new Error('upstream refused password=hunter2'), event, status: 500, message: 'Internal Server Error', })
expect(warnSpy).not.toHaveBeenCalled() const { raw, line } = singleJsonLine(errorSpy.mock.calls) expect(line).toMatchObject({ level: 'error', status: 500, method: 'GET', path: '/c/gardening', requestId: event.locals.requestId, }) expect(line.msg).toContain('Internal Server Error') expect(errOf(line)).toMatchObject({ name: 'Error', message: 'upstream refused password=[REDACTED]', }) expect(raw).not.toContain('hunter2') })
it('includes the stack in production by default', async () => { await handleError({ error: new Error('boom'), event: createMockEvent({ cookies: createMockCookies() }), status: 500, message: 'Internal Server Error', })
const { line } = singleJsonLine(errorSpy.mock.calls) expect('stack' in errOf(line)).toBe(true) expect(typeof errOf(line).stack).toBe('string') })
it("omits the stack in production when LOG_STACKS is '0'", async () => { mockLogStacks = '0'
await handleError({ error: new Error('boom'), event: createMockEvent({ cookies: createMockCookies() }), status: 500, message: 'Internal Server Error', })
const { line } = singleJsonLine(errorSpy.mock.calls) expect('stack' in errOf(line)).toBe(false) })
it('omits the requestId key entirely when locals carries none', async () => { // handleError can fire before hooks' handle() ran (or outside it), so an // absent id must be an absent key — never an empty string, which would // look like a real correlation id to a log search. await handleError({ error: new Error('boom'), event: createMockEvent({ cookies: createMockCookies(), locals: {} as unknown as App.Locals, }), status: 500, message: 'Internal Server Error', })
const { line } = singleJsonLine(errorSpy.mock.calls) expect('requestId' in line).toBe(false) })
it('includes the stack in dev', async () => { mockDev = true
await handleError({ error: new Error('boom'), event: createMockEvent({ cookies: createMockCookies() }), status: 500, message: 'Internal Server Error', })
const { line } = singleJsonLine(errorSpy.mock.calls) expect('stack' in errOf(line)).toBe(true) expect(typeof errOf(line).stack).toBe('string') })
it('writes nothing at all for a 404', async () => { await handleError({ error: new Error('Page not found'), event: createMockEvent({ cookies: createMockCookies() }), status: 404, message: 'Not Found', })
expect(errorSpy).not.toHaveBeenCalled() expect(warnSpy).not.toHaveBeenCalled() }) })
it('emits one scrubbed JSON log line with request context and leaks no secrets', async () => { const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
try { const secrets = ['hunter2', 'abc.def.ghi', 'SEALED123'] as const const error = new Error( 'db connect failed: password=hunter2 authorization=Bearer abc.def.ghi coves_session=SEALED123', ) // Overwrite the stack so it definitely carries the secrets too. error.stack = [ 'Error: db connect failed: password=hunter2 authorization=Bearer abc.def.ghi coves_session=SEALED123', ' at connect (src/lib/server/db.ts:10:5) password=hunter2', ' at handler (src/routes/+page.server.ts:3:1) Bearer abc.def.ghi coves_session=SEALED123', ].join('\n')
const event = createMockEvent({ cookies: createMockCookies(), locals: { auth: { authenticated: false }, requestId: 'req-outer-1', } as unknown as App.Locals, })
const result = await handleError({ error, event, status: 500, message: 'Internal Server Error', })
expect(result).toEqual({ message: 'An unexpected error occurred' })
// Nothing written to either console channel may contain a secret. const logged = [...errorSpy.mock.calls, ...warnSpy.mock.calls] .flat() .map((arg) => { if (typeof arg === 'string') return arg try { return JSON.stringify(arg) ?? String(arg) } catch { return String(arg) } }) for (const written of logged) { for (const secret of secrets) { expect(written).not.toContain(secret) } }
// Exactly one structured line, carrying the request context. expect(errorSpy).toHaveBeenCalledTimes(1) const call = errorSpy.mock.calls[0] expect(call).toHaveLength(1) expect(typeof call[0]).toBe('string') const line: unknown = JSON.parse(call[0] as string) expect(line).toMatchObject({ level: 'error', status: 500, method: 'GET', path: event.url.pathname, requestId: 'req-outer-1', }) } finally { errorSpy.mockRestore() warnSpy.mockRestore() } })})
describe('hooks.server /api/me failure logging', () => { // Carried inside each thrown error's message, to prove err.message is scrubbed. const SECRET_MESSAGE = 'upstream said Bearer SECRETTOK' const SCRUBBED_MESSAGE = 'upstream said Bearer [REDACTED]'
let warnSpy: ReturnType<typeof spyOnWarn>
beforeEach(() => { vi.clearAllMocks() mockPublicInternalInstance = 'http://localhost:4000' mockPublicInstanceUrl = undefined mockDev = false mockLogStacks = undefined warnSpy = spyOnWarn() })
it('logs a network failure as one scrubbed JSON line', async () => { mockFetch.mockRejectedValue( new TypeError(`fetch failed - ${SECRET_MESSAGE}`), ) const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies })
await handle({ event, resolve: createMockResolve() })
const { raw, line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('Network error calling /api/me') expect(line.requestId).toBe(event.locals.requestId) expect(errOf(line)).toMatchObject({ name: 'TypeError', message: `fetch failed - ${SCRUBBED_MESSAGE}`, }) expect(raw).not.toContain('SECRETTOK') expectOnlyStringArgs(warnSpy.mock.calls) })
it('logs a JSON parse failure as one scrubbed JSON line', async () => { // handle() only touches ok/status/json(), so this minimal stand-in lets // the SyntaxError carry a secret of our choosing. mockFetch.mockResolvedValue({ ok: true, status: 200, json: () => Promise.reject(new SyntaxError(`Unexpected token - ${SECRET_MESSAGE}`)), } as unknown as Response) const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies })
await handle({ event, resolve: createMockResolve() })
const { raw, line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('/api/me returned invalid JSON') expect(line.requestId).toBe(event.locals.requestId) expect(errOf(line)).toMatchObject({ name: 'SyntaxError', message: `Unexpected token - ${SCRUBBED_MESSAGE}`, }) expect(raw).not.toContain('SECRETTOK') expectOnlyStringArgs(warnSpy.mock.calls) })
it('logs an unexpected failure as one scrubbed JSON line', async () => { mockFetch.mockRejectedValue(new Error(`kaboom - ${SECRET_MESSAGE}`)) const cookies = createMockCookies({ coves_session: 'sealed-token-value' }) const event = createMockEvent({ cookies })
await handle({ event, resolve: createMockResolve() })
const { raw, line } = singleJsonLine(warnSpy.mock.calls) expect(line.level).toBe('warn') expect(line.msg).toContain('Unexpected error calling /api/me') expect(line.requestId).toBe(event.locals.requestId) expect(errOf(line)).toMatchObject({ name: 'Error', message: `kaboom - ${SCRUBBED_MESSAGE}`, }) expect(raw).not.toContain('SECRETTOK') expectOnlyStringArgs(warnSpy.mock.calls) })})
describe('hooks.server security headers', () => { beforeEach(() => { vi.clearAllMocks() mockPublicInternalInstance = 'http://localhost:4000' mockPublicInstanceUrl = 'https://coves.social' mockDev = false })
function csp(response: Response): Record<string, string> { return Object.fromEntries( (response.headers.get('content-security-policy') ?? '') .split(';') .map((d) => d.trim()) .filter(Boolean) .map((d) => { const [name, ...rest] = d.split(/\s+/) return [name, rest.join(' ')] }), ) }
it('hardens every response resolve() produces', async () => { const event = createMockEvent({ url: 'https://coves.social/api/whatever' }) const resolve = vi .fn() .mockResolvedValue( new Response('{}', { headers: { 'content-type': 'application/json' } }), )
const response = await handle({ event, resolve })
expect(response.headers.get('x-content-type-options')).toBe('nosniff') expect(response.headers.get('referrer-policy')).toBe( 'strict-origin-when-cross-origin', ) expect(response.headers.get('x-frame-options')).toBe('DENY') expect(response.headers.get('permissions-policy')).toContain('camera=()') // Not a document: CSP is left to the resource itself. expect(response.headers.get('content-security-policy')).toBeNull() })
it("completes Kit's nonce'd CSP on a page Kit rendered", async () => { const event = createMockEvent({ url: 'https://coves.social/' }) const resolve = kitPageResolve()
const response = await handle({ event, resolve }) const policy = csp(response)
expect(policy['script-src']).toBe("'self' 'nonce-kit123'") expect(policy['default-src']).toBe("'self'") expect(policy['img-src']).toBe( "'self' data: blob: https: https://coves.social", ) expect(policy['media-src']).toBe( "'self' blob: https://coves.social https://pds.coves.me", ) expect(policy['connect-src']).toBe("'self' https://coves.social") expect(policy['frame-ancestors']).toBe("'none'") expect(policy['upgrade-insecure-requests']).toBe('') expect(await response.text()).toBe('<html></html>') })
it("stamps the default font's class so the first paint is already in it", async () => { const event = createMockEvent({ url: 'https://coves.social/' }) const resolve = kitPageResolve('<html class="dark %coves.font%"></html>')
const response = await handle({ event, resolve })
// PUBLIC_FONT is unset in this suite, so the default is Inter. expect(await response.text()).toBe('<html class="dark font-inter"></html>') })
it('preserves a transformPageChunk the inner handle asked for', async () => { const event = createMockEvent({ url: 'https://coves.social/' }) const resolve = kitPageResolve('<p>x</p>') // The wrapper must chain, not replace, a caller-supplied transform. await handle({ event, resolve }) const opts = resolve.mock.calls[0][1] expect(await opts?.transformPageChunk?.({ html: '<b>', done: true })).toBe( '<b>', ) })
it('denies all on HTML an endpoint or the proxy produced, whatever CSP it carries', async () => { const event = createMockEvent({ url: 'https://coves.social/api/proxy/x' }) // Upstream HTML relayed with a permissive policy and even a forged // x-sveltekit-page header — transformPageChunk never fires, so it is not // a Kit page. const resolve = vi.fn().mockResolvedValue( new Response('<script>alert(1)</script>', { headers: { 'content-type': 'text/html', 'content-security-policy': "script-src 'self' 'unsafe-inline'", 'x-sveltekit-page': 'true', }, }), )
const response = await handle({ event, resolve }) const policy = csp(response)
expect(policy['default-src']).toBe("'none'") expect(policy['script-src']).toBeUndefined() expect(response.headers.get('content-security-policy')).not.toContain( 'unsafe-inline', ) })
it('omits upgrade-insecure-requests and opens HMR sources in plaintext dev', async () => { mockDev = true mockPublicInstanceUrl = 'http://127.0.0.1:8080' const event = createMockEvent({ url: 'http://127.0.0.1:8080/' }) const resolve = kitPageResolve()
const policy = csp(await handle({ event, resolve }))
expect(policy['upgrade-insecure-requests']).toBeUndefined() expect(policy['style-src-elem']).toContain("'unsafe-inline'") expect(policy['connect-src']).toContain('ws:') })
it('hardens the production /util 404 that bypasses resolve()', async () => { const event = createMockEvent({ url: 'https://coves.social/util/instance', }) const resolve = createMockResolve()
const response = await handle({ event, resolve })
expect(response.status).toBe(404) expect(resolve).not.toHaveBeenCalled() expect(response.headers.get('x-content-type-options')).toBe('nosniff') expect(response.headers.get('x-request-id')).toBeTruthy() })
it('still lands headers on a response whose Headers are immutable', async () => { const event = createMockEvent({ url: 'https://coves.social/x' }) const resolve = vi .fn() .mockResolvedValue(Response.redirect('https://coves.social/y', 302))
const response = await handle({ event, resolve })
expect(response.status).toBe(302) expect(response.headers.get('location')).toBe('https://coves.social/y') expect(response.headers.get('x-content-type-options')).toBe('nosniff') })
it('re-wraps an immutable HTML response without losing status, cookies or the nonce', async () => { const event = createMockEvent({ url: 'https://coves.social/' }) const resolve = kitPageResolve('<html></html>', { 'set-cookie': 'a=1', }) // First set() on the real response throws like an immutable guard would. const spy = vi .spyOn(Headers.prototype, 'set') .mockImplementationOnce(() => { throw new TypeError('immutable') })
const response = await handle({ event, resolve }) spy.mockRestore()
expect(response.headers.get('set-cookie')).toBe('a=1') expect(csp(response)['script-src']).toBe("'self' 'nonce-kit123'") expect(response.headers.get('x-content-type-options')).toBe('nosniff') })
it('lets a TypeError that is not the immutable guard propagate', async () => { const event = createMockEvent({ url: 'https://coves.social/' }) const resolve = kitPageResolve() const spy = vi.spyOn(Headers.prototype, 'set').mockImplementation(() => { throw new TypeError('bad header value') })
await expect(handle({ event, resolve })).rejects.toThrow(/bad header value/) spy.mockRestore() })
it('refuses to load at all with a malformed CSP_VIDEO_ORIGINS', async () => { mockCspVideoOrigins = 'javascript:alert(1)' vi.resetModules() await expect(import('./hooks.server')).rejects.toThrow(/javascript/) mockCspVideoOrigins = 'https://pds.coves.me' vi.resetModules() })})
describe('request event accessor', () => { // Self-contained: this suite builds its own module graph rather than reading // the one the file imported at the top. Sharing that graph made the suite // depend on running before the vi.resetModules() calls elsewhere in this // file, which is a property of source order and nothing else. let currentRequestEvent: () => RequestEvent | undefined
beforeEach(async () => { vi.clearAllMocks() vi.resetModules() // Order matters: load the accessor module first, then hooks.server, so the // install lands in the same fresh copy this suite is about to read. ;({ currentRequestEvent } = await import('$lib/app/util/request-event')) await import('./hooks.server') })
it('exposes the SvelteKit request event through currentRequestEvent', async () => { const event = createMockEvent({ url: 'https://coves.social/feed' }) mockGetRequestEvent.mockReturnValue(event)
expect(currentRequestEvent()).toBe(event) expect(mockGetRequestEvent).toHaveBeenCalled() })
it('returns undefined instead of throwing when called outside a request', async () => { mockGetRequestEvent.mockImplementation(() => { throw new Error('Cannot be called outside a request handler') })
expect(() => currentRequestEvent()).not.toThrow() expect(currentRequestEvent()).toBeUndefined() // Without this the assertion above passes vacuously: an uninstalled // accessor also returns undefined, having never reached getRequestEvent. expect(mockGetRequestEvent).toHaveBeenCalled() })})
/** * Kit pages and their `__data.json` twins embed the signed-in user's session * (handle, avatar, DID) in the payload, so an HTTP cache that stores and * replays them — a proxy, the browser's disk cache — hands one user's session * to the next person on the device. The response has to say so itself rather * than leaving every cache to guess right. This header does not reach the * service worker: the Cache API ignores Cache-Control entirely, which is why * the SW is closed separately in service-worker.ts. */describe('hooks.server cache policy', () => { beforeEach(() => { vi.clearAllMocks() mockPublicInternalInstance = 'http://localhost:4000' mockPublicInstanceUrl = 'https://coves.social' mockDev = false })
it('marks a rendered Kit page private, no-store', async () => { const event = createMockEvent({ url: 'https://coves.social/' })
const response = await handle({ event, resolve: kitPageResolve() })
expect(response.headers.get('cache-control')).toBe('private, no-store') })
it('marks a data request private, no-store', async () => { // Client-side navigation fetches `__data.json` instead of a document, so // no page chunk is ever transformed — this response is not a Kit page and // has to be recognised by `isDataRequest` alone. const event = Object.assign( createMockEvent({ url: 'https://coves.social/feed/__data.json' }), { isDataRequest: true }, )
const response = await handle({ event, resolve: createMockResolve() })
expect(response.headers.get('cache-control')).toBe('private, no-store') })
it('leaves an endpoint response its own cache-control', async () => { // The proxy relays unauthenticated upstream responses and picks their // freshness deliberately. Neither a page nor a data request, so the // session-privacy rule does not apply and must not clobber it. const event = createMockEvent({ url: 'https://coves.social/api/proxy/x' }) const resolve = vi.fn().mockResolvedValue( new Response('OK', { headers: { 'cache-control': 'public, max-age=60' }, }), )
const response = await handle({ event, resolve })
expect(response.headers.get('cache-control')).toBe('public, max-age=60') })
it('marks a data-request redirect private, no-store despite immutable headers', async () => { // Kit itself answers a data-request `redirect()` with a 200 JSON body // whose headers are mutable. Response.redirect() stands in here because it // carries the same immutable Headers guard a relayed fetch() Response // does, and the re-wrap path that guard forces must still land the header // rather than throwing. const event = Object.assign( createMockEvent({ url: 'https://coves.social/settings/__data.json' }), { isDataRequest: true }, ) const resolve = vi .fn() .mockResolvedValue(Response.redirect('https://coves.social/', 303))
const response = await handle({ event, resolve })
expect(response.status).toBe(303) expect(response.headers.get('cache-control')).toBe('private, no-store') })
it('marks a header-less redirect private, no-store', async () => { // A page-level `redirect()`: no page chunk is transformed and it is not a // data request, so neither signal fires — but the response still came out // of a route that may have branched on who is signed in, and it names no // policy of its own. Immutable headers, so this also exercises the // re-wrap. const event = createMockEvent({ url: 'https://coves.social/settings' }) const resolve = vi .fn() .mockResolvedValue(Response.redirect('https://coves.social/login', 302))
const response = await handle({ event, resolve })
expect(response.status).toBe(302) expect(response.headers.get('cache-control')).toBe('private, no-store') })
it('marks a header-less response private, no-store', async () => { // Silence is not permission to cache: only a response that names its own // policy (see the endpoint test above) keeps one. const event = createMockEvent({ url: 'https://coves.social/api/thing' })
const response = await handle({ event, resolve: createMockResolve() })
expect(response.headers.get('cache-control')).toBe('private, no-store') })
it('overrides a public cache-control on a Kit page', async () => { // A route that set its own freshness cannot know a session was serialised // into the payload underneath it, so the page rule wins. const event = createMockEvent({ url: 'https://coves.social/' }) const resolve = kitPageResolve('<html></html>', { 'cache-control': 'public, max-age=3600', })
const response = await handle({ event, resolve })
expect(response.headers.get('cache-control')).toBe('private, no-store') })
it('overrides a public cache-control on a data request', async () => { const event = Object.assign( createMockEvent({ url: 'https://coves.social/feed/__data.json' }), { isDataRequest: true }, ) const resolve = vi.fn().mockResolvedValue( new Response('{}', { headers: { 'cache-control': 'public, max-age=3600' }, }), )
const response = await handle({ event, resolve })
expect(response.headers.get('cache-control')).toBe('private, no-store') })})
describe('production origin startup gate', () => { it('refuses to initialize production hooks without ORIGIN', async () => { vi.stubEnv('PROD', true) mockOrigin = undefined vi.resetModules() try { await expect(import('./hooks.server')).rejects.toThrow( /ORIGIN is not set/, ) } finally { vi.unstubAllEnvs() vi.resetModules() } })
it('allows production builds without deployment environment variables', async () => { vi.stubEnv('PROD', true) mockBuilding = true mockOrigin = undefined vi.resetModules() try { await expect(import('./hooks.server')).resolves.toHaveProperty('handle') } finally { mockBuilding = false vi.unstubAllEnvs() vi.resetModules() } })
it('allows configured production and unconfigured development startup', async () => { try { vi.stubEnv('PROD', true) mockOrigin = 'https://coves.test' vi.resetModules() await expect(import('./hooks.server')).resolves.toHaveProperty('handle') vi.stubEnv('PROD', false) mockOrigin = undefined vi.resetModules() await expect(import('./hooks.server')).resolves.toHaveProperty('handle') } finally { mockOrigin = undefined vi.unstubAllEnvs() vi.resetModules() } })})