fix(auth): close diagnostic filesystem races
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import type { AuthenticationConfigProvider, AuthMode } from "./types.js";
|
||||
import type { LocalUserRegistry } from "./local-registry.js";
|
||||
import { OidcIssuerMismatchError, OidcJwksUnavailableError, type OidcProtocol } from "./oidc-client.js";
|
||||
import { validateAuthSessionRoot } from "./session-store.js";
|
||||
import { createWindowsAuthStorageBridge, type WindowsAuthStorageBridge } from "./windows-auth-storage.js";
|
||||
import {
|
||||
createPosixAuthStorageBridge,
|
||||
createWindowsAuthStorageBridge,
|
||||
type WindowsAuthStorageBridge,
|
||||
} from "./windows-auth-storage.js";
|
||||
import { isUsableAuthenticationSecret, type AuthenticationSecretReference } from "./secret-policy.js";
|
||||
import type { AuthDiagnostic, AuthDiagnosticCode, AuthDiagnostics, GroupCatalog } from "./group-catalog.js";
|
||||
|
||||
@@ -20,6 +23,7 @@ export interface AuthDiagnoserDependencies {
|
||||
/** Platform integrations may inject an equivalent side-effect-free owner/ACL validator. */
|
||||
sessionRootValidator?: (root: string) => void | Promise<void>;
|
||||
windowsStorageBridge?: Pick<WindowsAuthStorageBridge, "validateRoot">;
|
||||
posixStorageBridge?: Pick<WindowsAuthStorageBridge, "validateRoot">;
|
||||
authentication?: AuthenticationConfigProvider;
|
||||
secrets?: ReadonlyMap<string, string>;
|
||||
localUserRegistry?: LocalUserRegistry;
|
||||
@@ -101,7 +105,7 @@ async function localRegistryIsUsable(deps: AuthDiagnoserDependencies): Promise<A
|
||||
export function createAuthDiagnoser(deps: AuthDiagnoserDependencies): AuthDiagnoser {
|
||||
const validateSessionRoot = deps.sessionRootValidator ?? (process.platform === "win32"
|
||||
? (root: string) => (deps.windowsStorageBridge ?? createWindowsAuthStorageBridge()).validateRoot(root)
|
||||
: validateAuthSessionRoot);
|
||||
: (root: string) => (deps.posixStorageBridge ?? createPosixAuthStorageBridge()).validateRoot(root));
|
||||
return {
|
||||
async inspect(options): Promise<AuthDiagnostics> {
|
||||
const checks: AuthDiagnostic[] = [];
|
||||
|
||||
@@ -88,13 +88,24 @@ function discoveryStringList(value: unknown): value is readonly string[] {
|
||||
function schemaValidDiscoveryMetadata(value: unknown): value is Record<string, unknown> & { issuer: string } {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const metadata = value as Record<string, unknown>;
|
||||
return text(metadata.issuer, 2048)
|
||||
&& text(metadata.authorization_endpoint, 2048)
|
||||
&& text(metadata.token_endpoint, 2048)
|
||||
&& text(metadata.jwks_uri, 2048)
|
||||
&& discoveryStringList(metadata.response_types_supported)
|
||||
&& discoveryStringList(metadata.subject_types_supported)
|
||||
&& discoveryStringList(metadata.id_token_signing_alg_values_supported);
|
||||
const issuer = metadata.issuer;
|
||||
const authorizationEndpoint = metadata.authorization_endpoint;
|
||||
const tokenEndpoint = metadata.token_endpoint;
|
||||
const jwksUri = metadata.jwks_uri;
|
||||
if (!text(issuer, 2048) || !text(authorizationEndpoint, 2048)
|
||||
|| !text(tokenEndpoint, 2048) || !text(jwksUri, 2048)
|
||||
|| !discoveryStringList(metadata.response_types_supported)
|
||||
|| !discoveryStringList(metadata.subject_types_supported)
|
||||
|| !discoveryStringList(metadata.id_token_signing_alg_values_supported)) return false;
|
||||
try {
|
||||
configuredHttpsUrl(issuer);
|
||||
httpsEndpoint(authorizationEndpoint);
|
||||
httpsEndpoint(tokenEndpoint);
|
||||
httpsEndpoint(jwksUri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function configuredHttpsUrl(value: string): URL {
|
||||
@@ -526,11 +537,11 @@ export function createOidcProtocol(options: OidcProtocolOptions): OidcProtocol {
|
||||
{ [customFetch]: issuerCheckingFetch, timeout: httpTimeoutMs / 1000 },
|
||||
);
|
||||
const metadata = config.serverMetadata();
|
||||
if (certifiedIssuerMismatch) throw new OidcIssuerMismatchError();
|
||||
if (metadata.issuer !== options.issuer) throw new OidcProtocolError();
|
||||
httpsEndpoint(metadata.authorization_endpoint);
|
||||
httpsEndpoint(metadata.token_endpoint);
|
||||
httpsEndpoint(metadata.jwks_uri);
|
||||
if (certifiedIssuerMismatch) throw new OidcIssuerMismatchError();
|
||||
return config;
|
||||
} catch (error) {
|
||||
throw protocolFailure(error);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
fsyncSync,
|
||||
lstatSync,
|
||||
linkSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
opendirSync,
|
||||
readSync,
|
||||
@@ -29,6 +28,7 @@ import type {
|
||||
Role,
|
||||
} from "./types.js";
|
||||
import {
|
||||
createPosixAuthStorageBridge,
|
||||
createWindowsAuthStorageBridge,
|
||||
type WindowsAuthStorageBridge,
|
||||
} from "./windows-auth-storage.js";
|
||||
@@ -137,9 +137,11 @@ export interface AuthSessionStore {
|
||||
consumeOidcState(state: string, now?: Date): Promise<OidcStateRecord | undefined>;
|
||||
}
|
||||
|
||||
/** Narrow test seam for the native Windows tht-backed storage adaptor. */
|
||||
/** Narrow test seams for the native tht-backed storage adaptors. */
|
||||
export interface FileAuthSessionStoreOptions {
|
||||
windowsStorageBridge?: WindowsAuthStorageBridge;
|
||||
/** Test seam for POSIX layout creation; production uses the bounded hidden tht bridge. */
|
||||
posixStorageBridge?: Pick<WindowsAuthStorageBridge, "ensureLayout">;
|
||||
/** Test-only capacity seam; production always uses the fixed 64-state bound. */
|
||||
oidcStateCapacity?: number;
|
||||
}
|
||||
@@ -297,10 +299,6 @@ function isNotFound(error: unknown): boolean {
|
||||
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
||||
}
|
||||
|
||||
function isAlreadyExists(error: unknown): boolean {
|
||||
return (error as NodeJS.ErrnoException | undefined)?.code === "EEXIST";
|
||||
}
|
||||
|
||||
function canonicalRawValue(value: string): boolean {
|
||||
if (typeof value !== "string" || !TOKEN_PATTERN.test(value)) return false;
|
||||
try {
|
||||
@@ -387,6 +385,7 @@ interface SessionRootAncestor {
|
||||
dev: number;
|
||||
ino: number;
|
||||
uid: number;
|
||||
mode: number;
|
||||
}
|
||||
|
||||
function validateSessionRootSyntax(root: string): void {
|
||||
@@ -396,7 +395,7 @@ function validateSessionRootSyntax(root: string): void {
|
||||
|
||||
function sameAncestor(ancestor: SessionRootAncestor, info: Stats): boolean {
|
||||
return info.isDirectory() && !info.isSymbolicLink() && ancestor.dev === info.dev
|
||||
&& ancestor.ino === info.ino && ancestor.uid === info.uid;
|
||||
&& ancestor.ino === info.ino && ancestor.uid === info.uid && ancestor.mode === info.mode;
|
||||
}
|
||||
|
||||
function withSessionRootPreflight<T>(root: string, use: (exists: boolean) => T): T {
|
||||
@@ -428,11 +427,19 @@ function withSessionRootPreflight<T>(root: string, use: (exists: boolean) => T):
|
||||
const descriptor = openSync(current, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0)
|
||||
| (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
||||
const opened = fstatSync(descriptor) as Stats;
|
||||
if (!opened.isDirectory() || opened.dev !== info.dev || opened.ino !== info.ino || opened.uid !== info.uid) {
|
||||
if (!opened.isDirectory() || opened.dev !== info.dev || opened.ino !== info.ino
|
||||
|| opened.uid !== info.uid || opened.mode !== info.mode) {
|
||||
closeSync(descriptor);
|
||||
throw invalid();
|
||||
}
|
||||
ancestors.push({ path: current, descriptor, dev: info.dev, ino: info.ino, uid: info.uid });
|
||||
ancestors.push({
|
||||
path: current,
|
||||
descriptor,
|
||||
dev: info.dev,
|
||||
ino: info.ino,
|
||||
uid: info.uid,
|
||||
mode: info.mode,
|
||||
});
|
||||
}
|
||||
directoryIdentity(root);
|
||||
const result = use(true);
|
||||
@@ -450,34 +457,16 @@ function withSessionRootPreflight<T>(root: string, use: (exists: boolean) => T):
|
||||
}
|
||||
}
|
||||
|
||||
function privateDirectory(path: string): void {
|
||||
withSessionRootPreflight(path, (exists) => {
|
||||
if (exists) return;
|
||||
try {
|
||||
mkdirSync(path, { recursive: false, mode: PRIVATE_DIRECTORY_MODE });
|
||||
} catch (error) {
|
||||
if (!isAlreadyExists(error)) throw invalid();
|
||||
directoryIdentity(path);
|
||||
return;
|
||||
}
|
||||
const descriptor = openSync(path, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0)
|
||||
| (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
||||
try {
|
||||
fchmodSync(descriptor, PRIVATE_DIRECTORY_MODE);
|
||||
const opened = fstatSync(descriptor) as Stats;
|
||||
const current = directoryIdentity(path);
|
||||
if (opened.dev !== current.dev || opened.ino !== current.ino || opened.uid !== current.uid
|
||||
|| (opened.mode & 0o7777) !== current.mode) throw invalid();
|
||||
} finally {
|
||||
try { closeSync(descriptor); } catch { /* creation already fails closed */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Side-effect-free POSIX validator shared by runtime storage and static diagnostics. */
|
||||
export function validateAuthSessionRoot(root: string): void {
|
||||
try {
|
||||
withSessionRootPreflight(root, () => undefined);
|
||||
const exists = withSessionRootPreflight(root, (currentExists) => currentExists);
|
||||
if (!exists) return;
|
||||
for (const child of ["sessions", "oidc"]) {
|
||||
const path = join(root, child);
|
||||
if (dirname(path) !== root) throw invalid();
|
||||
withSessionRootPreflight(path, () => undefined);
|
||||
}
|
||||
} catch {
|
||||
throw invalid();
|
||||
}
|
||||
@@ -487,12 +476,11 @@ function storageDirectories(root: string): StorageDirectories {
|
||||
// Native Windows calls must dispatch to the tht DACL-capable bridge before reaching this
|
||||
// POSIX-only helper. Keep this guard so an un-routed caller cannot fall back to chmod.
|
||||
validateSessionRootSyntax(root);
|
||||
privateDirectory(root);
|
||||
validateAuthSessionRoot(root);
|
||||
const sessions = join(root, "sessions");
|
||||
const oidc = join(root, "oidc");
|
||||
privateDirectory(sessions);
|
||||
privateDirectory(oidc);
|
||||
directoryIdentity(sessions);
|
||||
directoryIdentity(oidc);
|
||||
return { root, sessions, oidc };
|
||||
}
|
||||
|
||||
@@ -1020,6 +1008,9 @@ export function createFileAuthSessionStore(
|
||||
const windowsStorage = process.platform === "win32"
|
||||
? options.windowsStorageBridge ?? createWindowsAuthStorageBridge()
|
||||
: undefined;
|
||||
const posixStorage = process.platform === "win32"
|
||||
? undefined
|
||||
: options.posixStorageBridge ?? createPosixAuthStorageBridge();
|
||||
let sessionPruneCursor: string | undefined;
|
||||
|
||||
function requiredWindowsStorage(): WindowsAuthStorageBridge {
|
||||
@@ -1027,9 +1018,25 @@ export function createFileAuthSessionStore(
|
||||
return windowsStorage;
|
||||
}
|
||||
|
||||
async function posixStorageDirectories(): Promise<StorageDirectories> {
|
||||
if (process.platform === "win32" || posixStorage === undefined) throw invalid();
|
||||
try {
|
||||
return storageDirectories(root);
|
||||
} catch {
|
||||
// A missing safe layout is the only case the helper can repair. Unsafe layouts are
|
||||
// rejected by the same native primitive without path-based fallback in this process.
|
||||
}
|
||||
try {
|
||||
await posixStorage.ensureLayout(root);
|
||||
return storageDirectories(root);
|
||||
} catch {
|
||||
throw invalid();
|
||||
}
|
||||
}
|
||||
|
||||
async function ordinarySessionPage(after: string | undefined): Promise<SessionDirectoryPage> {
|
||||
if (process.platform !== "win32") {
|
||||
return boundedSessionDirectoryPage(storageDirectories(root).sessions, after, MAX_SESSION_PRUNE_ENTRIES);
|
||||
return boundedSessionDirectoryPage((await posixStorageDirectories()).sessions, after, MAX_SESSION_PRUNE_ENTRIES);
|
||||
}
|
||||
const page = await requiredWindowsStorage().listPage(root, "sessions", after, MAX_SESSION_PRUNE_ENTRIES);
|
||||
if (!page || !Array.isArray(page.entries) || typeof page.more !== "boolean") throw invalid();
|
||||
@@ -1066,7 +1073,7 @@ export function createFileAuthSessionStore(
|
||||
if (sessionExpired(record, nowMs) && await bridge.remove(root, "sessions", filename)) removed += 1;
|
||||
}
|
||||
} else {
|
||||
const directory = storageDirectories(root).sessions;
|
||||
const directory = (await posixStorageDirectories()).sessions;
|
||||
for (const filename of page.entries) {
|
||||
await withLock(lockKey(root, "sessions", filename), async () => {
|
||||
const trusted = readTrusted(directory, filename, MAX_SESSION_RECORD_BYTES, parseSessionRecord);
|
||||
@@ -1082,7 +1089,7 @@ export function createFileAuthSessionStore(
|
||||
async function oidcStorageEntries(): Promise<string[]> {
|
||||
const entries = process.platform === "win32"
|
||||
? (await requiredWindowsStorage().list(root, "oidc", MAX_OIDC_STORAGE_ENTRIES)).map((entry) => entry.name)
|
||||
: boundedDirectoryNames(storageDirectories(root).oidc, MAX_OIDC_STORAGE_ENTRIES);
|
||||
: boundedDirectoryNames((await posixStorageDirectories()).oidc, MAX_OIDC_STORAGE_ENTRIES);
|
||||
if (entries.length > MAX_OIDC_STORAGE_ENTRIES) throw invalid();
|
||||
if (entries.some((entry) => !DIGEST_FILENAME_PATTERN.test(entry)
|
||||
&& !CLAIM_FILENAME_PATTERN.test(entry) && oidcSlotIndex(entry) === undefined)) throw invalid();
|
||||
@@ -1111,7 +1118,7 @@ export function createFileAuthSessionStore(
|
||||
if (retryDelayMs > 0) await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
||||
try {
|
||||
trusted = readTrusted(
|
||||
storageDirectories(root).oidc,
|
||||
(await posixStorageDirectories()).oidc,
|
||||
filename,
|
||||
MAX_OIDC_SLOT_RECORD_BYTES,
|
||||
parseOidcSlotRecord,
|
||||
@@ -1140,7 +1147,7 @@ export function createFileAuthSessionStore(
|
||||
|| !await requiredWindowsStorage().remove(root, "oidc", slot.filename)) throw invalid();
|
||||
return;
|
||||
}
|
||||
if (!slot.identity || !removeTrusted(storageDirectories(root).oidc, slot.filename, slot.identity)) throw invalid();
|
||||
if (!slot.identity || !removeTrusted((await posixStorageDirectories()).oidc, slot.filename, slot.identity)) throw invalid();
|
||||
}
|
||||
|
||||
async function releaseOidcSlot(index: number | undefined, stateFilename: string): Promise<void> {
|
||||
@@ -1172,7 +1179,7 @@ export function createFileAuthSessionStore(
|
||||
const filename = oidcSlotFilename(index);
|
||||
const created = process.platform === "win32"
|
||||
? await requiredWindowsStorage().create(root, "oidc", filename, contents)
|
||||
: writeExclusive(storageDirectories(root).oidc, filename, contents);
|
||||
: writeExclusive((await posixStorageDirectories()).oidc, filename, contents);
|
||||
if (created) return index;
|
||||
}
|
||||
throw new OidcStateCapacityError();
|
||||
@@ -1216,7 +1223,7 @@ export function createFileAuthSessionStore(
|
||||
}
|
||||
throw invalid();
|
||||
}
|
||||
const directories = storageDirectories(root);
|
||||
const directories = await posixStorageDirectories();
|
||||
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||||
const token = randomBytes(TOKEN_BYTES).toString("base64url");
|
||||
const filename = digestFilename(token);
|
||||
@@ -1255,7 +1262,7 @@ export function createFileAuthSessionStore(
|
||||
await bridge.remove(root, "sessions", filename);
|
||||
return undefined;
|
||||
}
|
||||
const directories = storageDirectories(root);
|
||||
const directories = await posixStorageDirectories();
|
||||
const trusted = readTrusted(directories.sessions, filename, MAX_SESSION_RECORD_BYTES, parseSessionRecord);
|
||||
if (!trusted) return undefined;
|
||||
if (sessionExpired(trusted.value, nowMs)) {
|
||||
@@ -1300,7 +1307,7 @@ export function createFileAuthSessionStore(
|
||||
await bridge.replace(root, "sessions", filename, serialize(touched, MAX_SESSION_RECORD_BYTES));
|
||||
return;
|
||||
}
|
||||
const directories = storageDirectories(root);
|
||||
const directories = await posixStorageDirectories();
|
||||
const trusted = readTrusted(directories.sessions, filename, MAX_SESSION_RECORD_BYTES, parseSessionRecord);
|
||||
if (!trusted) return;
|
||||
if (sessionExpired(trusted.value, nowMs)) {
|
||||
@@ -1333,7 +1340,7 @@ export function createFileAuthSessionStore(
|
||||
await requiredWindowsStorage().remove(root, "sessions", filename);
|
||||
return;
|
||||
}
|
||||
const directories = storageDirectories(root);
|
||||
const directories = await posixStorageDirectories();
|
||||
removeTrusted(directories.sessions, filename);
|
||||
});
|
||||
}
|
||||
@@ -1394,7 +1401,7 @@ export function createFileAuthSessionStore(
|
||||
return removed;
|
||||
}
|
||||
|
||||
const directory = storageDirectories(root).oidc;
|
||||
const directory = (await posixStorageDirectories()).oidc;
|
||||
const oidcEntries = boundedDirectoryNames(directory, MAX_OIDC_STORAGE_ENTRIES);
|
||||
const stateFilenames = new Set(oidcEntries.filter((entry) => DIGEST_FILENAME_PATTERN.test(entry)));
|
||||
let removed = 0;
|
||||
@@ -1485,7 +1492,7 @@ export function createFileAuthSessionStore(
|
||||
const contents = serialize(record, MAX_OIDC_STATE_RECORD_BYTES);
|
||||
const created = process.platform === "win32"
|
||||
? await requiredWindowsStorage().create(root, "oidc", filename, contents)
|
||||
: writeExclusive(storageDirectories(root).oidc, filename, contents);
|
||||
: writeExclusive((await posixStorageDirectories()).oidc, filename, contents);
|
||||
if (created) return { state, record };
|
||||
await releaseOidcSlot(capacitySlot, filename);
|
||||
}
|
||||
@@ -1505,7 +1512,7 @@ export function createFileAuthSessionStore(
|
||||
await releaseOidcSlot(record.capacitySlot, filename);
|
||||
return oidcStateExpired(record, nowMs) ? undefined : record;
|
||||
}
|
||||
const directories = storageDirectories(root);
|
||||
const directories = await posixStorageDirectories();
|
||||
const claim = claimOidcState(directories.oidc, filename);
|
||||
// An installed claim belongs to another process/store instance. Only the process which
|
||||
// created the hard link is allowed to receive the record.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { win32 } from "node:path";
|
||||
import { posix, win32 } from "node:path";
|
||||
import type { Readable, Writable } from "node:stream";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface WindowsAuthStoragePage {
|
||||
/** Internal adapter boundary for the file-session store's native Windows path. */
|
||||
export interface WindowsAuthStorageBridge {
|
||||
validateRoot(root: string): Promise<void>;
|
||||
ensureLayout(root: string): Promise<void>;
|
||||
readAuthConfig(path: string): Buffer;
|
||||
create(root: string, directory: WindowsAuthStorageDirectory, filename: string, contents: Buffer): Promise<boolean>;
|
||||
read(root: string, directory: WindowsAuthStorageDirectory, filename: string): Promise<Buffer | undefined>;
|
||||
@@ -100,6 +101,8 @@ export interface WindowsAuthStorageBridgeOptions {
|
||||
beforeInputForTest?: () => Promise<void>;
|
||||
}
|
||||
|
||||
type AuthStoragePathStyle = "posix" | "windows";
|
||||
|
||||
const responseSchema = z.strictObject({
|
||||
version: z.literal(PROTOCOL_VERSION),
|
||||
ok: z.literal(true),
|
||||
@@ -114,13 +117,14 @@ const responseSchema = z.strictObject({
|
||||
})).max(MAX_ENTRIES).optional(),
|
||||
more: z.boolean().optional(),
|
||||
validated: z.boolean().optional(),
|
||||
prepared: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type BridgeResponse = z.infer<typeof responseSchema>;
|
||||
|
||||
interface BridgeRequest {
|
||||
version: typeof PROTOCOL_VERSION;
|
||||
operation: "validate-root" | "read-auth-config" | "create" | "read" | "replace" | "remove" | "list" | "claim-consume" | "read-claim" | "remove-claim";
|
||||
operation: "validate-root" | "ensure-layout" | "read-auth-config" | "create" | "read" | "replace" | "remove" | "list" | "claim-consume" | "read-claim" | "remove-claim";
|
||||
root: string;
|
||||
directory?: WindowsAuthStorageDirectory;
|
||||
filename?: string;
|
||||
@@ -145,9 +149,10 @@ function canonicalBase64(value: string, maximum: number): Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
function validateRoot(root: string): void {
|
||||
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)
|
||||
|| !win32.isAbsolute(root) || win32.normalize(root) !== root) throw invalid();
|
||||
|| !paths.isAbsolute(root) || paths.normalize(root) !== root) throw invalid();
|
||||
}
|
||||
|
||||
function validateFilename(filename: string, allowClaim = false, allowOidcSlot = false): void {
|
||||
@@ -156,11 +161,13 @@ function validateFilename(filename: string, allowClaim = false, allowOidcSlot =
|
||||
&& !(allowOidcSlot && OIDC_SLOT_FILENAME.test(filename)))) throw invalid();
|
||||
}
|
||||
|
||||
function safeThtExecutable(value: string | undefined): string {
|
||||
function safeThtExecutable(value: string | undefined, pathStyle: AuthStoragePathStyle): string {
|
||||
const executable = value ?? process.env.THT_BIN ?? "tht";
|
||||
if (typeof executable !== "string" || executable.length === 0 || /[\u0000-\u001f\u007f]/.test(executable)) throw invalid();
|
||||
if (executable === "tht" || executable === "tht.exe") return executable;
|
||||
if (win32.isAbsolute(executable) && win32.normalize(executable) === executable && /\.exe$/i.test(executable)) return executable;
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -178,9 +185,9 @@ function parseResponse(result: WindowsAuthStorageInvocationResult, maximumOutput
|
||||
}
|
||||
}
|
||||
|
||||
function encodedRequest(request: BridgeRequest): Buffer {
|
||||
validateRoot(request.root);
|
||||
if (request.operation === "validate-root") {
|
||||
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") {
|
||||
@@ -390,8 +397,11 @@ function listedEntries(
|
||||
return response.entries.map((entry) => ({ name: entry.name, modifiedUnixMs: entry.modifiedUnixMs }));
|
||||
}
|
||||
|
||||
export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridgeOptions = {}): WindowsAuthStorageBridge {
|
||||
const executable = safeThtExecutable(options.thtExecutable);
|
||||
function createAuthStorageBridge(
|
||||
pathStyle: AuthStoragePathStyle,
|
||||
options: WindowsAuthStorageBridgeOptions = {},
|
||||
): WindowsAuthStorageBridge {
|
||||
const executable = safeThtExecutable(options.thtExecutable, pathStyle);
|
||||
const invoke = options.invoke ?? ((invocation: WindowsAuthStorageInvocation) => invokeTht(
|
||||
invocation,
|
||||
options.spawnChild,
|
||||
@@ -404,7 +414,7 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
|
||||
const response = await invoke({
|
||||
executable,
|
||||
args: ["_auth-storage"],
|
||||
input: encodedRequest(value),
|
||||
input: encodedRequest(value, pathStyle),
|
||||
timeoutMs: TIMEOUT_MS,
|
||||
maximumOutputBytes,
|
||||
});
|
||||
@@ -418,7 +428,7 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
|
||||
const response = invokeSync({
|
||||
executable,
|
||||
args: ["_auth-storage"],
|
||||
input: encodedRequest(value),
|
||||
input: encodedRequest(value, pathStyle),
|
||||
timeoutMs: TIMEOUT_MS,
|
||||
maximumOutputBytes: MAX_AUTH_CONFIG_RESPONSE_BYTES,
|
||||
});
|
||||
@@ -442,12 +452,18 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
|
||||
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)
|
||||
|| !win32.isAbsolute(path) || win32.normalize(path) !== path) throw invalid();
|
||||
const root = win32.dirname(path);
|
||||
const filename = win32.basename(path);
|
||||
if (!AUTH_CONFIG_FILENAME.test(filename) || win32.join(root, filename) !== path) throw invalid();
|
||||
|| !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);
|
||||
@@ -520,3 +536,12 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user