mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
chore(duplication): share auth zip extractors (#5475)
This commit is contained in:
@@ -1,89 +1,5 @@
|
||||
import path from "path";
|
||||
import { unzipSync, type Unzipped } from "fflate";
|
||||
|
||||
export interface ExtractedZipFile {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ExtractZipOptions {
|
||||
maxFiles?: number;
|
||||
maxFileSizeBytes?: number;
|
||||
maxTotalSizeBytes?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_FILES = 50;
|
||||
const DEFAULT_MAX_FILE_SIZE = 256 * 1024;
|
||||
const DEFAULT_MAX_TOTAL = 10 * 1024 * 1024;
|
||||
|
||||
function isSafeEntryName(name: string): boolean {
|
||||
if (!name.toLowerCase().endsWith(".json")) return false;
|
||||
if (name.includes("..")) return false;
|
||||
if (path.isAbsolute(name)) return false;
|
||||
if (/[\r\n\0]/.test(name)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function extractClaudeAuthZip(
|
||||
zipBuffer: Buffer,
|
||||
options: ExtractZipOptions = {}
|
||||
): ExtractedZipFile[] {
|
||||
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
||||
const maxFileSize = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE;
|
||||
const maxTotal = options.maxTotalSizeBytes ?? DEFAULT_MAX_TOTAL;
|
||||
|
||||
let unzipped: Unzipped;
|
||||
try {
|
||||
unzipped = unzipSync(new Uint8Array(zipBuffer));
|
||||
} catch {
|
||||
throw new Error("Could not parse ZIP archive — file may be corrupt or not a valid ZIP");
|
||||
}
|
||||
|
||||
const entries = Object.entries(unzipped).filter(([, data]) => data !== undefined);
|
||||
const jsonEntries = entries.filter(([name]) => name.toLowerCase().endsWith(".json"));
|
||||
|
||||
if (jsonEntries.length === 0) {
|
||||
throw new Error("ZIP archive contains no .json files");
|
||||
}
|
||||
|
||||
if (jsonEntries.length > maxFiles) {
|
||||
throw new Error(
|
||||
`ZIP archive contains ${jsonEntries.length} .json files — max allowed is ${maxFiles}`
|
||||
);
|
||||
}
|
||||
|
||||
let totalBytes = 0;
|
||||
const result: ExtractedZipFile[] = [];
|
||||
|
||||
for (const [entryName, data] of jsonEntries) {
|
||||
const baseName = path.basename(entryName);
|
||||
|
||||
if (!isSafeEntryName(baseName)) {
|
||||
throw new Error(
|
||||
`ZIP entry "${baseName}" has an unsafe filename (must be a .json file without path traversal)`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isSafeEntryName(entryName)) {
|
||||
throw new Error(
|
||||
`ZIP entry path "${entryName}" is unsafe (no "..", absolute paths, or control characters allowed)`
|
||||
);
|
||||
}
|
||||
|
||||
if (data.byteLength > maxFileSize) {
|
||||
throw new Error(
|
||||
`ZIP entry "${baseName}" is ${data.byteLength} bytes — exceeds ${maxFileSize} byte limit per file`
|
||||
);
|
||||
}
|
||||
|
||||
totalBytes += data.byteLength;
|
||||
if (totalBytes > maxTotal) {
|
||||
throw new Error(`ZIP archive total uncompressed size exceeds ${maxTotal} byte limit`);
|
||||
}
|
||||
|
||||
const content = new TextDecoder("utf-8").decode(data);
|
||||
result.push({ name: baseName, content });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
export {
|
||||
extractJsonZip as extractClaudeAuthZip,
|
||||
type ExtractedZipFile,
|
||||
type ExtractZipOptions,
|
||||
} from "@/lib/oauth/utils/jsonZipExtract";
|
||||
|
||||
@@ -1,89 +1,5 @@
|
||||
import path from "path";
|
||||
import { unzipSync, type Unzipped } from "fflate";
|
||||
|
||||
export interface ExtractedZipFile {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ExtractZipOptions {
|
||||
maxFiles?: number;
|
||||
maxFileSizeBytes?: number;
|
||||
maxTotalSizeBytes?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_FILES = 50;
|
||||
const DEFAULT_MAX_FILE_SIZE = 256 * 1024;
|
||||
const DEFAULT_MAX_TOTAL = 10 * 1024 * 1024;
|
||||
|
||||
function isSafeEntryName(name: string): boolean {
|
||||
if (!name.toLowerCase().endsWith(".json")) return false;
|
||||
if (name.includes("..")) return false;
|
||||
if (path.isAbsolute(name)) return false;
|
||||
if (/[\r\n\0]/.test(name)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function extractCodexAuthZip(
|
||||
zipBuffer: Buffer,
|
||||
options: ExtractZipOptions = {}
|
||||
): ExtractedZipFile[] {
|
||||
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
||||
const maxFileSize = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE;
|
||||
const maxTotal = options.maxTotalSizeBytes ?? DEFAULT_MAX_TOTAL;
|
||||
|
||||
let unzipped: Unzipped;
|
||||
try {
|
||||
unzipped = unzipSync(new Uint8Array(zipBuffer));
|
||||
} catch {
|
||||
throw new Error("Could not parse ZIP archive — file may be corrupt or not a valid ZIP");
|
||||
}
|
||||
|
||||
const entries = Object.entries(unzipped).filter(([, data]) => data !== undefined);
|
||||
const jsonEntries = entries.filter(([name]) => name.toLowerCase().endsWith(".json"));
|
||||
|
||||
if (jsonEntries.length === 0) {
|
||||
throw new Error("ZIP archive contains no .json files");
|
||||
}
|
||||
|
||||
if (jsonEntries.length > maxFiles) {
|
||||
throw new Error(
|
||||
`ZIP archive contains ${jsonEntries.length} .json files — max allowed is ${maxFiles}`
|
||||
);
|
||||
}
|
||||
|
||||
let totalBytes = 0;
|
||||
const result: ExtractedZipFile[] = [];
|
||||
|
||||
for (const [entryName, data] of jsonEntries) {
|
||||
const baseName = path.basename(entryName);
|
||||
|
||||
if (!isSafeEntryName(baseName)) {
|
||||
throw new Error(
|
||||
`ZIP entry "${baseName}" has an unsafe filename (must be a .json file without path traversal)`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isSafeEntryName(entryName)) {
|
||||
throw new Error(
|
||||
`ZIP entry path "${entryName}" is unsafe (no "..", absolute paths, or control characters allowed)`
|
||||
);
|
||||
}
|
||||
|
||||
if (data.byteLength > maxFileSize) {
|
||||
throw new Error(
|
||||
`ZIP entry "${baseName}" is ${data.byteLength} bytes — exceeds ${maxFileSize} byte limit per file`
|
||||
);
|
||||
}
|
||||
|
||||
totalBytes += data.byteLength;
|
||||
if (totalBytes > maxTotal) {
|
||||
throw new Error(`ZIP archive total uncompressed size exceeds ${maxTotal} byte limit`);
|
||||
}
|
||||
|
||||
const content = new TextDecoder("utf-8").decode(data);
|
||||
result.push({ name: baseName, content });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
export {
|
||||
extractJsonZip as extractCodexAuthZip,
|
||||
type ExtractedZipFile,
|
||||
type ExtractZipOptions,
|
||||
} from "@/lib/oauth/utils/jsonZipExtract";
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { zipSync, strToU8 } from "fflate";
|
||||
|
||||
// Mirror the safety logic from codexAuthZipExtract.ts so we can test without
|
||||
// importing the module (which is fine since it has no external DB deps, but
|
||||
// we test the pure logic to keep the test surface clear).
|
||||
|
||||
interface ExtractedZipFile {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface ExtractZipOptions {
|
||||
maxFiles?: number;
|
||||
maxFileSizeBytes?: number;
|
||||
maxTotalSizeBytes?: number;
|
||||
}
|
||||
|
||||
// Local re-implementation of the exported function to exercise it without
|
||||
// importing Node-only code in the test runner.
|
||||
import { extractCodexAuthZip } from "../../src/lib/oauth/utils/codexAuthZipExtract.ts";
|
||||
import { extractClaudeAuthZip } from "../../src/lib/oauth/utils/claudeAuthZipExtract.ts";
|
||||
import { extractJsonZip } from "../../src/lib/oauth/utils/jsonZipExtract.ts";
|
||||
|
||||
// ──── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -47,6 +31,17 @@ test("extractCodexAuthZip: happy path — returns all .json entries", () => {
|
||||
assert.deepEqual(names, ["auth-a.json", "auth-b.json", "auth-c.json"]);
|
||||
});
|
||||
|
||||
test("auth ZIP extractors share the generic JSON ZIP behavior", () => {
|
||||
const zip = makeZip({
|
||||
"auth-a.json": VALID_AUTH,
|
||||
"nested/auth-b.json": VALID_AUTH,
|
||||
"README.md": "# Readme",
|
||||
});
|
||||
|
||||
assert.deepEqual(extractCodexAuthZip(zip), extractJsonZip(zip));
|
||||
assert.deepEqual(extractClaudeAuthZip(zip), extractJsonZip(zip));
|
||||
});
|
||||
|
||||
test("extractCodexAuthZip: ignores non-.json entries", () => {
|
||||
const zip = makeZip({
|
||||
"auth-a.json": VALID_AUTH,
|
||||
|
||||
Reference in New Issue
Block a user