refactor: remove legacy admin and harden self-hosting
56 unmodified lines5758596061616263643 unmodified lines56 unmodified linesOAUTH_HANDLE_RESOLVER_URL=http://localhost:3300/xrpc/com.atproto.identity.resolveHandleOAUTH_PLC_DIRECTORY_URL=http://127.0.0.1:2582OAUTH_ALLOW_HTTP=true
SKIP_ADMIN_SETUP=trueDISABLE_DNS_WORKER=true
# Host port overrides for the dev infrastructure. Change these if another local# service already binds the defaults; docker-compose.dev.yml reads them directly.3 unmodified lines118 unmodified linesExpand all11912012112212312312412512635 unmodified lines118 unmodified linesExpand all
- OAuth authentication and session management- Site CRUD operations via PDS- Custom domain management- Admin database view in /admin- React frontend in public/
**`apps/hosting-service`** - CDN static file server (Bun + Hono)- Serves sites at `https://sites.wisp.place/{did}/{site-name}` and custom domains35 unmodified lines7 unmodified lines8910111213141516171213141533 unmodified lines7 unmodified lines "files": { "ignoreUnknown": true, "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "!packages/@wispplace/lexicons"] }, "overrides": [ { "includes": ["**/public/admin/admin.tsx"], "linter": { "enabled": false } } ], "formatter": { "enabled": true, "indentStyle": "tab", "lineWidth": 12033 unmodified lines71 unmodified lines727374757676777879133 unmodified linesExpand all71 unmodified lines PORT: "8000" DOMAIN: http://127.0.0.1:8000 DATABASE_URL: postgres://postgres:postgres@postgres:5432/wisp REDIS_URL: redis://redis:6379 SKIP_ADMIN_SETUP: "true" DISABLE_DNS_WORKER: "true" BASE_DOMAIN: wisp.place PRIVATE_HOST: priv.wisp.localhost:3001 S3_BUCKET: wisp-e2e133 unmodified linesExpand all284 unmodified linesExpand all285286287288289290291292293294295296297298299300301302491 unmodified linesExpand all284 unmodified linesExpand all .description('Serve a site locally with live updates from firehose') .requiredOption('-s, --site <name>', 'Site name to serve') .option('-p, --path <path>', 'Local directory to cache site', '.wisp-serve') .option('-P, --port <port>', 'Port to serve on', '8080') .option('--host <host>', 'Bind address (defaults to loopback; use 0.0.0.0 for public access)', '127.0.0.1') .option('--spa [file]', 'Enable SPA mode (serve file for all unmatched routes, defaults to index.html)') .option('--directory-listing', 'Enable directory listing') .action( withExit(async (handle: string, options) => { await serve(handle, { site: options.site, path: options.path, port: parseInt(options.port, 10), host: options.host, spa: options.spa, directoryListing: options.directoryListing, }) }),491 unmodified linesExpand all17 unmodified lines18192021222324222324258 unmodified lines343536374142434445383940413 unmodified lines454647485758594950515222 unmodified lines75767778909192939479808182838416 unmodified lines17 unmodified lines - curl - jq
environment: # The mirror the release builds actually clone from. Not a secret — it is # this repo's own public remote — but it is the one thing the release has # to be told, so it lives here rather than inline in two commands. SOURCE_REPO: WaveringAna/wisp.place-monorepo
steps: - name: validate tag8 unmodified lines echo "suffix like -rc1 has nowhere to go." >&2 exit 1 fi
# NOT $TANGLED_SHA. On a tag push that is the sha of the *ref*, and for # an annotated tag (`git tag -a`) the ref is a tag object, not a # commit — the mirror check then 422s and a build pinned to it would # find nothing. The clone is of the tag, so HEAD is the commit it # points at, for both annotated and lightweight tags. commit="$(git rev-parse 'HEAD^{commit}')"
printf '%s' "$version" > /tmp/wisp-version printf '%s' "$commit" > /tmp/wisp-commit3 unmodified lines command: | export PATH="$HOME/.nix-profile/bin:$PATH" set -euo pipefail
# The builds clone $SOURCE_REPO, not the knot. A tag pushed only to # origin would build whatever is on the mirror's default branch # instead — same version number, different tree. sha="$(cat /tmp/wisp-commit)" code="$(curl -s -o /dev/null -w '%{http_code}' \ "https://api.github.com/repos/$SOURCE_REPO/commits/$sha")" if [ "$code" != "200" ]; then22 unmodified lines -H "X-Api-Secret: $KOMODO_API_SECRET" \ -d "$2" }
# RunAction is ASYNCHRONOUS. It returns in well under a second with # `success: true, status: InProgress` — that field is the initial value # of a record the release has not started writing yet, not a result. # Gating on it would make every tag go green no matter what happened. # The only honest signal is the Update, polled until it completes. # RunAction is ASYNCHRONOUS. # The only signal is the Update, polled until it completes. started="$(komodo execute/RunAction "$payload")" id="$(echo "$started" | jq -r '.id // ._id."$oid" // empty')" [ -n "$id" ] || { echo "no update id in RunAction response" >&2; exit 1; } echo "release started, update $id"16 unmodified lines111 unmodified linesExpand all112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199116117118119204120121122123124125210211212213214126127128111 unmodified linesExpand all`
await Bun.write(`${distDir}/editor/index.html`, htmlContent)
// Create admin directoryawait mkdir(`${distDir}/admin`, { recursive: true })
// Build the admin React appconst adminResult = await Bun.build({ entrypoints: [`${publicDir}/admin/admin.tsx`], outdir: `${distDir}/admin`, target: 'browser', format: 'esm', minify: true, sourcemap: 'none', splitting: true, naming: { entry: '[name].[hash].js', chunk: '[name].[hash].js', asset: '[name].[hash][ext]', },})
if (!adminResult.success) { console.error('❌ Admin build failed:') for (const log of adminResult.logs) { console.error(log) } process.exit(1)}
// Find the main entry bundle for adminconst adminBundle = adminResult.outputs.find((o) => o.path.includes('admin.') && o.path.endsWith('.js'))
if (!adminBundle) { console.error('❌ Could not find admin bundle in outputs') process.exit(1)}
const adminBundleName = path.basename(adminBundle.path)
// Generate the production HTML for adminconst adminHtmlContent = `<!doctype html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Admin - wisp.place</title> <meta name="description" content="Admin dashboard for wisp.place decentralized static site hosting." /> <meta name="robots" content="noindex, nofollow" />
<!-- Theme --> <meta name="theme-color" content="#7c3aed" />
<link rel="icon" type="image/x-icon" href="/favicon.ico"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="manifest" href="/site.webmanifest"> <link rel="stylesheet" href="/dist/styles.css"> <style> /* Dark theme fallback styles for before JS loads */ @media (prefers-color-scheme: dark) { body { background-color: oklch(0.23 0.015 285); color: oklch(0.90 0.005 285); }
pre { background-color: oklch(0.33 0.015 285) !important; color: oklch(0.90 0.005 285) !important; }
.bg-muted { background-color: oklch(0.33 0.015 285) !important; } } </style> </head> <body> <div id="root"></div> <script type="module" src="/admin/${adminBundleName}"></script> </body></html>`
await Bun.write(`${distDir}/admin/index.html`, adminHtmlContent)
// Write wisp.css to public directory so it can be served by static pluginawait Bun.write(`${publicDir}/wisp.css`, wispCss)
console.log('✅ Build successful!')console.log(`📦 Generated ${editorResult.outputs.length + adminResult.outputs.length + 4} file(s):`)console.log(`📦 Generated ${editorResult.outputs.length + 2} file(s):`)console.log(`\n Editor:`)console.log(` - ${distDir}/editor/index.html`)for (const output of editorResult.outputs) { console.log(` - ${output.path}`)}console.log(`\n Admin:`)console.log(` - ${distDir}/admin/index.html`)for (const output of adminResult.outputs) { console.log(` - ${output.path}`)}console.log(`\n CSS:`)console.log(` - ${publicDir}/wisp.css`)console.log(` - ${distDir}/wisp.css`)1 unmodified line2345667891011121314151658 unmodified lines757677787980818283848586878889909192939495968 unmodified lines1 unmodified lineimport { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs'import { tmpdir } from 'node:os'import { join } from 'node:path'import { gzipSync } from 'node:zlib'import { decompressPulledGzip, readPulledBlob, resolvePullFilePath } from './pull'import { createLogicalSiteBudget, decompressPulledGzip, readPulledBlob, resolvePullFilePath, validatePulledGzipHeader,} from './pull'
const temporaryPaths: string[] = []
function makeTempDirectory(): string {58 unmodified lines expect(await decompressPulledGzip(gzipSync(original), original.byteLength)).toEqual(original) })})
describe('pull safety budgets', () => { it('rejects gzip metadata without a gzip header', () => { expect(() => validatePulledGzipHeader(Buffer.from('plain text'))).toThrow('not a gzip stream') })
it('enforces the inclusive aggregate logical site limit', () => { const budget = createLogicalSiteBudget(10) budget.reserve(6) budget.reserve(4) expect(budget.totalSize).toBe(10) expect(() => budget.reserve(1)).toThrow('logical size limit') })})
describe('readPulledBlob', () => { it('cancels an oversized blob response before retaining its full body', async () => { await expect(readPulledBlob(new Response(Buffer.alloc(1025)), 1024)).rejects.toThrow( 'Downloaded blob exceeds the 1024-byte limit',8 unmodified lines123456678910111213141 unmodified line1617181920212223242325262728293031323331343536373839404134 unmodified lines76777879808182838485868788899091929394959697989910010110210310410510610783 unmodified lines19119219319419519619719819920020120216516616716816917020320420520620720820921021121221321421533 unmodified lines24925025125221225325425521525625725825926015 unmodified lines27627727827923924024124224324424524624724824925028028128228328428528628728828929029129229329425629529629729829930030130230330430530630730830931031131231331431525931631726131831932032126326426532232332432532626732732826927027127227332933033133233333427527627727827933533633733833934034128134234334434534634734834976 unmodified lines4264274284293664304314324334344354364374384394404414424434444454464474484494504514524534544554566 unmodified lines4634644654663854674684694704714724734744753924764774783943953963974794804814824834844853994004014864874884894904914924934944954964974984995005015025035044124134144155055065075084174184194205095105115125135145152 unmodified lines51851952052152243152352452552652752852953053153253353453516 unmodified linesimport { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync,} from 'node:fs'import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'import { promisify } from 'node:util'1 unmodified lineimport { expandSubfs, extractBlobCid, getPdsForDid, readBoundedIdentityJson, resolveDid, type SubfsSubject, unsafeRawIdentityGet,} from '@wispplace/atproto-utils'import { MAX_BLOB_SIZE, MAX_FILE_COUNT } from '@wispplace/constants'import { MAX_BLOB_SIZE, MAX_FILE_COUNT, MAX_SITE_SIZE_SUPPORTER } from '@wispplace/constants'import { normalizeSitePath } from '@wispplace/fs-utils'import { parseLexiconJson } from '@wispplace/lexicons/public-json'import type { Entry, Record as FsRecord } from '@wispplace/lexicons/types/place/wisp/fs'import { validateRecord as validateFsRecord } from '@wispplace/lexicons/types/place/wisp/fs'import { loadMetadata, type SiteMetadata, saveMetadata } from '../lib/metadata.ts'import { createSpinner, pc } from '../lib/progress.ts'
// full blobs remain resident through base64/gzip decoding and the atomic writeconst MAX_CONCURRENT_DOWNLOADS = 20const MAX_CONCURRENT_DOWNLOADS = 3export const MAX_PDS_RECORD_RESPONSE_BYTES = 1024 * 1024export const PDS_RECORD_TIMEOUT_MS = 10_000export const PDS_BLOB_TIMEOUT_MS = 300_000const WINDOWS_RESERVED_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iconst gunzipAsync = promisify(gunzip)
/** Decompress one pulled site blob without accepting unbounded gzip output. */34 unmodified lines } return Buffer.concat(chunks, totalBytes)}
export function validatePulledGzipHeader(content: Uint8Array): void { if (content.length < 2 || content[0] !== 0x1f || content[1] !== 0x8b) { throw new Error('Blob is marked gzip but is not a gzip stream') }}
export function createLogicalSiteBudget(maxBytes = MAX_SITE_SIZE_SUPPORTER) { if (!Number.isSafeInteger(maxBytes) || maxBytes < 0 || maxBytes > MAX_SITE_SIZE_SUPPORTER) { throw new RangeError('Logical site size limit is invalid') } let totalSize = 0 return { reserve(bytes: number) { if (!Number.isSafeInteger(bytes) || bytes < 0 || totalSize > maxBytes - bytes) { throw new Error(`Pulled site exceeds the ${maxBytes}-byte logical size limit`) } totalSize += bytes }, get totalSize() { return totalSize }, }}
function requireSiteFilePath(path: string): string { const normalized = normalizeSitePath(path) if ( !normalized ||83 unmodified lines collection: string, rkey: string,): Promise<GetRecordResponse> { const query = new URLSearchParams({ repo: did, collection, rkey }) const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), PDS_RECORD_TIMEOUT_MS) let res: Response try { res = await fetch(`${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?${query.toString()}`, { signal: controller.signal, }) if (!res.ok) { const res = await fetch(`${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?${query.toString()}`) if (!res.ok) throw new Error(`Failed to fetch record: ${res.status}`)
const data: unknown = await res.json() if (!isGetRecordResponse(data)) throw new Error('PDS returned an invalid record response') return data await res.body?.cancel().catch(() => undefined) throw new Error(`Failed to fetch record: ${res.status}`) } const data: unknown = await readBoundedIdentityJson(res, MAX_PDS_RECORD_RESPONSE_BYTES, controller.signal) if (!isGetRecordResponse(data)) throw new Error('PDS returned an invalid record response') return data } finally { clearTimeout(timeout) }}
type PdsEndpointResolver = (did: string) => Promise<string>
33 unmodified lines entries: Entry[], ownerDidByFilePath: ReadonlyMap<string, string>, pathPrefix: string, existingCids: Record<string, string>,): { toDownload: FileToDownload[]; toSkip: number } {): { toDownload: FileToDownload[]; unchangedFiles: FileToDownload[] } { const toDownload: FileToDownload[] = [] const seenPaths = new Set<string>() let toSkip = 0 const unchangedFiles: FileToDownload[] = []
function entryPath(parent: string, name: string): string { const segment = requireSiteFilePath(name) if (segment.includes('/')) throw new Error('Site entry names must be single path segments')15 unmodified lines if (!cid) continue
const ownerDid = ownerDidByFilePath.get(fullPath) if (!ownerDid) throw new Error('Expanded file is missing its source repository') if (existingCids[fullPath] === cid) { toSkip++ } else { toDownload.push({ path: fullPath, cid, ownerDid, encoding: node.encoding, mimeType: node.mimeType, base64: node.base64, }) } const file = { path: fullPath, cid, ownerDid, encoding: node.encoding, mimeType: node.mimeType, base64: node.base64, } satisfies FileToDownload if (existingCids[fullPath] === cid) unchangedFiles.push(file) else toDownload.push(file) } } }
collect(entries, pathPrefix) return { toDownload, toSkip } return { toDownload, unchangedFiles }}
function withDeadline( parent: AbortSignal | undefined, timeoutMs: number,): { signal: AbortSignal; dispose: () => void } { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), timeoutMs) const abort = () => controller.abort(parent?.reason) if (parent?.aborted) controller.abort(parent.reason) parent?.addEventListener('abort', abort, { once: true }) return { signal: controller.signal, dispose: () => { clearTimeout(timeout) parent?.removeEventListener('abort', abort) }, }}
async function downloadBlob(pdsEndpoint: string, file: FileToDownload): Promise<Buffer> {async function downloadBlob(pdsEndpoint: string, file: FileToDownload, signal?: AbortSignal): Promise<Buffer> { const url = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(file.ownerDid)}&cid=${encodeURIComponent(file.cid)}` const res = await fetch(url) const deadline = withDeadline(signal, PDS_BLOB_TIMEOUT_MS) try { const res = await fetch(url, { signal: deadline.signal })
if (!res.ok) { throw new Error(`Failed to download blob ${file.cid}: ${res.status}`) } if (!res.ok) { await res.body?.cancel().catch(() => undefined) throw new Error(`Failed to download blob ${file.cid}: ${res.status}`) }
let content: Buffer = await readPulledBlob(res) let content: Buffer = await readPulledBlob(res)
// Decode base64 if needed if (file.base64) { const base64String = content.toString('utf-8') content = Buffer.from(base64String, 'base64') } // Decode base64 if needed if (file.base64) { const base64String = content.toString('utf-8') content = Buffer.from(base64String, 'base64') }
// The CLI writes decoded site files, so malformed or oversized gzip must // fail the pull rather than becoming an opaque canonical-looking output. if (file.encoding === 'gzip' && content.length >= 2 && content[0] === 0x1f && content[1] === 0x8b) { content = await decompressPulledGzip(content) } // The CLI writes decoded site files, so malformed or oversized gzip must // fail the pull rather than becoming an opaque canonical-looking output. if (file.encoding === 'gzip') { validatePulledGzipHeader(content) content = await decompressPulledGzip(content) }
return content return content } finally { deadline.dispose() }}
export async function pull(identifier: string, options: PullOptions): Promise<void> { const { site, path: outputPath } = options76 unmodified lines } }
// 6. Collect files to download. const { toDownload, toSkip } = collectFiles(expandedRoot.entries, ownerDidByFilePath, '', existingCids) const collected = collectFiles(expandedRoot.entries, ownerDidByFilePath, '', existingCids) const unchangedFiles = collected.unchangedFiles.filter((file) => { try { return statSync(resolvePullFilePath(outputPath, file.path)).isFile() } catch { return false } }) const toDownload = [ ...collected.toDownload, ...collected.unchangedFiles.filter((file) => !unchangedFiles.includes(file)), ] const unchangedPaths = unchangedFiles.map((file) => file.path) const toSkip = unchangedFiles.length
console.log(pc.dim(`Files to download: ${toDownload.length}, unchanged: ${toSkip}`))
const logicalBudget = createLogicalSiteBudget() if (toDownload.length === 0 && toSkip > 0) { for (const filePath of unchangedPaths) { const srcPath = resolvePullFilePath(outputPath, filePath) if (existsSync(srcPath)) logicalBudget.reserve(statSync(srcPath).size) } console.log(pc.green('\n✓ Site is already up to date\n')) return }
6 unmodified lines }
// 8. Download files. const downloadSpinner = createSpinner(`Downloading ${toDownload.length} files...`).start() const newFileCids: Record<string, string> = { ...existingCids } const newFileCids: Record<string, string> = Object.fromEntries( unchangedFiles.map((file) => [file.path, file.cid]), ) as Record<string, string> let downloaded = 0
try { for (let i = 0; i < toDownload.length; i += MAX_CONCURRENT_DOWNLOADS) { const batch = toDownload.slice(i, i + MAX_CONCURRENT_DOWNLOADS)
await Promise.all( const batchController = new AbortController() const results = await Promise.allSettled( batch.map(async (file) => { const sourcePdsEndpoint = await resolveSourcePdsEndpoint(file.ownerDid) const content = await downloadBlob(sourcePdsEndpoint, file) const filePath = resolvePullFilePath(tempDir, file.path, true) writeFileSync(filePath, content) try { const sourcePdsEndpoint = await resolveSourcePdsEndpoint(file.ownerDid) const content = await downloadBlob(sourcePdsEndpoint, file, batchController.signal) logicalBudget.reserve(content.byteLength) const filePath = resolvePullFilePath(tempDir, file.path, true) writeFileSync(filePath, content)
newFileCids[file.path] = file.cid downloaded++ downloadSpinner.text = `Downloading files: ${downloaded}/${toDownload.length}` newFileCids[file.path] = file.cid downloaded++ downloadSpinner.text = `Downloading files: ${downloaded}/${toDownload.length}` } catch (error) { batchController.abort(error) throw error } }), ) const failure = results.find((result): result is PromiseRejectedResult => result.status === 'rejected') if (failure) throw failure.reason }
downloadSpinner.succeed(`Downloaded ${downloaded} files`)
// 9. Copy unchanged files from existing directory if (toSkip > 0 && existsSync(outputPath)) { const copySpinner = createSpinner(`Copying ${toSkip} unchanged files...`).start()
for (const [filePath, _cid] of Object.entries(existingCids)) { if (!toDownload.find((file) => file.path === filePath)) { const srcPath = resolvePullFilePath(outputPath, filePath) const destPath = resolvePullFilePath(tempDir, filePath, true) for (const filePath of unchangedPaths) { const srcPath = resolvePullFilePath(outputPath, filePath) const destPath = resolvePullFilePath(tempDir, filePath, true)
if (existsSync(srcPath)) { const content = readFileSync(srcPath) writeFileSync(destPath, content) } if (existsSync(srcPath)) { logicalBudget.reserve(statSync(srcPath).size) copyFileSync(srcPath, destPath) } }
copySpinner.succeed(`Copied ${toSkip} unchanged files`)2 unmodified lines // 10. Atomic replace if (existsSync(outputPath)) { const backupPath = `${outputPath}.backup-${Date.now()}` renameSync(outputPath, backupPath) try { renameSync(tempDir, outputPath) renameSync(tempDir, outputPath) } catch (error) { try { renameSync(backupPath, outputPath) } catch { // preserve the original replacement failure; the backup remains recoverable } throw error } rmSync(backupPath, { recursive: true, force: true }) } else { renameSync(tempDir, outputPath) }16 unmodified lines1212345678910111213141516171819202122232415 unmodified lines40414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import { describe, expect, it } from 'bun:test'import { normalizeServeRequestPath } from './serve'import { afterEach, describe, expect, it } from 'bun:test'import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'import { tmpdir } from 'node:os'import { join } from 'node:path'import { MAX_REDIRECT_FILE_BYTES } from '@wispplace/fs-utils'import { handleRequest, loadRedirectRules, MAX_ACTIVE_FILE_STREAMS, normalizeServeRequestPath } from './serve'
const state = (siteDir: string) => ({ did: 'did:plc:test', rkey: 'site', pdsEndpoint: 'https://pds.example', siteDir, settings: null, redirectRules: [],})const temporaryDirectories: string[] = []afterEach(() => { for (const directory of temporaryDirectories.splice(0)) rmSync(directory, { recursive: true, force: true })})
describe('normalizeServeRequestPath', () => { it('keeps only canonical local request paths', () => { expect(normalizeServeRequestPath('/')).toBe('/') expect(normalizeServeRequestPath('/assets/app.js')).toBe('/assets/app.js')15 unmodified lines expect(normalizeServeRequestPath(path)).toBeNull() } })})
describe('serve boundary', () => { it('streams get responses and preserves headers for head', async () => { const dir = mkdtempSync(join(tmpdir(), 'wispctl-')) temporaryDirectories.push(dir) writeFileSync(join(dir, 'hello.txt'), 'hello') const get = handleRequest(new Request('http://localhost/hello.txt'), state(dir)) const head = handleRequest(new Request('http://localhost/hello.txt', { method: 'HEAD' }), state(dir)) expect(await get.text()).toBe('hello') expect(get.headers.get('content-length')).toBe('5') expect(head.status).toBe(get.status) expect(head.headers.get('content-length')).toBe(get.headers.get('content-length')) expect(await head.text()).toBe('') })
it('rejects methods other than get and head', () => { const response = handleRequest( new Request('http://localhost/', { method: 'POST', body: 'ignored' }), state(tmpdir()), ) expect(response.status).toBe(405) expect(response.headers.get('allow')).toBe('GET, HEAD') })
it('rejects oversized redirects before decoding', () => { const dir = mkdtempSync(join(tmpdir(), 'wispctl-')) temporaryDirectories.push(dir) writeFileSync(join(dir, '_redirects'), Buffer.alloc(MAX_REDIRECT_FILE_BYTES + 1, 0x61)) expect(loadRedirectRules(dir)).toEqual([]) })
it('caps active file streams and releases permits when consumed', async () => { const dir = mkdtempSync(join(tmpdir(), 'wispctl-')) temporaryDirectories.push(dir) writeFileSync(join(dir, 'hello.txt'), 'hello') const responses = Array.from({ length: MAX_ACTIVE_FILE_STREAMS }, () => handleRequest(new Request('http://localhost/hello.txt'), state(dir)), ) const saturated = handleRequest(new Request('http://localhost/hello.txt'), state(dir)) expect(saturated.status).toBe(503) await Promise.all(responses.map((response) => response.text())) await new Promise((resolve) => setTimeout(resolve, 0)) expect(handleRequest(new Request('http://localhost/hello.txt'), state(dir)).status).toBe(200) })})5 unmodified lines678910101112137 unmodified lines5 unmodified linesAllow: /$
# Disallow application pagesDisallow: /editorDisallow: /adminDisallow: /onboarding
# Disallow API routesDisallow: /api/7 unmodified lines879 unmodified linesExpand all880881882883884884885886887888581 unmodified linesExpand all879 unmodified linesExpand all await cursorPersistQueue.catch(() => undefined) // A previous test's in-flight stop must finish before the lifecycle reset, // otherwise its teardown tail can race the next test's start. if (stopPromise) await stopPromise // Subscriber lifecycle reset so each test starts from 'idle' again. else if (lifecycleState !== 'idle') await stopCacheInvalidationSubscriber() stopSubscriberHeartbeat() stopSubscriberSupervisor() subscriberContext = null subscriberGeneration += 1581 unmodified linesExpand all