@orkestrel/test 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["/**\n * The attempts `removeTree` makes before rethrowing a retryable removal error.\n */\nexport const REMOVE_TREE_MAX_ATTEMPTS = 10\n\n/**\n * The synchronous delay, in milliseconds, `removeTree` waits between attempts.\n */\nexport const REMOVE_TREE_RETRY_DELAY_MS = 100\n\n/**\n * The error codes `removeTree` retries; every other code rethrows immediately.\n */\nexport const REMOVE_TREE_RETRYABLE_CODES: readonly string[] = Object.freeze([\n\t'EBUSY',\n\t'ENOTEMPTY',\n\t'EPERM',\n])\n","import type { Socket } from 'node:net'\nimport type { WaitOptions } from '@src/core'\nimport type {\n\tInventoryOptions,\n\tScratchIdentity,\n\tScratchInterface,\n\tUpgradeOptions,\n\tUpgradeResult,\n} from './types.js'\nimport { Buffer } from 'node:buffer'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\trealpathSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { request as requestHTTP } from 'node:http'\nimport { tmpdir } from 'node:os'\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { waitForDelay } from '@src/core'\nimport {\n\tREMOVE_TREE_MAX_ATTEMPTS,\n\tREMOVE_TREE_RETRY_DELAY_MS,\n\tREMOVE_TREE_RETRYABLE_CODES,\n} from './constants.js'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Creates a symbolic link with a directory-junction fallback for hosts that refuse symbolic links.\n *\n * @param path - The path where the link is created.\n * @param source - The destination path the link points at.\n * @throws The original link error when its code is not `EPERM`, or when the source names an\n * existing non-directory; otherwise, any error from inspecting the source or creating the junction.\n * @remarks Only `EPERM` from the first symbolic-link attempt triggers the fallback. The fallback\n * resolves the source against the link's directory. An existing non-directory rethrows the original\n * `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is\n * accepted to create a dangling junction. Where the host creates a junction, its stored value is the\n * resolved absolute path.\n */\nexport function createLink(path: string, source: string): void {\n\ttry {\n\t\tsymlinkSync(source, path)\n\t} catch (error) {\n\t\tconst code =\n\t\t\ttypeof error === 'object' &&\n\t\t\terror !== null &&\n\t\t\t'code' in error &&\n\t\t\ttypeof error.code === 'string'\n\t\t\t\t? error.code\n\t\t\t\t: undefined\n\t\tif (code !== 'EPERM') throw error\n\n\t\tconst resolved = resolve(dirname(path), source)\n\t\tconst status = statSync(resolved, { throwIfNoEntry: false })\n\t\tif (status !== undefined && !status.isDirectory()) throw error\n\t\tsymlinkSync(resolved, path, 'junction')\n\t}\n}\n\n/**\n * Removes a directory tree, retrying past a transient Windows handle-release race.\n *\n * @param path - The absolute directory to remove.\n * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,\n * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.\n * @remarks On Windows, a directory that a just-exited process still holds as its current\n * working directory throws `EPERM` for a short interval after that process exits. Node's own\n * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed\n * against a real held directory, they neither delay nor retry before rethrowing, so the retry\n * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait\n * at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which\n * retries every refusal inside a caller's budget rather than the codes named here.\n */\nexport function removeTree(path: string): void {\n\tfor (let attempt = 1; ; attempt++) {\n\t\ttry {\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === 'object' &&\n\t\t\t\terror !== null &&\n\t\t\t\t'code' in error &&\n\t\t\t\ttypeof error.code === 'string'\n\t\t\t\t\t? error.code\n\t\t\t\t\t: undefined\n\t\t\tif (\n\t\t\t\tcode === undefined ||\n\t\t\t\t!REMOVE_TREE_RETRYABLE_CODES.includes(code) ||\n\t\t\t\tattempt >= REMOVE_TREE_MAX_ATTEMPTS\n\t\t\t) {\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, REMOVE_TREE_RETRY_DELAY_MS)\n\t\t}\n\t}\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n\n/**\n * Reports whether a process id names a live process.\n *\n * @param pid - The process id to read.\n * @returns True if a process holds that id at the moment of the call; false otherwise, including a\n * pid the host refuses.\n * @throws Nothing. Every host refusal reads as false.\n * @remarks This is an instantaneous observation rather than a claim of ownership. A host reuses a\n * process id after the process holding it exits, so a true answer says some process holds that id now\n * and never says it is the process the caller started. Two host answers are worth knowing. A POSIX\n * host refuses signal `0` to a process another user owns with `EPERM`, and that refusal reads as\n * false here. A pid of `0` names the caller's own process group on POSIX and the system idle process\n * on Windows, so it reads as true on both without naming a process anyone started.\n *\n * A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts\n * signal `0`, so its `/proc` status is read and a `Z` state reads as false.\n */\nexport function isRunning(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0)\n\t} catch {\n\t\treturn false\n\t}\n\tif (process.platform !== 'linux') return true\n\n\t// The zombie refinement is unproven on a host that carries no `/proc`; a Linux gate drives it.\n\ttry {\n\t\tconst status = readFileSync(`/proc/${String(pid)}/stat`, 'utf8')\n\t\tconst boundary = status.lastIndexOf(') ')\n\t\treturn boundary < 0 || status.slice(boundary + 2, boundary + 3) !== 'Z'\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Waits for a socket to close, accepting a peer reset as a forced close.\n *\n * @param socket - The socket to wait on. One that has already closed resolves without listening.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves when the socket emits `close`.\n * @throws The socket's own error when its code is not `ECONNRESET`, the abort reason, or an `Error`\n * when a bound is invalid or the socket does not close within the budget.\n * @remarks Default budget: `1000` milliseconds. A reset is the peer forcing the connection down, and\n * the socket still emits `close` afterwards, so `ECONNRESET` is waited past rather than raised while\n * every other error ends the wait. The interval is validated for consistency with the wait family but\n * is not used, because this helper parks on the socket's events. Both listeners are removed on every\n * settlement, so a caller may wait on one socket repeatedly.\n */\nexport async function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tif (!Number.isFinite(budget) || budget < 0) {\n\t\tthrow new Error('Socket budget must be finite and non-negative')\n\t}\n\tif (!Number.isFinite(interval) || interval < 0) {\n\t\tthrow new Error('Socket interval must be finite and non-negative')\n\t}\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\tif (socket.closed) return\n\n\t// The resolvers are the listeners themselves, so the same references remove them afterwards.\n\tconst closed = Promise.withResolvers<boolean>()\n\tconst failed = Promise.withResolvers<NodeJS.ErrnoException>()\n\tconst expiry = Promise.withResolvers<never>()\n\tconst aborted = Promise.withResolvers<never>()\n\tconst subscription = new AbortController()\n\tsocket.on('close', closed.resolve)\n\tsocket.on('error', failed.resolve)\n\tsignal?.addEventListener('abort', () => aborted.reject(signal.reason), {\n\t\tonce: true,\n\t\tsignal: subscription.signal,\n\t})\n\tconst timer = setTimeout(() => {\n\t\texpiry.reject(new Error(`Socket did not close within ${budget}ms`))\n\t}, budget)\n\n\ttry {\n\t\tconst error = await Promise.race([\n\t\t\tclosed.promise.then(() => undefined),\n\t\t\tfailed.promise,\n\t\t\texpiry.promise,\n\t\t\taborted.promise,\n\t\t])\n\t\tif (error === undefined) return\n\t\tif (error.code !== 'ECONNRESET') throw error\n\t\tawait Promise.race([closed.promise, expiry.promise, aborted.promise])\n\t} finally {\n\t\tclearTimeout(timer)\n\t\tsubscription.abort()\n\t\tsocket.off('close', closed.resolve)\n\t\tsocket.off('error', failed.resolve)\n\t}\n}\n\n/**\n * Destroys a scratch directory, retrying until the host releases it.\n *\n * @param scratch - The scratch directory to destroy.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves once `destroy()` returns without throwing.\n * @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The\n * exhaustion error carries the last host refusal as its `cause`.\n * @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a\n * directory for a short interval after the process that held it exits, and a just-stopped child's\n * working directory is the case this exists for, so removal is attempted until the host lets go\n * rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this\n * is the bounded retry around it. A directory nothing releases still fails, with the host's own\n * refusal as the `cause`.\n *\n * Every refusal is retried, deliberately, and that is wider than {@link removeTree}'s policy: that\n * one retries the codes {@link REMOVE_TREE_RETRYABLE_CODES} names and rethrows the rest at once.\n * The hold this waits out is not classifiable across hosts — Windows reports a working-directory\n * hold as `EPERM`, POSIX hosts and network filesystems report their own — so a code list here would\n * be a list of the hosts it had been run on. The residual is the cost of that: a fault no wait can\n * clear, such as a path removed from under the allocation or a permission the process never had,\n * spends the whole budget before it surfaces, and it surfaces wrapped in the exhaustion error with\n * the host's refusal as `cause` rather than by identity. Pass a shorter `budget` or a `signal`\n * wherever a caller must bound that cost.\n */\nexport async function destroyScratch(\n\tscratch: ScratchInterface,\n\toptions?: WaitOptions,\n): Promise<void> {\n\tconst budget = options?.budget ?? 10_000\n\tconst interval = options?.interval ?? 25\n\tif (!Number.isFinite(budget) || budget < 0) {\n\t\tthrow new Error('Scratch budget must be finite and non-negative')\n\t}\n\tif (!Number.isFinite(interval) || interval < 0) {\n\t\tthrow new Error('Scratch interval must be finite and non-negative')\n\t}\n\n\tconst start = performance.now()\n\tlet refusal: unknown\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\ttry {\n\t\t\tscratch.destroy()\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\trefusal = error\n\t\t}\n\n\t\tif (performance.now() - start >= budget) {\n\t\t\tthrow new Error(`Scratch directory was not destroyed within ${budget}ms`, {\n\t\t\t\tcause: refusal,\n\t\t\t})\n\t\t}\n\t\tawait waitForDelay(interval)\n\t}\n}\n\n/**\n * Drives a real client upgrade request against a loopback port and reports what the server did.\n *\n * @param port - The port the server listens on at `127.0.0.1`.\n * @param options - Optional request path, offered subprotocols, time bounds, and abort signal.\n * @returns A promise resolving to the server's answer: a claimed upgrade with the protocol it\n * selected, or a refusal with the status it answered.\n * @throws The client's own transport error, such as the `ECONNREFUSED` a closed port answers, the\n * abort reason, or an `Error` when a bound is invalid or the server does not answer within the\n * budget.\n * @remarks Default budget: `1000` milliseconds. The request carries `Connection: Upgrade` and\n * `Upgrade: websocket`, which is what makes a server's `upgrade` handler the one that answers it.\n * The `upgrade`, `response`, and `error` events are mutually exclusive in practice and the promise\n * settles on whichever arrives first, so a second event changes nothing. The client socket is\n * destroyed before every settlement, on the claimed path because an upgraded socket is detached from\n * the request and outlives it otherwise. The request is made with no agent, so no pooled connection\n * survives the call to keep a suite's event loop alive.\n *\n * A server that accepts the connection and answers nothing raises no transport error, so the budget\n * is what ends that call: the rejection names the port and path it was waiting on. The interval is\n * validated for consistency with the wait family but is not used, because this helper parks on the\n * request's events.\n *\n * A `101` is the claimed path's status on the wire and is deliberately not reported: `status` is the\n * refused arm's member, and a claimed upgrade produced no plain answer.\n * @example\n * ```ts\n * const answer = await requestUpgrade(loopback.port, { path: '/socket', protocols: ['chat'] })\n * // { claimed: true, protocol: 'chat' }\n * ```\n */\nexport async function requestUpgrade(\n\tport: number,\n\toptions?: UpgradeOptions,\n): Promise<UpgradeResult> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tif (!Number.isFinite(budget) || budget < 0) {\n\t\tthrow new Error('Upgrade budget must be finite and non-negative')\n\t}\n\tif (!Number.isFinite(interval) || interval < 0) {\n\t\tthrow new Error('Upgrade interval must be finite and non-negative')\n\t}\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\n\tconst path = options?.path ?? '/'\n\tconst target = `127.0.0.1:${port}${path}`\n\tconst headers: Record<string, string> = { connection: 'Upgrade', upgrade: 'websocket' }\n\tconst protocols = options?.protocols ?? []\n\tif (protocols.length > 0) headers['sec-websocket-protocol'] = protocols.join(', ')\n\n\tconst request = requestHTTP({ agent: false, headers, host: '127.0.0.1', path, port })\n\tconst settled = Promise.withResolvers<UpgradeResult>()\n\tconst expiry = Promise.withResolvers<never>()\n\tconst aborted = Promise.withResolvers<never>()\n\tconst subscription = new AbortController()\n\trequest.on('upgrade', (response, socket) => {\n\t\tconst protocol = response.headers['sec-websocket-protocol']\n\t\tsocket.destroy()\n\t\tsettled.resolve({ claimed: true, protocol })\n\t})\n\trequest.on('response', (response) => {\n\t\tconst status = response.statusCode\n\t\tresponse.destroy()\n\t\trequest.destroy()\n\t\t// A client response reaches this listener only after its status line is parsed, so the\n\t\t// refusal is unproven; the server-side `IncomingMessage` that shares this type carries no\n\t\t// status and would drive it.\n\t\tif (status === undefined) {\n\t\t\tsettled.reject(new Error(`Upgrade request to ${target} was answered without a status`))\n\t\t\treturn\n\t\t}\n\t\tsettled.resolve({ claimed: false, status })\n\t})\n\trequest.on('error', (error) => {\n\t\trequest.destroy()\n\t\tsettled.reject(error)\n\t})\n\tsignal?.addEventListener('abort', () => aborted.reject(signal.reason), {\n\t\tonce: true,\n\t\tsignal: subscription.signal,\n\t})\n\tconst timer = setTimeout(() => {\n\t\texpiry.reject(new Error(`Upgrade request to ${target} was not answered within ${budget}ms`))\n\t}, budget)\n\trequest.end()\n\n\ttry {\n\t\treturn await Promise.race([settled.promise, expiry.promise, aborted.promise])\n\t} finally {\n\t\tclearTimeout(timer)\n\t\tsubscription.abort()\n\t\trequest.destroy()\n\t}\n}\n\n/**\n * Checks whether this host links a directory, by creating one link and reading through it.\n *\n * @returns True if the created link reports as a symbolic link, resolves to a directory, and reaches\n * the destination's contents; false otherwise, including every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks `symlinkSync(source, target, 'junction')` creates a directory junction on Windows, which\n * needs no privilege, and Node ignores the type argument off Windows, so one call covers both hosts.\n * The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call\n * probes and cleans up after itself, so a host whose answer changes is read again rather than\n * remembered.\n */\nexport function supportsDirectoryLinks(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-directory-links-'))\n\ttry {\n\t\tconst source = join(directory, 'source')\n\t\tconst link = join(directory, 'link')\n\t\tmkdirSync(source)\n\t\twriteFileSync(join(source, 'marker.txt'), 'marked')\n\t\tsymlinkSync(source, link, 'junction')\n\t\treturn (\n\t\t\tlstatSync(link).isSymbolicLink() &&\n\t\t\tstatSync(link).isDirectory() &&\n\t\t\treadFileSync(join(link, 'marker.txt'), 'utf8') === 'marked'\n\t\t)\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host links a file, by creating one link and reading the file through it.\n *\n * @returns True if the file's contents are readable through the link; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks `symlinkSync(source, target, 'file')` needs the symbolic-link privilege, which Windows\n * grants under Developer Mode or administrator rights and refuses with `EPERM` otherwise, so the\n * answer is true on POSIX and on a privileged Windows host. Where it is false, no mechanism reaches a\n * file through a link and a proof that reads one back cannot run. This is a separate question from\n * {@link supportsDirectoryLinks}, which an unprivileged Windows host answers true through a junction\n * while answering this one false.\n */\nexport function supportsFileLinks(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-file-links-'))\n\ttry {\n\t\tconst source = join(directory, 'source.txt')\n\t\tconst link = join(directory, 'link.txt')\n\t\twriteFileSync(source, 'linked')\n\t\tsymlinkSync(source, link, 'file')\n\t\treturn readFileSync(link, 'utf8') === 'linked'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether POSIX permission bits round-trip through this host's `chmod` and `stat`.\n *\n * @returns True if a directory created with mode `0o700` reports that mode back; false otherwise,\n * including every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks POSIX reports `mode & 0o777 === 0o700` and Windows reports `0o666` regardless, so the\n * answer is true on POSIX and false on Windows. Storing a bit is a narrower question than enforcing\n * it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the\n * bits describe, so a caller that needs a permission to be enforced probes the refusal it needs\n * rather than reading this.\n */\nexport function supportsMode(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-mode-'))\n\ttry {\n\t\tconst path = join(directory, 'moded')\n\t\tmkdirSync(path, { mode: 0o700 })\n\t\treturn (statSync(path).mode & 0o777) === 0o700\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host treats two names differing only by case as distinct files.\n *\n * @returns True if `A` and `a` hold the contents each was written with; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks The names `A` and `a` differ by case and by nothing else, which is what makes the reading\n * an answer about case folding rather than an answer about two unrelated files. A case-folding volume\n * routes the second write onto the first entry, so reading the first back returns the second's\n * contents and the answer is false. The answer is true on a typical POSIX host and false on a\n * case-folding Windows or macOS volume.\n */\nexport function supportsCase(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-case-'))\n\ttry {\n\t\tconst upper = join(directory, 'A')\n\t\tconst lower = join(directory, 'a')\n\t\twriteFileSync(upper, 'upper')\n\t\twriteFileSync(lower, 'lower')\n\t\treturn readFileSync(upper, 'utf8') === 'upper' && readFileSync(lower, 'utf8') === 'lower'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host accepts a filename carrying a raw byte no UTF-8 decoder resolves.\n *\n * @returns True if a name ending in byte `0x80` is written and read back; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows\n * rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed\n * as a `Buffer` because the byte survives no string round trip.\n */\nexport function supportsBytes(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-bytes-'))\n\ttry {\n\t\tconst name = Buffer.concat([Buffer.from(`${directory}${sep}`), Buffer.from([0x80])])\n\t\twriteFileSync(name, 'raw')\n\t\treturn readFileSync(name, 'utf8') === 'raw'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n","import type { Server } from 'node:net'\nimport type {\n\tCookieJarInterface,\n\tLoopbackInterface,\n\tScratchIdentity,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { createLink, matchesIdentity, removeTree, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\tremoveTree(path)\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t\treturn candidate\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tcreateLink(candidate, source)\n\t\t\treturn candidate\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\tremoveTree(candidate)\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\tremoveTree(path)\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n\n/**\n * Creates a cookie jar that records a real response's cookies and replays them as one header.\n *\n * @returns The rendered request header, and the members that read and capture cookies.\n * @remarks Selection is by name alone: no `Domain` or `Path` matching, no `Expires` or `Secure`\n * handling, and no persistence beyond the jar. That is what a test driving one origin over one path\n * needs, and a fixture needing a browser's cookie store needs a browser rather than this.\n */\nexport function createCookieJar(): CookieJarInterface {\n\tconst cookies = new Map<string, string>()\n\treturn {\n\t\tget header() {\n\t\t\tconst pairs = [...cookies].map(([name, value]) => `${name}=${value}`)\n\t\t\treturn pairs.length === 0 ? undefined : pairs.join('; ')\n\t\t},\n\t\tread(name) {\n\t\t\treturn cookies.get(name)\n\t\t},\n\t\tcapture(response) {\n\t\t\tconst fields = response.headers.getSetCookie()\n\t\t\tfor (const field of fields) {\n\t\t\t\tconst boundary = field.indexOf(';')\n\t\t\t\tconst pair = boundary < 0 ? field : field.slice(0, boundary)\n\t\t\t\tconst separator = pair.indexOf('=')\n\t\t\t\tif (separator < 1) continue\n\n\t\t\t\tconst name = pair.slice(0, separator)\n\t\t\t\t// An origin spells a deletion `Max-Age=0` in whatever case and spacing it likes, so the\n\t\t\t\t// attribute is matched rather than compared.\n\t\t\t\tif (/;\\s*max-age\\s*=\\s*0\\s*(?:;|$)/iu.test(field)) cookies.delete(name)\n\t\t\t\telse cookies.set(name, pair.slice(separator + 1))\n\t\t\t}\n\t\t\treturn fields\n\t\t},\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["/**\n * Caps the attempts `removeTree` makes before rethrowing a retryable removal error.\n */\nexport const REMOVE_TREE_MAX_ATTEMPTS = 10\n\n/**\n * Names the synchronous delay, in milliseconds, `removeTree` waits between attempts.\n */\nexport const REMOVE_TREE_RETRY_DELAY_MS = 100\n\n/**\n * Names the error codes `removeTree` retries; every other code rethrows immediately.\n */\nexport const REMOVE_TREE_RETRYABLE_CODES: readonly string[] = Object.freeze([\n\t'EBUSY',\n\t'ENOTEMPTY',\n\t'EPERM',\n])\n","import type { Stats } from 'node:fs'\nimport type { Socket } from 'node:net'\nimport type { WaitOptions } from '@src/core'\nimport type {\n\tInventoryOptions,\n\tScratchIdentity,\n\tScratchInterface,\n\tUpgradeOptions,\n\tUpgradeResult,\n} from './types.js'\nimport { Buffer } from 'node:buffer'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\trealpathSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { request as requestHTTP } from 'node:http'\nimport { tmpdir } from 'node:os'\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { checkBounds, waitForDelay } from '@src/core'\nimport {\n\tREMOVE_TREE_MAX_ATTEMPTS,\n\tREMOVE_TREE_RETRY_DELAY_MS,\n\tREMOVE_TREE_RETRYABLE_CODES,\n} from './constants.js'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// `relative` answers with an absolute path where the target carries a root of its own — a second\n\t// drive letter or a UNC share on Windows — and that spelling names no ancestor, so the `..` tests\n\t// miss it and containment turns on `isAbsolute`.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Resolves a target that stays below a root directory, refusing an escape.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target below the root.\n * @throws An `Error` reading `Path outside scratch directory: <target>` when the target escapes the\n * root.\n * @remarks This is {@link resolveContained} with the refusal every contained scratch operation makes\n * of an escape, so the check and its one message are stated once. Read `resolveContained` where an\n * escape is an answer rather than a refusal.\n */\nexport function requireContained(root: string, target: string): string {\n\tconst candidate = resolveContained(root, target)\n\tif (candidate === undefined) throw new Error(`Path outside scratch directory: ${target}`)\n\treturn candidate\n}\n\n/**\n * Reads the identity of one allocated directory off a host status.\n *\n * @param status - The status read from the directory's path.\n * @returns The device, index node, and creation time that together name the allocation.\n */\nexport function readIdentity(status: Stats): ScratchIdentity {\n\treturn { birth: status.birthtimeMs, device: status.dev, inode: status.ino }\n}\n\n/**\n * Reads the `code` an unknown thrown value carries.\n *\n * @param error - The thrown value to read.\n * @returns The string `code` the value carries, or `undefined` when it carries none.\n * @remarks The read is contained on its own terms: a value that is not an object, one carrying no\n * `code`, and one carrying a `code` that is not a string all answer `undefined`. A null-prototype\n * object is read the same way, because the key is tested with `in` rather than through\n * `hasOwnProperty`.\n */\nexport function readErrorCode(error: unknown): string | undefined {\n\treturn typeof error === 'object' &&\n\t\terror !== null &&\n\t\t'code' in error &&\n\t\ttypeof error.code === 'string'\n\t\t? error.code\n\t\t: undefined\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns True if the device, the index node, and the creation time all match; false otherwise.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns True if an exclusion names the key or one of its ancestors; false otherwise.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Creates a symbolic link with a directory-junction fallback for hosts that refuse symbolic links.\n *\n * @param path - The path where the link is created.\n * @param source - The destination path the link points at.\n * @throws The original link error when its code is not `EPERM`, or when the source names an\n * existing non-directory; otherwise, any error from inspecting the source or creating the junction.\n * @remarks Only `EPERM` from the first symbolic-link attempt triggers the fallback. The fallback\n * resolves the source against the link's directory. An existing non-directory rethrows the original\n * `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is\n * accepted to create a dangling junction. Where the host creates a junction, its stored value is the\n * resolved absolute path.\n */\nexport function createLink(path: string, source: string): void {\n\ttry {\n\t\tsymlinkSync(source, path)\n\t} catch (error) {\n\t\tif (readErrorCode(error) !== 'EPERM') throw error\n\n\t\tconst resolved = resolve(dirname(path), source)\n\t\tconst status = statSync(resolved, { throwIfNoEntry: false })\n\t\tif (status !== undefined && !status.isDirectory()) throw error\n\t\tsymlinkSync(resolved, path, 'junction')\n\t}\n}\n\n/**\n * Removes a directory tree, retrying past a transient Windows handle-release race.\n *\n * @param path - The absolute directory to remove.\n * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,\n * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.\n * @remarks On Windows, a directory that a just-exited process still holds as its current\n * working directory throws `EPERM` for a short interval after that process exits. Node's own\n * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed\n * against a real held directory, they neither delay nor retry before rethrowing, so the retry\n * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait\n * at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which\n * retries every refusal inside a caller's budget rather than the codes named here.\n */\nexport function removeTree(path: string): void {\n\tfor (let attempt = 1; ; attempt++) {\n\t\ttry {\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tconst code = readErrorCode(error)\n\t\t\tif (\n\t\t\t\tcode === undefined ||\n\t\t\t\t!REMOVE_TREE_RETRYABLE_CODES.includes(code) ||\n\t\t\t\tattempt >= REMOVE_TREE_MAX_ATTEMPTS\n\t\t\t) {\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, REMOVE_TREE_RETRY_DELAY_MS)\n\t\t}\n\t}\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n\n/**\n * Reports whether a process id names a live process.\n *\n * @param pid - The process id to read.\n * @returns True if a process holds that id at the moment of the call; false otherwise, including a\n * pid the host refuses.\n * @throws Nothing. Every host refusal reads as false.\n * @remarks This is an instantaneous observation rather than a claim of ownership. A host reuses a\n * process id after the process holding it exits, so a true answer says some process holds that id now\n * and never says it is the process the caller started. Two host answers are worth knowing. A POSIX\n * host refuses signal `0` to a process another user owns with `EPERM`, and that refusal reads as\n * false here. A pid of `0` names the caller's own process group on POSIX and the system idle process\n * on Windows, so it reads as true on both without naming a process anyone started.\n *\n * A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts\n * signal `0`, so its `/proc` status is read and a `Z` state reads as false.\n */\nexport function isRunning(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0)\n\t} catch {\n\t\treturn false\n\t}\n\tif (process.platform !== 'linux') return true\n\n\t// The zombie refinement is unproven on a host that carries no `/proc`; a Linux gate drives it.\n\ttry {\n\t\tconst status = readFileSync(`/proc/${String(pid)}/stat`, 'utf8')\n\t\tconst boundary = status.lastIndexOf(') ')\n\t\treturn boundary < 0 || status.slice(boundary + 2, boundary + 3) !== 'Z'\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Waits for a socket to close, accepting a peer reset as a forced close.\n *\n * @param socket - The socket to wait on. One that has already closed resolves without listening.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves when the socket emits `close`.\n * @throws The socket's own error when its code is not `ECONNRESET`, the abort reason, or an `Error`\n * when a bound is invalid or the socket does not close within the budget.\n * @remarks Default budget: `1000` milliseconds. A reset is the peer forcing the connection down, and\n * the socket still emits `close` afterwards, so `ECONNRESET` is waited past rather than raised while\n * every other error ends the wait. The interval is validated for consistency with the wait family but\n * is not used, because this helper parks on the socket's events. Both listeners are removed on every\n * settlement, so a caller may wait on one socket repeatedly.\n */\nexport async function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Socket', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\tif (socket.closed) return\n\n\t// The resolvers are the listeners themselves, so the same references remove them afterwards.\n\tconst closed = Promise.withResolvers<boolean>()\n\tconst failed = Promise.withResolvers<NodeJS.ErrnoException>()\n\tconst expiry = Promise.withResolvers<never>()\n\tconst aborted = Promise.withResolvers<never>()\n\tconst subscription = new AbortController()\n\tsocket.on('close', closed.resolve)\n\tsocket.on('error', failed.resolve)\n\tsignal?.addEventListener('abort', () => aborted.reject(signal.reason), {\n\t\tonce: true,\n\t\tsignal: subscription.signal,\n\t})\n\tconst timer = setTimeout(() => {\n\t\texpiry.reject(new Error(`Socket did not close within ${budget}ms`))\n\t}, budget)\n\n\ttry {\n\t\tconst error = await Promise.race([\n\t\t\tclosed.promise.then(() => undefined),\n\t\t\tfailed.promise,\n\t\t\texpiry.promise,\n\t\t\taborted.promise,\n\t\t])\n\t\tif (error === undefined) return\n\t\tif (error.code !== 'ECONNRESET') throw error\n\t\tawait Promise.race([closed.promise, expiry.promise, aborted.promise])\n\t} finally {\n\t\tclearTimeout(timer)\n\t\tsubscription.abort()\n\t\tsocket.off('close', closed.resolve)\n\t\tsocket.off('error', failed.resolve)\n\t}\n}\n\n/**\n * Destroys a scratch directory, retrying until the host releases it.\n *\n * @param scratch - The scratch directory to destroy.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves once `destroy()` returns without throwing.\n * @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The\n * exhaustion error carries the last host refusal as its `cause`.\n * @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a\n * directory for a short interval after the process that held it exits, and a just-stopped child's\n * working directory is the case this exists for, so removal is attempted until the host lets go\n * rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this\n * is the bounded retry around it. A directory nothing releases still fails, with the host's own\n * refusal as the `cause`.\n *\n * Every refusal is retried, deliberately, and that is wider than {@link removeTree}'s policy: that\n * one retries the codes {@link REMOVE_TREE_RETRYABLE_CODES} names and rethrows the rest at once.\n * The hold this waits out is not classifiable across hosts — Windows reports a working-directory\n * hold as `EPERM`, POSIX hosts and network filesystems report their own — so a code list here would\n * be a list of the hosts it had been run on. The residual is the cost of that: a fault no wait can\n * clear, such as a path removed from under the allocation or a permission the process never had,\n * spends the whole budget before it surfaces, and it surfaces wrapped in the exhaustion error with\n * the host's refusal as `cause` rather than by identity. Pass a shorter `budget` or a `signal`\n * wherever a caller must bound that cost.\n */\nexport async function destroyScratch(\n\tscratch: ScratchInterface,\n\toptions?: WaitOptions,\n): Promise<void> {\n\tconst budget = options?.budget ?? 10_000\n\tconst interval = options?.interval ?? 25\n\tcheckBounds('Scratch', budget, interval)\n\n\tconst start = performance.now()\n\tlet refusal: unknown\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\ttry {\n\t\t\tscratch.destroy()\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\trefusal = error\n\t\t}\n\n\t\tif (performance.now() - start >= budget) {\n\t\t\tthrow new Error(`Scratch directory was not destroyed within ${budget}ms`, {\n\t\t\t\tcause: refusal,\n\t\t\t})\n\t\t}\n\t\tawait waitForDelay(interval)\n\t}\n}\n\n/**\n * Drives a real client upgrade request against a loopback port and reports what the server did.\n *\n * @param port - The port the server listens on at `127.0.0.1`.\n * @param options - Optional request path, offered subprotocols, time bounds, and abort signal.\n * @returns A promise resolving to the server's answer: a claimed upgrade with the protocol it\n * selected, or a refusal with the status it answered.\n * @throws The client's own transport error, such as the `ECONNREFUSED` a closed port answers, the\n * abort reason, or an `Error` when a bound is invalid or the server does not answer within the\n * budget.\n * @remarks Default budget: `1000` milliseconds. The request carries `Connection: Upgrade` and\n * `Upgrade: websocket`, which is what makes a server's `upgrade` handler the one that answers it.\n * The `upgrade`, `response`, and `error` events are mutually exclusive in practice and the promise\n * settles on whichever arrives first, so a second event changes nothing. The client socket is\n * destroyed before every settlement, on the claimed path because an upgraded socket is detached from\n * the request and outlives it otherwise. The request is made with no agent, so no pooled connection\n * survives the call to keep a suite's event loop alive.\n *\n * A server that accepts the connection and answers nothing raises no transport error, so the budget\n * is what ends that call: the rejection names the port and path it was waiting on. The interval is\n * validated for consistency with the wait family but is not used, because this helper parks on the\n * request's events.\n *\n * A `101` is the claimed path's status on the wire and is deliberately not reported: `status` is the\n * refused arm's member, and a claimed upgrade produced no plain answer.\n * @example\n * ```ts\n * const answer = await requestUpgrade(loopback.port, { path: '/socket', protocols: ['chat'] })\n * // { claimed: true, protocol: 'chat' }\n * ```\n */\nexport async function requestUpgrade(\n\tport: number,\n\toptions?: UpgradeOptions,\n): Promise<UpgradeResult> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Upgrade', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\n\tconst path = options?.path ?? '/'\n\tconst target = `127.0.0.1:${port}${path}`\n\tconst headers: Record<string, string> = { connection: 'Upgrade', upgrade: 'websocket' }\n\tconst protocols = options?.protocols ?? []\n\tif (protocols.length > 0) headers['sec-websocket-protocol'] = protocols.join(', ')\n\n\tconst request = requestHTTP({ agent: false, headers, host: '127.0.0.1', path, port })\n\tconst settled = Promise.withResolvers<UpgradeResult>()\n\tconst expiry = Promise.withResolvers<never>()\n\tconst aborted = Promise.withResolvers<never>()\n\tconst subscription = new AbortController()\n\trequest.on('upgrade', (response, socket) => {\n\t\tconst protocol = response.headers['sec-websocket-protocol']\n\t\tsocket.destroy()\n\t\tsettled.resolve({ claimed: true, protocol })\n\t})\n\trequest.on('response', (response) => {\n\t\tconst status = response.statusCode\n\t\tresponse.destroy()\n\t\trequest.destroy()\n\t\t// A client response reaches this listener only after its status line is parsed, so the\n\t\t// refusal is unproven; the server-side `IncomingMessage` that shares this type carries no\n\t\t// status and would drive it.\n\t\tif (status === undefined) {\n\t\t\tsettled.reject(new Error(`Upgrade request to ${target} was answered without a status`))\n\t\t\treturn\n\t\t}\n\t\tsettled.resolve({ claimed: false, status })\n\t})\n\trequest.on('error', (error) => {\n\t\trequest.destroy()\n\t\tsettled.reject(error)\n\t})\n\tsignal?.addEventListener('abort', () => aborted.reject(signal.reason), {\n\t\tonce: true,\n\t\tsignal: subscription.signal,\n\t})\n\tconst timer = setTimeout(() => {\n\t\texpiry.reject(new Error(`Upgrade request to ${target} was not answered within ${budget}ms`))\n\t}, budget)\n\trequest.end()\n\n\ttry {\n\t\treturn await Promise.race([settled.promise, expiry.promise, aborted.promise])\n\t} finally {\n\t\tclearTimeout(timer)\n\t\tsubscription.abort()\n\t\trequest.destroy()\n\t}\n}\n\n/**\n * Checks whether this host links a directory, by creating one link and reading through it.\n *\n * @returns True if the created link reports as a symbolic link, resolves to a directory, and reaches\n * the destination's contents; false otherwise, including every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks `symlinkSync(source, target, 'junction')` creates a directory junction on Windows, which\n * needs no privilege, and Node ignores the type argument off Windows, so one call covers both hosts.\n * The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call\n * probes and cleans up after itself, so a host whose answer changes is read again rather than\n * remembered.\n */\nexport function supportsDirectoryLinks(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-directory-links-'))\n\ttry {\n\t\tconst source = join(directory, 'source')\n\t\tconst link = join(directory, 'link')\n\t\tmkdirSync(source)\n\t\twriteFileSync(join(source, 'marker.txt'), 'marked')\n\t\tsymlinkSync(source, link, 'junction')\n\t\treturn (\n\t\t\tlstatSync(link).isSymbolicLink() &&\n\t\t\tstatSync(link).isDirectory() &&\n\t\t\treadFileSync(join(link, 'marker.txt'), 'utf8') === 'marked'\n\t\t)\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host links a file, by creating one link and reading the file through it.\n *\n * @returns True if the file's contents are readable through the link; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks `symlinkSync(source, target, 'file')` needs the symbolic-link privilege, which Windows\n * grants under Developer Mode or administrator rights and refuses with `EPERM` otherwise, so the\n * answer is true on POSIX and on a privileged Windows host. Where it is false, no mechanism reaches a\n * file through a link and a proof that reads one back cannot run. This is a separate question from\n * {@link supportsDirectoryLinks}, which an unprivileged Windows host answers true through a junction\n * while answering this one false.\n */\nexport function supportsFileLinks(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-file-links-'))\n\ttry {\n\t\tconst source = join(directory, 'source.txt')\n\t\tconst link = join(directory, 'link.txt')\n\t\twriteFileSync(source, 'linked')\n\t\tsymlinkSync(source, link, 'file')\n\t\treturn readFileSync(link, 'utf8') === 'linked'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether POSIX permission bits round-trip through this host's `chmod` and `stat`.\n *\n * @returns True if a directory created with mode `0o700` reports that mode back; false otherwise,\n * including every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks POSIX reports `mode & 0o777 === 0o700` and Windows reports `0o666` regardless, so the\n * answer is true on POSIX and false on Windows. Storing a bit is a narrower question than enforcing\n * it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the\n * bits describe, so a caller that needs a permission to be enforced probes the refusal it needs\n * rather than reading this.\n */\nexport function supportsMode(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-mode-'))\n\ttry {\n\t\tconst path = join(directory, 'moded')\n\t\tmkdirSync(path, { mode: 0o700 })\n\t\treturn (statSync(path).mode & 0o777) === 0o700\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host treats two names differing only by case as distinct files.\n *\n * @returns True if `A` and `a` hold the contents each was written with; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks The names `A` and `a` differ by case and by nothing else, which is what makes the reading\n * an answer about case folding rather than an answer about two unrelated files. A case-folding volume\n * routes the second write onto the first entry, so reading the first back returns the second's\n * contents and the answer is false. The answer is true on a typical POSIX host and false on a\n * case-folding Windows or macOS volume.\n */\nexport function supportsCase(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-case-'))\n\ttry {\n\t\tconst upper = join(directory, 'A')\n\t\tconst lower = join(directory, 'a')\n\t\twriteFileSync(upper, 'upper')\n\t\twriteFileSync(lower, 'lower')\n\t\treturn readFileSync(upper, 'utf8') === 'upper' && readFileSync(lower, 'utf8') === 'lower'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host accepts a filename carrying a raw byte no UTF-8 decoder resolves.\n *\n * @returns True if a name ending in byte `0x80` is written and read back; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows\n * rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed\n * as a `Buffer` because the byte survives no string round trip.\n */\nexport function supportsBytes(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-bytes-'))\n\ttry {\n\t\tconst name = Buffer.concat([Buffer.from(`${directory}${sep}`), Buffer.from([0x80])])\n\t\twriteFileSync(name, 'raw')\n\t\treturn readFileSync(name, 'utf8') === 'raw'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n","import type { Server } from 'node:net'\nimport type {\n\tCookieJarInterface,\n\tLoopbackInterface,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport {\n\tcreateLink,\n\tmatchesIdentity,\n\treadIdentity,\n\tremoveTree,\n\trequireContained,\n} from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocation = readIdentity(statSync(path))\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\tremoveTree(path)\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t\treturn candidate\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tcreateLink(candidate, source)\n\t\t\treturn candidate\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && matchesIdentity(readIdentity(status), allocation)) {\n\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t}\n\t\t\tremoveTree(candidate)\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tif (!matchesIdentity(readIdentity(status), allocation)) return\n\t\t\tremoveTree(path)\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n\n/**\n * Creates a cookie jar that records a real response's cookies and replays them as one header.\n *\n * @returns The rendered request header, and the members that read and capture cookies.\n * @remarks Selection is by name alone: no `Domain` or `Path` matching, no `Expires` or `Secure`\n * handling, and no persistence beyond the jar. That is what a test driving one origin over one path\n * needs, and a fixture needing a browser's cookie store needs a browser rather than this.\n */\nexport function createCookieJar(): CookieJarInterface {\n\tconst cookies = new Map<string, string>()\n\treturn {\n\t\tget header() {\n\t\t\tconst pairs = [...cookies].map(([name, value]) => `${name}=${value}`)\n\t\t\treturn pairs.length === 0 ? undefined : pairs.join('; ')\n\t\t},\n\t\tread(name) {\n\t\t\treturn cookies.get(name)\n\t\t},\n\t\tcapture(response) {\n\t\t\tconst fields = response.headers.getSetCookie()\n\t\t\tfor (const field of fields) {\n\t\t\t\tconst boundary = field.indexOf(';')\n\t\t\t\tconst pair = boundary < 0 ? field : field.slice(0, boundary)\n\t\t\t\tconst separator = pair.indexOf('=')\n\t\t\t\tif (separator < 1) continue\n\n\t\t\t\tconst name = pair.slice(0, separator)\n\t\t\t\t// An origin spells a deletion `Max-Age=0` in whatever case and spacing it likes, so the\n\t\t\t\t// attribute is matched rather than compared.\n\t\t\t\tif (/;\\s*max-age\\s*=\\s*0\\s*(?:;|$)/iu.test(field)) cookies.delete(name)\n\t\t\t\telse cookies.set(name, pair.slice(separator + 1))\n\t\t\t}\n\t\t\treturn fields\n\t\t},\n\t}\n}\n"],"mappings":"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"}
@@ -1,11 +1,13 @@
1
1
  import { Server } from 'node:net';
2
2
  import { Socket } from 'node:net';
3
+ import { Stats } from 'node:fs';
3
4
  import { WaitOptions } from '@orkestrel/test';
4
5
 
5
- /** A name-keyed cookie store a test drives one origin with, filled from real responses. */
6
+ /** Holds a name-keyed cookie store a test drives one origin with, filled from real responses. */
6
7
  export declare interface CookieJarInterface {
7
8
  /**
8
- * The `Cookie` request header naming every stored cookie, or `undefined` while the jar holds none.
9
+ * Reports the `Cookie` request header naming every stored cookie, or `undefined` while the jar
10
+ * holds none.
9
11
  */
10
12
  readonly header: string | undefined;
11
13
  /**
@@ -100,13 +102,14 @@ export declare function createScratch(options?: ScratchOptions): ScratchInterfac
100
102
  */
101
103
  export declare function destroyScratch(scratch: ScratchInterface, options?: WaitOptions): Promise<void>;
102
104
 
103
- /** Options for reading a source inventory. */
105
+ /** Configures a source inventory read. */
104
106
  export declare interface InventoryOptions {
105
- /** The file extensions to include, each written with its leading dot. */
107
+ /** Lists the file extensions to include, each written with its leading dot. */
106
108
  readonly extensions?: readonly string[];
107
109
  /**
108
- * The root-relative path keys to exclude. A key excludes itself and every key below it, matched
109
- * on whole segments, so `excluded` drops `excluded/file.ts` and keeps `excluded-other/file.ts`.
110
+ * Lists the root-relative path keys to exclude. A key excludes itself and every key below it,
111
+ * matched on whole segments, so `excluded` drops `excluded/file.ts` and keeps
112
+ * `excluded-other/file.ts`.
110
113
  */
111
114
  readonly exclude?: readonly string[];
112
115
  }
@@ -116,7 +119,7 @@ export declare interface InventoryOptions {
116
119
  *
117
120
  * @param key - The root-relative key to test.
118
121
  * @param exclusions - The normalized root-relative exclusion keys.
119
- * @returns Whether an exclusion names the key or one of its ancestors.
122
+ * @returns True if an exclusion names the key or one of its ancestors; false otherwise.
120
123
  */
121
124
  export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
122
125
 
@@ -139,14 +142,14 @@ export declare function isExcluded(key: string, exclusions: readonly string[]):
139
142
  */
140
143
  export declare function isRunning(pid: number): boolean;
141
144
 
142
- /** A server a test owns, listening on an ephemeral loopback port until the test releases it. */
145
+ /** Holds a server a test owns, listening on an ephemeral loopback port until the test releases it. */
143
146
  export declare interface LoopbackInterface {
144
147
  /**
145
- * The `http` origin for the assigned port, without a trailing slash. A TLS server answers on the
146
- * same port under `https`.
148
+ * Names the `http` origin for the assigned port, without a trailing slash. A TLS server answers
149
+ * on the same port under `https`.
147
150
  */
148
151
  readonly url: string;
149
- /** The ephemeral port the host assigned. */
152
+ /** Holds the ephemeral port the host assigned. */
150
153
  readonly port: number;
151
154
  /**
152
155
  * Drops every live connection on a server that carries `closeAllConnections`, stops listening, and
@@ -162,13 +165,33 @@ export declare interface LoopbackInterface {
162
165
  *
163
166
  * @param current - The identity read from the path now.
164
167
  * @param allocation - The identity recorded when the directory was allocated.
165
- * @returns Whether the device, the index node, and the creation time all match.
168
+ * @returns True if the device, the index node, and the creation time all match; false otherwise.
166
169
  * @remarks All three fields are compared because none of them alone identifies an allocation. A
167
170
  * device is shared by every directory on one filesystem, an index node is reused once its directory
168
171
  * is removed, and a creation time repeats within the host's timestamp resolution.
169
172
  */
170
173
  export declare function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean;
171
174
 
175
+ /**
176
+ * Reads the `code` an unknown thrown value carries.
177
+ *
178
+ * @param error - The thrown value to read.
179
+ * @returns The string `code` the value carries, or `undefined` when it carries none.
180
+ * @remarks The read is contained on its own terms: a value that is not an object, one carrying no
181
+ * `code`, and one carrying a `code` that is not a string all answer `undefined`. A null-prototype
182
+ * object is read the same way, because the key is tested with `in` rather than through
183
+ * `hasOwnProperty`.
184
+ */
185
+ export declare function readErrorCode(error: unknown): string | undefined;
186
+
187
+ /**
188
+ * Reads the identity of one allocated directory off a host status.
189
+ *
190
+ * @param status - The status read from the directory's path.
191
+ * @returns The device, index node, and creation time that together name the allocation.
192
+ */
193
+ export declare function readIdentity(status: Stats): ScratchIdentity;
194
+
172
195
  /**
173
196
  * Reads files from selected targets below a root directory.
174
197
  *
@@ -185,17 +208,17 @@ export declare function matchesIdentity(current: ScratchIdentity, allocation: Sc
185
208
  export declare function readInventory(root: URL | string, targets: readonly string[], options?: InventoryOptions): Readonly<Record<string, string>>;
186
209
 
187
210
  /**
188
- * The attempts `removeTree` makes before rethrowing a retryable removal error.
211
+ * Caps the attempts `removeTree` makes before rethrowing a retryable removal error.
189
212
  */
190
213
  export declare const REMOVE_TREE_MAX_ATTEMPTS = 10;
191
214
 
192
215
  /**
193
- * The synchronous delay, in milliseconds, `removeTree` waits between attempts.
216
+ * Names the synchronous delay, in milliseconds, `removeTree` waits between attempts.
194
217
  */
195
218
  export declare const REMOVE_TREE_RETRY_DELAY_MS = 100;
196
219
 
197
220
  /**
198
- * The error codes `removeTree` retries; every other code rethrows immediately.
221
+ * Names the error codes `removeTree` retries; every other code rethrows immediately.
199
222
  */
200
223
  export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
201
224
 
@@ -248,6 +271,20 @@ export declare function removeTree(path: string): void;
248
271
  */
249
272
  export declare function requestUpgrade(port: number, options?: UpgradeOptions): Promise<UpgradeResult>;
250
273
 
274
+ /**
275
+ * Resolves a target that stays below a root directory, refusing an escape.
276
+ *
277
+ * @param root - The absolute root directory.
278
+ * @param target - The relative or absolute target to resolve.
279
+ * @returns The absolute target below the root.
280
+ * @throws An `Error` reading `Path outside scratch directory: <target>` when the target escapes the
281
+ * root.
282
+ * @remarks This is {@link resolveContained} with the refusal every contained scratch operation makes
283
+ * of an escape, so the check and its one message are stated once. Read `resolveContained` where an
284
+ * escape is an answer rather than a refusal.
285
+ */
286
+ export declare function requireContained(root: string, target: string): string;
287
+
251
288
  /**
252
289
  * Resolves a target that stays below a root directory.
253
290
  *
@@ -257,19 +294,19 @@ export declare function requestUpgrade(port: number, options?: UpgradeOptions):
257
294
  */
258
295
  export declare function resolveContained(root: string, target: string): string | undefined;
259
296
 
260
- /** The fields that together identify one allocated directory on its host. */
297
+ /** Represents the fields that together identify one allocated directory on its host. */
261
298
  export declare interface ScratchIdentity {
262
- /** The identifier of the device holding the directory. */
299
+ /** Holds the identifier of the device holding the directory. */
263
300
  readonly device: number;
264
- /** The number of the directory's index node on that device. */
301
+ /** Holds the number of the directory's index node on that device. */
265
302
  readonly inode: number;
266
- /** The directory's creation time in milliseconds. */
303
+ /** Holds the directory's creation time in milliseconds. */
267
304
  readonly birth: number;
268
305
  }
269
306
 
270
- /** A temporary directory a test owns, writes into, reads back, and removes when it is done. */
307
+ /** Holds a temporary directory a test owns, writes into, reads back, and removes when it is done. */
271
308
  export declare interface ScratchInterface {
272
- /** The absolute path of the allocated directory. */
309
+ /** Holds the absolute path of the allocated directory. */
273
310
  readonly path: string;
274
311
  /**
275
312
  * Writes a file, creating each parent directory that does not exist.
@@ -296,7 +333,8 @@ export declare interface ScratchInterface {
296
333
  * Reports whether a path exists without following its final symbolic link.
297
334
  *
298
335
  * @param target - A relative or absolute path contained by the scratch directory.
299
- * @returns True when the entry exists, including a symbolic link whose target is missing.
336
+ * @returns True if the entry exists, including a symbolic link whose target is
337
+ * missing; false otherwise.
300
338
  * @throws When the path escapes the scratch directory or its root is a symbolic link or file.
301
339
  */
302
340
  has(target: string): boolean;
@@ -353,23 +391,23 @@ export declare interface ScratchInterface {
353
391
  destroy(): void;
354
392
  }
355
393
 
356
- /** Options for allocating a scratch directory. */
394
+ /** Configures a scratch directory allocation. */
357
395
  export declare interface ScratchOptions {
358
396
  /**
359
- * The existing directory in which to create the allocation. Defaults to the host temporary
397
+ * Names the existing directory in which to create the allocation. Defaults to the host temporary
360
398
  * directory. Allocation throws when this path is missing, a symbolic link, or not a directory.
361
399
  */
362
400
  readonly parent?: string;
363
401
  /**
364
- * The name fragment that starts the generated directory name. Allocation throws when this value
365
- * contains `/` or `\`. Both are refused on every host, so the rule does not vary by host. A
402
+ * Holds the name fragment that starts the generated directory name. Allocation throws when this
403
+ * value contains `/` or `\`. Both are refused on every host, so the rule does not vary by host. A
366
404
  * fragment carrying no separator is one path segment and cannot steer the allocation, so
367
405
  * `release-0..2-` allocates.
368
406
  */
369
407
  readonly prefix?: string;
370
408
  /**
371
- * Files to write on allocation, keyed by path below the scratch directory. Allocation removes its
372
- * directory and rethrows when a key escapes or the host refuses a write.
409
+ * Holds the files to write on allocation, keyed by path below the scratch directory. Allocation
410
+ * removes its directory and rethrows when a key escapes or the host refuses a write.
373
411
  */
374
412
  readonly files?: Readonly<Record<string, string>>;
375
413
  }
@@ -449,23 +487,23 @@ export declare function supportsFileLinks(): boolean;
449
487
  export declare function supportsMode(): boolean;
450
488
 
451
489
  /**
452
- * Options for driving a client upgrade request.
490
+ * Configures a client upgrade request.
453
491
  *
454
492
  * @remarks The time bounds and abort signal bound the wait for the server's answer, so a server
455
493
  * that accepts the connection and never answers ends the call rather than parking it.
456
494
  */
457
495
  export declare interface UpgradeOptions extends WaitOptions {
458
- /** The request path, written with its leading slash. Defaults to `/`. */
496
+ /** Names the request path, written with its leading slash. Defaults to `/`. */
459
497
  readonly path?: string;
460
498
  /**
461
- * The subprotocol tokens the request offers. They are sent as one comma-separated
499
+ * Lists the subprotocol tokens the request offers. They are sent as one comma-separated
462
500
  * `Sec-WebSocket-Protocol` field, and an empty or omitted list sends no field at all.
463
501
  */
464
502
  readonly protocols?: readonly string[];
465
503
  }
466
504
 
467
505
  /**
468
- * What one server did with a client upgrade request.
506
+ * Represents what one server did with a client upgrade request.
469
507
  *
470
508
  * @remarks `claimed` is the discriminant. The claimed arm carries `protocol`, the subprotocol the
471
509
  * server selected, which is `undefined` when it selected none; a claimed upgrade produced no plain