import { spawn, spawnSync } from "node:child_process"; import { posix, win32 } from "node:path"; import type { Readable, Writable } from "node:stream"; import { z } from "zod"; const PROTOCOL_VERSION = 1; const MAX_PROTOCOL_BYTES = 64 * 1024; const MAX_RESPONSE_BYTES = 64 * 1024; const MAX_AUTH_CONFIG_BYTES = 1024 * 1024; const MAX_AUTH_CONFIG_BASE64_BYTES = 4 * Math.ceil(MAX_AUTH_CONFIG_BYTES / 3); const MAX_AUTH_CONFIG_RESPONSE_BYTES = MAX_AUTH_CONFIG_BASE64_BYTES + 1024; const MAX_SESSION_BYTES = 16 * 1024; const MAX_OIDC_BYTES = 8 * 1024; const DEFAULT_MAX_ENTRIES = 256; const MAX_ENTRIES = 512; const TIMEOUT_MS = 5_000; const TERMINATION_GRACE_MS = 100; const FINAL_SETTLEMENT_MS = 750; const DIGEST_FILENAME = /^[a-f0-9]{64}\.json$/; const CLAIM_FILENAME = /^[a-f0-9]{64}\.claim$/; const OIDC_SLOT_FILENAME = /^slot-(?:[0-5][0-9]|6[0-3])\.json$/; const AUTH_CONFIG_FILENAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,249}\.yaml$/; const invalid = (): Error => new Error("auth_session_store_invalid"); export type WindowsAuthStorageDirectory = "sessions" | "oidc"; export interface WindowsAuthStorageEntry { name: string; modifiedUnixMs: number; } export interface WindowsAuthStoragePage { entries: WindowsAuthStorageEntry[]; more: boolean; } /** Internal adapter boundary for the file-session store's native Windows path. */ export interface WindowsAuthStorageBridge { validateRoot(root: string): Promise; ensureLayout(root: string): Promise; readAuthConfig(path: string): Buffer; readLocalUsers(path: string): Promise; create(root: string, directory: WindowsAuthStorageDirectory, filename: string, contents: Buffer): Promise; read(root: string, directory: WindowsAuthStorageDirectory, filename: string): Promise; replace(root: string, directory: WindowsAuthStorageDirectory, filename: string, contents: Buffer): Promise; remove(root: string, directory: WindowsAuthStorageDirectory, filename: string): Promise; list( root: string, directory: WindowsAuthStorageDirectory, maximumEntries?: number, ): Promise; listPage( root: string, directory: "sessions", afterName: string | undefined, maximumEntries: number, ): Promise; claimConsume(root: string, filename: string): Promise; readClaim(root: string, filename: string): Promise; removeClaim(root: string, filename: string): Promise; } export interface WindowsAuthStorageInvocation { executable: string; args: readonly string[]; input: Buffer; timeoutMs: number; maximumOutputBytes: number; } export interface WindowsAuthStorageInvocationResult { code: number; stdout: Buffer; stderr: Buffer; } interface WindowsAuthStorageChild { readonly stdin: Writable | null; readonly stdout: Readable | null; readonly stderr: Readable | null; kill(signal?: NodeJS.Signals | number): boolean; unref?(): void; on(event: "error", listener: (error: Error) => void): this; once(event: "error", listener: (error: Error) => void): this; once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; removeListener?(event: "error" | "close", listener: (...args: any[]) => void): this; } type WindowsAuthStorageSpawn = ( executable: string, args: readonly string[], options: { shell: false; windowsHide: true; stdio: ["pipe", "pipe", "pipe"]; env: NodeJS.ProcessEnv }, ) => WindowsAuthStorageChild; export interface WindowsAuthStorageBridgeOptions { /** Test-only transport seam. Production always uses the no-shell child-process invocation. */ invoke?: (invocation: WindowsAuthStorageInvocation) => Promise; /** Test-only synchronous seam used by the synchronous authentication-config provider. */ invokeSync?: (invocation: WindowsAuthStorageInvocation) => WindowsAuthStorageInvocationResult; /** Optional configured tht path. Defaults to THT_BIN, then the safe bare command `tht`. */ thtExecutable?: string; /** Test-only child-launch seam; production uses the fixed no-shell Node child-process launcher. */ spawnChild?: WindowsAuthStorageSpawn; /** Test-only input scheduling seam for real child-process lifecycle tests. */ beforeInputForTest?: () => Promise; /** Test-only bounded lifecycle timings. Production always uses the fixed deadlines below. */ deadlinesForTest?: { timeoutMs?: number; terminationGraceMs?: number; finalSettlementMs?: number; }; } type AuthStoragePathStyle = "posix" | "windows"; const responseSchema = z.strictObject({ version: z.literal(PROTOCOL_VERSION), ok: z.literal(true), created: z.boolean().optional(), replaced: z.boolean().optional(), removed: z.boolean().optional(), found: z.boolean().optional(), contentBase64: z.string().max(MAX_AUTH_CONFIG_BASE64_BYTES).optional(), entries: z.array(z.strictObject({ name: z.string().max(128), modifiedUnixMs: z.number().int().safe().nonnegative(), })).max(MAX_ENTRIES).optional(), more: z.boolean().optional(), validated: z.boolean().optional(), prepared: z.boolean().optional(), }); type BridgeResponse = z.infer; interface BridgeRequest { version: typeof PROTOCOL_VERSION; operation: "validate-root" | "ensure-layout" | "read-auth-config" | "read-local-users" | "create" | "read" | "replace" | "remove" | "list" | "claim-consume" | "read-claim" | "remove-claim"; root: string; directory?: WindowsAuthStorageDirectory; filename?: string; contentBase64?: string; maximumEntries?: number; afterName?: string; continuation?: true; } function directoryMaximum(directory: WindowsAuthStorageDirectory): number { return directory === "sessions" ? MAX_SESSION_BYTES : MAX_OIDC_BYTES; } function canonicalBase64(value: string, maximum: number): Buffer { if (typeof value !== "string" || value.length > Math.ceil(maximum / 3) * 4) throw invalid(); try { const decoded = Buffer.from(value, "base64"); if (decoded.length === 0 || decoded.length > maximum || decoded.toString("base64") !== value) throw invalid(); return decoded; } catch { throw invalid(); } } function validateRoot(root: string, pathStyle: AuthStoragePathStyle): void { const paths = pathStyle === "windows" ? win32 : posix; if (typeof root !== "string" || root.length === 0 || /[\u0000-\u001f\u007f]/.test(root) || !paths.isAbsolute(root) || paths.normalize(root) !== root) throw invalid(); } function validateFilename(filename: string, allowClaim = false, allowOidcSlot = false): void { if (typeof filename !== "string" || (!DIGEST_FILENAME.test(filename) && !(allowClaim && CLAIM_FILENAME.test(filename)) && !(allowOidcSlot && OIDC_SLOT_FILENAME.test(filename)))) throw invalid(); } function safeThtExecutable(value: string | undefined, pathStyle: AuthStoragePathStyle): string { const executable = value ?? process.env.THT_AUTH_STORAGE_BIN ?? process.env.THT_BIN ?? "tht"; if (typeof executable !== "string" || executable.length === 0 || /[\u0000-\u001f\u007f]/.test(executable)) throw invalid(); if (executable === "tht" || (pathStyle === "windows" && executable === "tht.exe")) return executable; const paths = pathStyle === "windows" ? win32 : posix; if (paths.isAbsolute(executable) && paths.normalize(executable) === executable && (pathStyle === "posix" || /\.exe$/i.test(executable))) return executable; throw invalid(); } function parseResponse(result: WindowsAuthStorageInvocationResult, maximumOutputBytes: number): BridgeResponse { if (!Number.isInteger(result.code) || result.code !== 0 || !Buffer.isBuffer(result.stdout) || !Buffer.isBuffer(result.stderr) || result.stderr.length > MAX_RESPONSE_BYTES || result.stdout.length === 0 || result.stdout.length > maximumOutputBytes) { throw invalid(); } try { const source = new TextDecoder("utf-8", { fatal: true }).decode(result.stdout); return responseSchema.parse(JSON.parse(source)); } catch { throw invalid(); } } function encodedRequest(request: BridgeRequest, pathStyle: AuthStoragePathStyle): Buffer { validateRoot(request.root, pathStyle); if (request.operation === "validate-root" || request.operation === "ensure-layout") { if (request.directory !== undefined || request.filename !== undefined || request.contentBase64 !== undefined || request.maximumEntries !== undefined || request.afterName !== undefined || request.continuation !== undefined) throw invalid(); } else if (request.operation === "read-auth-config" || request.operation === "read-local-users") { if (request.directory !== undefined || request.contentBase64 !== undefined || request.maximumEntries !== undefined || request.afterName !== undefined || request.continuation !== undefined || request.filename === undefined || !AUTH_CONFIG_FILENAME.test(request.filename)) throw invalid(); } else if (request.operation === "list") { if (request.directory === undefined) throw invalid(); if (request.filename !== undefined || request.contentBase64 !== undefined) throw invalid(); if (request.maximumEntries !== undefined && (!Number.isInteger(request.maximumEntries) || request.maximumEntries < 1 || request.maximumEntries > MAX_ENTRIES)) throw invalid(); if (request.continuation === true) { if (request.directory !== "sessions" || (request.afterName !== undefined && !DIGEST_FILENAME.test(request.afterName))) { throw invalid(); } } else if (request.afterName !== undefined || request.continuation !== undefined) { throw invalid(); } } else { if (request.directory === undefined) throw invalid(); if (request.maximumEntries !== undefined || request.afterName !== undefined || request.continuation !== undefined) throw invalid(); if (request.filename === undefined) throw invalid(); const allowClaim = request.operation === "remove" && request.directory === "oidc"; const allowOidcSlot = request.directory === "oidc" && (request.operation === "create" || request.operation === "read" || request.operation === "remove"); validateFilename(request.filename, allowClaim, allowOidcSlot); if (request.contentBase64 !== undefined && request.operation !== "create" && request.operation !== "replace") throw invalid(); } if ((request.operation === "claim-consume" || request.operation === "read-claim" || request.operation === "remove-claim") && request.directory !== "oidc") throw invalid(); if (request.contentBase64 !== undefined) { if (request.directory === undefined) throw invalid(); canonicalBase64(request.contentBase64, directoryMaximum(request.directory)); } const encoded = Buffer.from(JSON.stringify(request), "utf8"); if (encoded.length === 0 || encoded.length > MAX_PROTOCOL_BYTES) throw invalid(); return encoded; } function environmentForBridge(): NodeJS.ProcessEnv { const path = process.env.PATH; const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT; return { ...(path === undefined ? {} : { PATH: path }), ...(systemRoot === undefined ? {} : { SystemRoot: systemRoot }), }; } const spawnTht: WindowsAuthStorageSpawn = (executable, args, options) => spawn(executable, [...args], options); function invokeThtSync(invocation: WindowsAuthStorageInvocation): WindowsAuthStorageInvocationResult { try { const result = spawnSync(invocation.executable, [...invocation.args], { shell: false, windowsHide: true, env: environmentForBridge(), input: invocation.input, timeout: invocation.timeoutMs, maxBuffer: invocation.maximumOutputBytes, encoding: "buffer", }); if (result.error || result.signal !== null || typeof result.status !== "number" || !Buffer.isBuffer(result.stdout) || !Buffer.isBuffer(result.stderr)) throw invalid(); return { code: result.status, stdout: result.stdout, stderr: result.stderr }; } catch { throw invalid(); } } async function invokeTht( invocation: WindowsAuthStorageInvocation, spawnChild: WindowsAuthStorageSpawn = spawnTht, beforeInputForTest?: () => Promise, terminationGraceMs = TERMINATION_GRACE_MS, finalSettlementMs = FINAL_SETTLEMENT_MS, ): Promise { return new Promise((resolve, reject) => { let settled = false; let aborted = false; let timeout: NodeJS.Timeout | undefined; let terminationTimer: NodeJS.Timeout | undefined; let finalSettlementTimer: NodeJS.Timeout | undefined; let lateErrorReleaseTimer: NodeJS.Timeout | undefined; const stdout: Buffer[] = []; const stderr: Buffer[] = []; let stdoutBytes = 0; let stderrBytes = 0; let child: WindowsAuthStorageChild | undefined; let stdin: Writable | undefined; let stdoutStream: Readable | undefined; let stderrStream: Readable | undefined; const swallowChildError = (): void => undefined; const swallowStreamError = (): void => undefined; const releaseQuarantine = (): void => { if (lateErrorReleaseTimer !== undefined) clearTimeout(lateErrorReleaseTimer); lateErrorReleaseTimer = undefined; removeChildListener("error", swallowChildError); removeChildListener("close", releaseQuarantine); removeStreamListener(stdin, "error", swallowStreamError); removeStreamListener(stdoutStream, "error", swallowStreamError); removeStreamListener(stderrStream, "error", swallowStreamError); }; const quarantineLateErrors = (): void => { // A final-deadline settlement can precede a broken ChildProcess object's terminal events. // Keep only no-capture listeners for a bounded grace period so a late EventEmitter error // cannot become uncaught, including after an already-observed close event. child?.on("error", swallowChildError); child?.once("close", releaseQuarantine); stdin?.on("error", swallowStreamError); stdoutStream?.on("error", swallowStreamError); stderrStream?.on("error", swallowStreamError); lateErrorReleaseTimer = setTimeout(releaseQuarantine, finalSettlementMs); lateErrorReleaseTimer.unref?.(); }; const removeChildListener = (event: "error" | "close", listener: (...args: any[]) => void): void => { try { child?.removeListener?.(event, listener); } catch { /* the helper is already terminal */ } }; const removeStreamListener = (stream: Writable | Readable | undefined, event: "data" | "error", listener: (...args: any[]) => void): void => { try { stream?.removeListener(event, listener); } catch { /* the helper is already terminal */ } }; const stopStream = (stream: Writable | Readable | null | undefined): void => { try { stream?.destroy(); } catch { /* abort is already fail-closed */ } }; const onChildError = (): void => abort(); const onStdinError = (): void => abort(); const onStdoutError = (): void => abort(); const onStderrError = (): void => abort(); const onStdoutData = (chunk: Buffer): void => { if (aborted || settled) return; stdoutBytes += chunk.length; if (stdoutBytes > invocation.maximumOutputBytes) { abort(); return; } stdout.push(Buffer.from(chunk)); }; const onStderrData = (chunk: Buffer): void => { if (aborted || settled) return; stderrBytes += chunk.length; if (stderrBytes > MAX_RESPONSE_BYTES) { abort(); return; } stderr.push(Buffer.from(chunk)); }; const onClose = (code: number | null, signal: NodeJS.Signals | null): void => { if (settled) return; if (aborted || code === null || !Number.isInteger(code) || signal !== null) { settle(() => reject(invalid()), true); return; } settle(() => resolve({ code, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr), })); }; const cleanup = (quarantine = false): void => { if (timeout !== undefined) clearTimeout(timeout); if (terminationTimer !== undefined) clearTimeout(terminationTimer); if (finalSettlementTimer !== undefined) clearTimeout(finalSettlementTimer); removeChildListener("error", onChildError); removeChildListener("close", onClose as (...args: any[]) => void); removeStreamListener(stdin, "error", onStdinError); removeStreamListener(stdoutStream, "data", onStdoutData); removeStreamListener(stdoutStream, "error", onStdoutError); removeStreamListener(stderrStream, "data", onStderrData); removeStreamListener(stderrStream, "error", onStderrError); if (quarantine) quarantineLateErrors(); }; const settle = (callback: () => void, quarantine = false): void => { if (settled) return; settled = true; cleanup(quarantine); callback(); }; const abort = (): void => { if (aborted || settled) return; aborted = true; if (timeout !== undefined) clearTimeout(timeout); if (child !== undefined) { stopStream(stdin); stopStream(stdoutStream); stopStream(stderrStream); try { child.kill("SIGTERM"); } catch { /* final settlement still owns completion */ } try { child.unref?.(); } catch { /* the bounded timers still own completion */ } } terminationTimer = setTimeout(() => { if (settled || child === undefined) return; try { child.kill("SIGKILL"); } catch { /* final settlement still owns completion */ } }, terminationGraceMs); finalSettlementTimer = setTimeout(() => { settle(() => reject(invalid()), true); }, finalSettlementMs); }; try { child = spawnChild(invocation.executable, invocation.args, { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], env: environmentForBridge(), }); } catch { settle(() => reject(invalid())); return; } child.once("close", onClose); child.on("error", onChildError); if (!child.stdin || !child.stdout || !child.stderr) { abort(); return; } stdin = child.stdin; stdoutStream = child.stdout; stderrStream = child.stderr; timeout = setTimeout(() => { abort(); }, invocation.timeoutMs); stdoutStream.on("data", onStdoutData); stdoutStream.once("error", onStdoutError); stderrStream.on("data", onStderrData); stderrStream.once("error", onStderrError); stdin.once("error", onStdinError); const writeInput = (): void => { if (aborted || settled) return; try { stdin.end(invocation.input); } catch { abort(); } }; if (beforeInputForTest === undefined) { writeInput(); } else { void Promise.resolve().then(beforeInputForTest).then(writeInput, abort); } }); } function contentFrom(response: BridgeResponse, maximum: number): Buffer | undefined { if (response.found !== true) { if (response.contentBase64 !== undefined) throw invalid(); return undefined; } if (response.contentBase64 === undefined) throw invalid(); return canonicalBase64(response.contentBase64, maximum); } function listedEntries( response: BridgeResponse, directory: WindowsAuthStorageDirectory, maximumEntries: number, ): WindowsAuthStorageEntry[] { if (response.entries === undefined || response.entries.length > maximumEntries) throw invalid(); const names = new Set(); for (const entry of response.entries) { if (!DIGEST_FILENAME.test(entry.name) && !(directory === "oidc" && (CLAIM_FILENAME.test(entry.name) || OIDC_SLOT_FILENAME.test(entry.name)))) throw invalid(); if (names.has(entry.name)) throw invalid(); names.add(entry.name); } return response.entries.map((entry) => ({ name: entry.name, modifiedUnixMs: entry.modifiedUnixMs })); } function createAuthStorageBridge( pathStyle: AuthStoragePathStyle, options: WindowsAuthStorageBridgeOptions = {}, ): WindowsAuthStorageBridge { const executable = safeThtExecutable(options.thtExecutable, pathStyle); const testDeadlines = options.deadlinesForTest; const timeoutMs = testDeadlines?.timeoutMs ?? TIMEOUT_MS; const terminationGraceMs = testDeadlines?.terminationGraceMs ?? TERMINATION_GRACE_MS; const finalSettlementMs = testDeadlines?.finalSettlementMs ?? FINAL_SETTLEMENT_MS; if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > TIMEOUT_MS || !Number.isSafeInteger(terminationGraceMs) || terminationGraceMs < 1 || terminationGraceMs > TIMEOUT_MS || !Number.isSafeInteger(finalSettlementMs) || finalSettlementMs <= terminationGraceMs || finalSettlementMs > TIMEOUT_MS) { throw invalid(); } const invoke = options.invoke ?? ((invocation: WindowsAuthStorageInvocation) => invokeTht( invocation, options.spawnChild, options.beforeInputForTest, terminationGraceMs, finalSettlementMs, )); const invokeSync = options.invokeSync ?? invokeThtSync; const request = async (value: BridgeRequest): Promise => { try { const maximumOutputBytes = value.operation === "read-local-users" ? MAX_AUTH_CONFIG_RESPONSE_BYTES : MAX_RESPONSE_BYTES; const response = await invoke({ executable, args: ["_auth-storage"], input: encodedRequest(value, pathStyle), timeoutMs, maximumOutputBytes, }); return parseResponse(response, maximumOutputBytes); } catch { throw invalid(); } }; const syncRequest = (value: BridgeRequest): BridgeResponse => { try { const response = invokeSync({ executable, args: ["_auth-storage"], input: encodedRequest(value, pathStyle), timeoutMs, maximumOutputBytes: MAX_AUTH_CONFIG_RESPONSE_BYTES, }); return parseResponse(response, MAX_AUTH_CONFIG_RESPONSE_BYTES); } catch { throw invalid(); } }; const recordRequest = (operation: "create" | "read" | "replace" | "remove" | "claim-consume" | "read-claim" | "remove-claim", root: string, directory: WindowsAuthStorageDirectory, filename: string, contents?: Buffer): BridgeRequest => ({ version: PROTOCOL_VERSION, operation, root, directory, filename, ...(contents === undefined ? {} : { contentBase64: contents.toString("base64") }), }); return { async validateRoot(root) { const response = await request({ version: PROTOCOL_VERSION, operation: "validate-root", root }); if (response.validated !== true || Object.keys(response).some((key) => !["version", "ok", "validated"].includes(key))) throw invalid(); }, async ensureLayout(root) { const response = await request({ version: PROTOCOL_VERSION, operation: "ensure-layout", root }); if (response.prepared !== true || Object.keys(response).some((key) => !["version", "ok", "prepared"].includes(key))) throw invalid(); }, readAuthConfig(path) { const paths = pathStyle === "windows" ? win32 : posix; if (typeof path !== "string" || path.length === 0 || /[\u0000-\u001f\u007f]/.test(path) || !paths.isAbsolute(path) || paths.normalize(path) !== path) throw invalid(); const root = paths.dirname(path); const filename = paths.basename(path); if (!AUTH_CONFIG_FILENAME.test(filename) || paths.join(root, filename) !== path) throw invalid(); const response = syncRequest({ version: PROTOCOL_VERSION, operation: "read-auth-config", root, filename }); if (Object.keys(response).some((key) => !["version", "ok", "found", "contentBase64"].includes(key))) throw invalid(); const contents = contentFrom(response, MAX_AUTH_CONFIG_BYTES); if (contents === undefined) throw invalid(); return contents; }, async readLocalUsers(path) { const paths = pathStyle === "windows" ? win32 : posix; if (typeof path !== "string" || path.length === 0 || /[\u0000-\u001f\u007f]/.test(path) || !paths.isAbsolute(path) || paths.normalize(path) !== path) throw invalid(); const root = paths.dirname(path); const filename = paths.basename(path); if (!AUTH_CONFIG_FILENAME.test(filename) || paths.join(root, filename) !== path) throw invalid(); const response = await request({ version: PROTOCOL_VERSION, operation: "read-local-users", root, filename }); if (Object.keys(response).some((key) => !["version", "ok", "found", "contentBase64"].includes(key))) throw invalid(); const contents = contentFrom(response, MAX_AUTH_CONFIG_BYTES); if (contents === undefined) throw invalid(); return contents; }, async create(root, directory, filename, contents) { if (!Buffer.isBuffer(contents) || contents.length === 0 || contents.length > directoryMaximum(directory)) throw invalid(); const response = await request(recordRequest("create", root, directory, filename, contents)); if (response.created === undefined) throw invalid(); return response.created; }, async read(root, directory, filename) { return contentFrom(await request(recordRequest("read", root, directory, filename)), directoryMaximum(directory)); }, async replace(root, directory, filename, contents) { if (!Buffer.isBuffer(contents) || contents.length === 0 || contents.length > directoryMaximum(directory)) throw invalid(); const response = await request(recordRequest("replace", root, directory, filename, contents)); if (response.replaced !== true) throw invalid(); }, async remove(root, directory, filename) { const response = await request(recordRequest("remove", root, directory, filename)); return response.removed === true; }, async list(root, directory, maximumEntries = DEFAULT_MAX_ENTRIES) { if (!Number.isInteger(maximumEntries) || maximumEntries < 1 || maximumEntries > MAX_ENTRIES) throw invalid(); const response = await request({ version: PROTOCOL_VERSION, operation: "list", root, directory, maximumEntries, }); if (response.more !== undefined) throw invalid(); return listedEntries(response, directory, maximumEntries); }, async listPage(root, directory, afterName, maximumEntries) { if (directory !== "sessions" || !Number.isInteger(maximumEntries) || maximumEntries < 1 || maximumEntries > MAX_ENTRIES || (afterName !== undefined && !DIGEST_FILENAME.test(afterName))) throw invalid(); const response = await request({ version: PROTOCOL_VERSION, operation: "list", root, directory, maximumEntries, continuation: true, ...(afterName === undefined ? {} : { afterName }), }); if (response.more === undefined) throw invalid(); const entries = listedEntries(response, directory, maximumEntries); let previous = afterName; for (const entry of entries) { if (previous !== undefined && entry.name <= previous) throw invalid(); previous = entry.name; } if (response.more && entries.length !== maximumEntries) throw invalid(); if (response.more && (previous === undefined || previous === afterName)) throw invalid(); return { entries, more: response.more }; }, async claimConsume(root, filename) { return contentFrom(await request(recordRequest("claim-consume", root, "oidc", filename)), MAX_OIDC_BYTES); }, async readClaim(root, filename) { return contentFrom(await request(recordRequest("read-claim", root, "oidc", filename)), MAX_OIDC_BYTES); }, async removeClaim(root, filename) { const response = await request(recordRequest("remove-claim", root, "oidc", filename)); return response.removed === true; }, }; } export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridgeOptions = {}): WindowsAuthStorageBridge { return createAuthStorageBridge("windows", options); } /** POSIX uses the same single hidden tht protocol and bounds, with native canonical path rules. */ export function createPosixAuthStorageBridge(options: WindowsAuthStorageBridgeOptions = {}): WindowsAuthStorageBridge { return createAuthStorageBridge("posix", options); }