Files
curriculum-project-hub/hub/src/feishu/fileDelivery.ts
T

61 lines
1.9 KiB
TypeScript

import { isAbsolute, join, relative, resolve, sep } from "node:path";
import {
WorkspaceFileBoundaryError,
readWorkspaceFileNoFollow,
} from "../security/workspaceFiles.js";
export interface DeliverableFile {
readonly path: string;
readonly name: string;
readonly data: Buffer;
}
const DELIVERABLE_CPH_DIRECTORY = "inbox";
export async function resolveDeliverableFile(
requestedPath: string,
workspaceRoot: string,
workspaceDir: string,
maxBytes?: number,
): Promise<DeliverableFile | null> {
const token = requestedPath.trim();
if (token === "") return null;
for (const candidate of candidatePaths(token)) {
assertNotPlatformRuntimePath(candidate, workspaceDir);
try {
return await readWorkspaceFileNoFollow(workspaceRoot, workspaceDir, candidate, maxBytes);
} catch (error) {
if (!(error instanceof WorkspaceFileBoundaryError) || error.reason !== "not_found") {
throw error;
}
// Try the explicit workspace/build fallback for a bare filename. No
// candidate outside the project workspace is ever considered.
}
}
return null;
}
function assertNotPlatformRuntimePath(candidate: string, workspaceDir: string): void {
const workspace = resolve(workspaceDir);
const target = isAbsolute(candidate) ? resolve(candidate) : resolve(workspace, candidate);
const rel = relative(workspace, target);
const components = rel.split(sep);
if (components[0] === ".cph" && components[1] !== DELIVERABLE_CPH_DIRECTORY) {
throw new WorkspaceFileBoundaryError(
`platform runtime files cannot be delivered: ${candidate}`,
candidate,
"boundary",
);
}
}
function candidatePaths(token: string): string[] {
if (isAbsolute(token)) return [token];
const candidates = [token];
if (!token.includes("/") && !token.includes("\\")) {
candidates.push(join("build", token));
}
return [...new Set(candidates)];
}