fix(auth): isolate bounded OIDC cleanup
This commit is contained in:
@@ -173,7 +173,7 @@ async function readWithAbort(
|
||||
signal.throwIfAborted();
|
||||
return await new Promise((resolve, reject) => {
|
||||
const aborted = () => {
|
||||
void reader.cancel().catch(() => undefined);
|
||||
cancelReaderBestEffort(reader);
|
||||
reject(signal.reason);
|
||||
};
|
||||
signal.addEventListener("abort", aborted, { once: true });
|
||||
@@ -181,6 +181,14 @@ async function readWithAbort(
|
||||
});
|
||||
}
|
||||
|
||||
function cancelReaderBestEffort(reader: ReadableStreamDefaultReader<Uint8Array>): void {
|
||||
try {
|
||||
void reader.cancel().catch(() => undefined);
|
||||
} catch {
|
||||
// Cancellation is advisory; the bounded fetch timeout/rejection remains authoritative.
|
||||
}
|
||||
}
|
||||
|
||||
async function boundedJwksBody(response: Response, signal: AbortSignal): Promise<string> {
|
||||
if (!response.body) throw new OidcProtocolError();
|
||||
const reader = response.body.getReader();
|
||||
@@ -204,8 +212,11 @@ async function boundedJwksBody(response: Response, signal: AbortSignal): Promise
|
||||
}
|
||||
completed = true;
|
||||
} finally {
|
||||
if (!completed) await reader.cancel().catch(() => undefined);
|
||||
try { reader.releaseLock(); } catch { /* cancellation already made the response unusable */ }
|
||||
try {
|
||||
if (!completed) cancelReaderBestEffort(reader);
|
||||
} finally {
|
||||
try { reader.releaseLock(); } catch { /* cancellation already made the response unusable */ }
|
||||
}
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
try {
|
||||
|
||||
+160
-157
@@ -10,8 +10,8 @@ import {
|
||||
linkSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
opendirSync,
|
||||
readSync,
|
||||
readdirSync,
|
||||
realpathSync,
|
||||
renameSync,
|
||||
unlinkSync,
|
||||
@@ -40,6 +40,8 @@ const MAX_OIDC_SLOT_RECORD_BYTES = 512;
|
||||
const MAX_TTL_MS = 365 * 24 * 60 * 60 * 1000;
|
||||
const OIDC_STATE_TTL_MS = 10 * 60 * 1000;
|
||||
const OIDC_STATE_CAPACITY = 64;
|
||||
const MAX_OIDC_STORAGE_ENTRIES = OIDC_STATE_CAPACITY * 3;
|
||||
const MAX_SESSION_PRUNE_ENTRIES = 512;
|
||||
const TOUCH_INTERVAL_MS = 5 * 60 * 1000;
|
||||
const CSRF_CONTEXT = Buffer.from("thothii-csrf-v1", "utf8");
|
||||
const EMPTY_HKDF_SALT = Buffer.alloc(0);
|
||||
@@ -374,6 +376,33 @@ function storageDirectories(root: string): StorageDirectories {
|
||||
return { root, sessions, oidc };
|
||||
}
|
||||
|
||||
function boundedDirectoryNames(directory: string, maximumEntries: number): string[] {
|
||||
if (!Number.isInteger(maximumEntries) || maximumEntries < 1) throw invalid();
|
||||
let handle: ReturnType<typeof opendirSync> | undefined;
|
||||
try {
|
||||
const before = directoryIdentity(directory);
|
||||
handle = opendirSync(directory);
|
||||
const names: string[] = [];
|
||||
while (names.length <= maximumEntries) {
|
||||
const entry = handle.readSync();
|
||||
if (entry === null) {
|
||||
handle.closeSync();
|
||||
handle = undefined;
|
||||
if (!sameDirectoryIdentity(before, directoryIdentity(directory))) throw invalid();
|
||||
return names;
|
||||
}
|
||||
names.push(entry.name);
|
||||
}
|
||||
throw invalid();
|
||||
} catch {
|
||||
throw invalid();
|
||||
} finally {
|
||||
if (handle !== undefined) {
|
||||
try { handle.closeSync(); } catch { /* the operation is already fail-closed */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openDirectory(directory: string): number | undefined {
|
||||
if (process.platform === "win32") return undefined;
|
||||
return openSync(directory, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0)
|
||||
@@ -814,14 +843,9 @@ export function createFileAuthSessionStore(
|
||||
|
||||
async function oidcStorageEntries(): Promise<string[]> {
|
||||
const entries = process.platform === "win32"
|
||||
? (await requiredWindowsStorage().list(root, "oidc")).map((entry) => entry.name)
|
||||
: (() => {
|
||||
try {
|
||||
return readdirSync(storageDirectories(root).oidc);
|
||||
} catch {
|
||||
throw invalid();
|
||||
}
|
||||
})();
|
||||
? (await requiredWindowsStorage().list(root, "oidc", MAX_OIDC_STORAGE_ENTRIES)).map((entry) => entry.name)
|
||||
: boundedDirectoryNames(storageDirectories(root).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();
|
||||
return entries;
|
||||
@@ -1076,6 +1100,121 @@ export function createFileAuthSessionStore(
|
||||
});
|
||||
}
|
||||
|
||||
async function pruneOidcStates(nowMs: number): Promise<number> {
|
||||
if (process.platform === "win32") {
|
||||
const bridge = requiredWindowsStorage();
|
||||
const oidcEntries = await bridge.list(root, "oidc", MAX_OIDC_STORAGE_ENTRIES);
|
||||
if (oidcEntries.length > MAX_OIDC_STORAGE_ENTRIES) throw invalid();
|
||||
const stateNames = new Set(oidcEntries
|
||||
.filter((entry) => DIGEST_FILENAME_PATTERN.test(entry.name))
|
||||
.map((entry) => entry.name));
|
||||
const claimEntries = new Map(oidcEntries
|
||||
.filter((entry) => CLAIM_FILENAME_PATTERN.test(entry.name))
|
||||
.map((entry) => [entry.name, entry]));
|
||||
const slotEntries = oidcEntries.filter((entry) => oidcSlotIndex(entry.name) !== undefined);
|
||||
if (stateNames.size + claimEntries.size + slotEntries.length !== oidcEntries.length) throw invalid();
|
||||
let removed = 0;
|
||||
for (const filename of stateNames) {
|
||||
const claim = claimFilename(filename);
|
||||
const contents = claimEntries.has(claim)
|
||||
? await bridge.readClaim(root, filename)
|
||||
: await bridge.read(root, "oidc", filename);
|
||||
if (!contents) continue;
|
||||
const record = parseWindowsRecord(contents, MAX_OIDC_STATE_RECORD_BYTES, parseOidcStateRecord);
|
||||
if (oidcStateExpired(record, nowMs)) {
|
||||
const didRemove = claimEntries.has(claim)
|
||||
? await bridge.removeClaim(root, filename)
|
||||
: await bridge.remove(root, "oidc", filename);
|
||||
if (didRemove) removed += 1;
|
||||
}
|
||||
}
|
||||
for (const [claim, entry] of claimEntries) {
|
||||
const filename = `${claim.slice(0, -".claim".length)}.json`;
|
||||
if (stateNames.has(filename)) continue;
|
||||
if (nowMs >= entry.modifiedUnixMs + OIDC_STATE_TTL_MS
|
||||
&& await bridge.remove(root, "oidc", claim)) removed += 1;
|
||||
}
|
||||
for (const entry of slotEntries) {
|
||||
const contents = await bridge.read(root, "oidc", entry.name);
|
||||
if (!contents) continue;
|
||||
const slot = parseWindowsRecord(contents, MAX_OIDC_SLOT_RECORD_BYTES, parseOidcSlotRecord);
|
||||
if (nowMs < Date.parse(slot.expiresAt)) continue;
|
||||
const claim = claimFilename(slot.stateFilename);
|
||||
const stateContents = claimEntries.has(claim)
|
||||
? await bridge.readClaim(root, slot.stateFilename)
|
||||
: await bridge.read(root, "oidc", slot.stateFilename);
|
||||
if (stateContents) {
|
||||
const state = parseWindowsRecord(stateContents, MAX_OIDC_STATE_RECORD_BYTES, parseOidcStateRecord);
|
||||
if (!oidcStateExpired(state, nowMs)) throw invalid();
|
||||
const didRemove = claimEntries.has(claim)
|
||||
? await bridge.removeClaim(root, slot.stateFilename)
|
||||
: await bridge.remove(root, "oidc", slot.stateFilename);
|
||||
if (didRemove) removed += 1;
|
||||
}
|
||||
if (!await bridge.remove(root, "oidc", entry.name)) throw invalid();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
const directory = storageDirectories(root).oidc;
|
||||
const oidcEntries = boundedDirectoryNames(directory, MAX_OIDC_STORAGE_ENTRIES);
|
||||
const stateFilenames = new Set(oidcEntries.filter((entry) => DIGEST_FILENAME_PATTERN.test(entry)));
|
||||
let removed = 0;
|
||||
for (const filename of stateFilenames) {
|
||||
await withLock(lockKey(root, "oidc", filename), async () => {
|
||||
const claim = inspectOidcStateClaim(directory, filename);
|
||||
if (claim === "orphan") return;
|
||||
if (claim) {
|
||||
if (oidcStateExpired(claim.state.value, nowMs)) {
|
||||
removeClaimedOidcState(directory, filename, claim);
|
||||
removed += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const trusted = readTrusted(directory, filename, MAX_OIDC_STATE_RECORD_BYTES, parseOidcStateRecord);
|
||||
if (trusted && oidcStateExpired(trusted.value, nowMs)
|
||||
&& removeTrusted(directory, filename, trusted.identity)) removed += 1;
|
||||
});
|
||||
}
|
||||
for (const claimedFilename of oidcEntries) {
|
||||
if (!CLAIM_FILENAME_PATTERN.test(claimedFilename)) continue;
|
||||
const filename = `${claimedFilename.slice(0, -".claim".length)}.json`;
|
||||
if (stateFilenames.has(filename)) continue;
|
||||
await withLock(lockKey(root, "oidc", filename), async () => {
|
||||
const claim = inspectOidcStateClaim(directory, filename);
|
||||
if (claim !== "orphan") return;
|
||||
const claimIdentity = fileIdentity(lstatSync(filePath(directory, claimedFilename)) as Stats);
|
||||
if (nowMs >= claimIdentity.mtimeMs + OIDC_STATE_TTL_MS
|
||||
&& removeTrusted(directory, claimedFilename, claimIdentity)) removed += 1;
|
||||
});
|
||||
}
|
||||
const slots = await storedOidcSlots(oidcEntries.filter((entry) => oidcSlotIndex(entry) !== undefined));
|
||||
for (const slot of slots) {
|
||||
if (nowMs < Date.parse(slot.record.expiresAt)) continue;
|
||||
await withLock(lockKey(root, "oidc", slot.record.stateFilename), async () => {
|
||||
const claim = inspectOidcStateClaim(directory, slot.record.stateFilename);
|
||||
if (claim && claim !== "orphan") {
|
||||
if (!oidcStateExpired(claim.state.value, nowMs)) throw invalid();
|
||||
removeClaimedOidcState(directory, slot.record.stateFilename, claim);
|
||||
removed += 1;
|
||||
} else if (!claim) {
|
||||
const trusted = readTrusted(
|
||||
directory,
|
||||
slot.record.stateFilename,
|
||||
MAX_OIDC_STATE_RECORD_BYTES,
|
||||
parseOidcStateRecord,
|
||||
);
|
||||
if (trusted) {
|
||||
if (!oidcStateExpired(trusted.value, nowMs)) throw invalid();
|
||||
if (removeTrusted(directory, slot.record.stateFilename, trusted.identity)) removed += 1;
|
||||
}
|
||||
}
|
||||
await removeOidcSlot(slot);
|
||||
});
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
async function createOidcState(input: OidcStateCreateInput, now = new Date()): Promise<CreatedOidcState> {
|
||||
const nowMs = dateMilliseconds(now);
|
||||
let validated: z.infer<typeof oidcStateInputSchema>;
|
||||
@@ -1087,7 +1226,7 @@ export function createFileAuthSessionStore(
|
||||
const expiresMs = nowMs + OIDC_STATE_TTL_MS;
|
||||
if (!Number.isSafeInteger(expiresMs)) throw invalid();
|
||||
return withLock(lockKey(root, "oidc", "capacity"), async () => {
|
||||
await prune(now);
|
||||
await pruneOidcStates(nowMs);
|
||||
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||||
const state = randomBytes(TOKEN_BYTES).toString("base64url");
|
||||
const filename = digestFilename(state);
|
||||
@@ -1147,8 +1286,8 @@ export function createFileAuthSessionStore(
|
||||
const nowMs = dateMilliseconds(now);
|
||||
if (process.platform === "win32") {
|
||||
const bridge = requiredWindowsStorage();
|
||||
const sessionEntries = await bridge.list(root, "sessions");
|
||||
const oidcEntries = await bridge.list(root, "oidc");
|
||||
const sessionEntries = await bridge.list(root, "sessions", MAX_SESSION_PRUNE_ENTRIES);
|
||||
if (sessionEntries.length > MAX_SESSION_PRUNE_ENTRIES) throw invalid();
|
||||
let removed = 0;
|
||||
for (const entry of sessionEntries) {
|
||||
if (!DIGEST_FILENAME_PATTERN.test(entry.name)) throw invalid();
|
||||
@@ -1157,156 +1296,20 @@ export function createFileAuthSessionStore(
|
||||
const record = parseWindowsRecord(contents, MAX_SESSION_RECORD_BYTES, parseSessionRecord);
|
||||
if (sessionExpired(record, nowMs) && await bridge.remove(root, "sessions", entry.name)) removed += 1;
|
||||
}
|
||||
const stateNames = new Set(oidcEntries.filter((entry) => DIGEST_FILENAME_PATTERN.test(entry.name)).map((entry) => entry.name));
|
||||
const claimEntries = new Map(oidcEntries
|
||||
.filter((entry) => CLAIM_FILENAME_PATTERN.test(entry.name))
|
||||
.map((entry) => [entry.name, entry]));
|
||||
const slotEntries = oidcEntries.filter((entry) => oidcSlotIndex(entry.name) !== undefined);
|
||||
if (stateNames.size + claimEntries.size + slotEntries.length !== oidcEntries.length) throw invalid();
|
||||
for (const filename of stateNames) {
|
||||
const claim = claimFilename(filename);
|
||||
const contents = claimEntries.has(claim)
|
||||
? await bridge.readClaim(root, filename)
|
||||
: await bridge.read(root, "oidc", filename);
|
||||
if (!contents) continue;
|
||||
const record = parseWindowsRecord(contents, MAX_OIDC_STATE_RECORD_BYTES, parseOidcStateRecord);
|
||||
if (oidcStateExpired(record, nowMs)) {
|
||||
const didRemove = claimEntries.has(claim)
|
||||
? await bridge.removeClaim(root, filename)
|
||||
: await bridge.remove(root, "oidc", filename);
|
||||
if (didRemove) removed += 1;
|
||||
}
|
||||
}
|
||||
for (const [claim, entry] of claimEntries) {
|
||||
const filename = `${claim.slice(0, -".claim".length)}.json`;
|
||||
if (stateNames.has(filename)) continue;
|
||||
if (nowMs >= entry.modifiedUnixMs + OIDC_STATE_TTL_MS
|
||||
&& await bridge.remove(root, "oidc", claim)) removed += 1;
|
||||
}
|
||||
for (const entry of slotEntries) {
|
||||
const contents = await bridge.read(root, "oidc", entry.name);
|
||||
if (!contents) continue;
|
||||
const slot = parseWindowsRecord(contents, MAX_OIDC_SLOT_RECORD_BYTES, parseOidcSlotRecord);
|
||||
if (nowMs < Date.parse(slot.expiresAt)) continue;
|
||||
const claim = claimFilename(slot.stateFilename);
|
||||
const stateContents = claimEntries.has(claim)
|
||||
? await bridge.readClaim(root, slot.stateFilename)
|
||||
: await bridge.read(root, "oidc", slot.stateFilename);
|
||||
if (stateContents) {
|
||||
const state = parseWindowsRecord(stateContents, MAX_OIDC_STATE_RECORD_BYTES, parseOidcStateRecord);
|
||||
if (!oidcStateExpired(state, nowMs)) throw invalid();
|
||||
const didRemove = claimEntries.has(claim)
|
||||
? await bridge.removeClaim(root, slot.stateFilename)
|
||||
: await bridge.remove(root, "oidc", slot.stateFilename);
|
||||
if (didRemove) removed += 1;
|
||||
}
|
||||
if (!await bridge.remove(root, "oidc", entry.name)) throw invalid();
|
||||
}
|
||||
return removed;
|
||||
return removed + await pruneOidcStates(nowMs);
|
||||
}
|
||||
const directories = storageDirectories(root);
|
||||
let removed = 0;
|
||||
const pruneDirectory = async (
|
||||
directory: string,
|
||||
kind: "sessions" | "oidc",
|
||||
maximumBytes: number,
|
||||
parse: (source: string) => AuthSessionRecord | OidcStateRecord,
|
||||
expired: (record: AuthSessionRecord | OidcStateRecord, timestamp: number) => boolean,
|
||||
): Promise<void> => {
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = readdirSync(directory);
|
||||
} catch {
|
||||
throw invalid();
|
||||
}
|
||||
for (const filename of entries) {
|
||||
if (!DIGEST_FILENAME_PATTERN.test(filename)) continue;
|
||||
await withLock(lockKey(root, kind, filename), async () => {
|
||||
const trusted = readTrusted(directory, filename, maximumBytes, parse);
|
||||
if (trusted && expired(trusted.value, nowMs)) {
|
||||
if (removeTrusted(directory, filename, trusted.identity)) removed += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
await pruneDirectory(
|
||||
directories.sessions,
|
||||
"sessions",
|
||||
MAX_SESSION_RECORD_BYTES,
|
||||
parseSessionRecord,
|
||||
(record, timestamp) => sessionExpired(record as AuthSessionRecord, timestamp),
|
||||
);
|
||||
let oidcEntries: string[];
|
||||
try {
|
||||
oidcEntries = readdirSync(directories.oidc);
|
||||
} catch {
|
||||
throw invalid();
|
||||
}
|
||||
const stateFilenames = new Set(oidcEntries.filter((entry) => DIGEST_FILENAME_PATTERN.test(entry)));
|
||||
for (const filename of stateFilenames) {
|
||||
await withLock(lockKey(root, "oidc", filename), async () => {
|
||||
const claim = inspectOidcStateClaim(directories.oidc, filename);
|
||||
if (claim === "orphan") return;
|
||||
if (claim) {
|
||||
if (oidcStateExpired(claim.state.value, nowMs)) {
|
||||
removeClaimedOidcState(directories.oidc, filename, claim);
|
||||
removed += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const trusted = readTrusted(
|
||||
directories.oidc,
|
||||
filename,
|
||||
MAX_OIDC_STATE_RECORD_BYTES,
|
||||
parseOidcStateRecord,
|
||||
);
|
||||
if (trusted && oidcStateExpired(trusted.value, nowMs)
|
||||
&& removeTrusted(directories.oidc, filename, trusted.identity)) {
|
||||
removed += 1;
|
||||
}
|
||||
const sessionEntries = boundedDirectoryNames(directories.sessions, MAX_SESSION_PRUNE_ENTRIES);
|
||||
for (const filename of sessionEntries) {
|
||||
if (!DIGEST_FILENAME_PATTERN.test(filename)) throw invalid();
|
||||
await withLock(lockKey(root, "sessions", filename), async () => {
|
||||
const trusted = readTrusted(directories.sessions, filename, MAX_SESSION_RECORD_BYTES, parseSessionRecord);
|
||||
if (trusted && sessionExpired(trusted.value, nowMs)
|
||||
&& removeTrusted(directories.sessions, filename, trusted.identity)) removed += 1;
|
||||
});
|
||||
}
|
||||
for (const claimedFilename of oidcEntries) {
|
||||
if (!CLAIM_FILENAME_PATTERN.test(claimedFilename)) continue;
|
||||
const filename = `${claimedFilename.slice(0, -".claim".length)}.json`;
|
||||
if (stateFilenames.has(filename)) continue;
|
||||
await withLock(lockKey(root, "oidc", filename), async () => {
|
||||
const claim = inspectOidcStateClaim(directories.oidc, filename);
|
||||
if (claim !== "orphan") return;
|
||||
const claimIdentity = fileIdentity(lstatSync(filePath(directories.oidc, claimedFilename)) as Stats);
|
||||
// A hard link retains the source mtime. It is therefore a conservative, bounded
|
||||
// expiry marker even if a process crashed between the two unlink operations.
|
||||
if (nowMs >= claimIdentity.mtimeMs + OIDC_STATE_TTL_MS
|
||||
&& removeTrusted(directories.oidc, claimedFilename, claimIdentity)) {
|
||||
removed += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
const slots = await storedOidcSlots(oidcEntries.filter((entry) => oidcSlotIndex(entry) !== undefined));
|
||||
for (const slot of slots) {
|
||||
if (nowMs < Date.parse(slot.record.expiresAt)) continue;
|
||||
await withLock(lockKey(root, "oidc", slot.record.stateFilename), async () => {
|
||||
const claim = inspectOidcStateClaim(directories.oidc, slot.record.stateFilename);
|
||||
if (claim && claim !== "orphan") {
|
||||
if (!oidcStateExpired(claim.state.value, nowMs)) throw invalid();
|
||||
removeClaimedOidcState(directories.oidc, slot.record.stateFilename, claim);
|
||||
removed += 1;
|
||||
} else if (!claim) {
|
||||
const trusted = readTrusted(
|
||||
directories.oidc,
|
||||
slot.record.stateFilename,
|
||||
MAX_OIDC_STATE_RECORD_BYTES,
|
||||
parseOidcStateRecord,
|
||||
);
|
||||
if (trusted) {
|
||||
if (!oidcStateExpired(trusted.value, nowMs)) throw invalid();
|
||||
if (removeTrusted(directories.oidc, slot.record.stateFilename, trusted.identity)) removed += 1;
|
||||
}
|
||||
}
|
||||
await removeOidcSlot(slot);
|
||||
});
|
||||
}
|
||||
return removed;
|
||||
return removed + await pruneOidcStates(nowMs);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,8 @@ const MAX_PROTOCOL_BYTES = 64 * 1024;
|
||||
const MAX_RESPONSE_BYTES = 64 * 1024;
|
||||
const MAX_SESSION_BYTES = 16 * 1024;
|
||||
const MAX_OIDC_BYTES = 8 * 1024;
|
||||
const MAX_ENTRIES = 256;
|
||||
const DEFAULT_MAX_ENTRIES = 256;
|
||||
const MAX_ENTRIES = 512;
|
||||
const TIMEOUT_MS = 5_000;
|
||||
const DIGEST_FILENAME = /^[a-f0-9]{64}\.json$/;
|
||||
const CLAIM_FILENAME = /^[a-f0-9]{64}\.claim$/;
|
||||
@@ -29,7 +30,11 @@ export interface WindowsAuthStorageBridge {
|
||||
read(root: string, directory: WindowsAuthStorageDirectory, filename: string): Promise<Buffer | undefined>;
|
||||
replace(root: string, directory: WindowsAuthStorageDirectory, filename: string, contents: Buffer): Promise<void>;
|
||||
remove(root: string, directory: WindowsAuthStorageDirectory, filename: string): Promise<boolean>;
|
||||
list(root: string, directory: WindowsAuthStorageDirectory): Promise<WindowsAuthStorageEntry[]>;
|
||||
list(
|
||||
root: string,
|
||||
directory: WindowsAuthStorageDirectory,
|
||||
maximumEntries?: number,
|
||||
): Promise<WindowsAuthStorageEntry[]>;
|
||||
claimConsume(root: string, filename: string): Promise<Buffer | undefined>;
|
||||
readClaim(root: string, filename: string): Promise<Buffer | undefined>;
|
||||
removeClaim(root: string, filename: string): Promise<boolean>;
|
||||
@@ -98,6 +103,7 @@ interface BridgeRequest {
|
||||
directory: WindowsAuthStorageDirectory;
|
||||
filename?: string;
|
||||
contentBase64?: string;
|
||||
maximumEntries?: number;
|
||||
}
|
||||
|
||||
function directoryMaximum(directory: WindowsAuthStorageDirectory): number {
|
||||
@@ -151,7 +157,10 @@ function encodedRequest(request: BridgeRequest): Buffer {
|
||||
validateRoot(request.root);
|
||||
if (request.operation === "list") {
|
||||
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();
|
||||
} else {
|
||||
if (request.maximumEntries !== undefined) throw invalid();
|
||||
if (request.filename === undefined) throw invalid();
|
||||
const allowClaim = request.operation === "remove" && request.directory === "oidc";
|
||||
const allowOidcSlot = request.directory === "oidc"
|
||||
@@ -350,9 +359,17 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
|
||||
const response = await request(recordRequest("remove", root, directory, filename));
|
||||
return response.removed === true;
|
||||
},
|
||||
async list(root, directory) {
|
||||
const response = await request({ version: PROTOCOL_VERSION, operation: "list", root, directory });
|
||||
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.entries === undefined) throw invalid();
|
||||
if (response.entries.length > maximumEntries) throw invalid();
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user