Archived
feat: add safe manual workspace uploads
This commit is contained in:
@@ -2,7 +2,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { loadEffectiveProjectPathAccess, loadProjectPiWebConfig, mergePathAccessConfigs, PROJECT_PI_WEB_CONFIG_PATH } from "./projectPiWebConfig.js";
|
||||
import { loadEffectiveProjectPathAccess, loadEffectiveProjectUploadsConfig, loadProjectPiWebConfig, mergePathAccessConfigs, PROJECT_PI_WEB_CONFIG_PATH } from "./projectPiWebConfig.js";
|
||||
|
||||
let tempDir: string;
|
||||
let projectPath: string;
|
||||
@@ -26,13 +26,13 @@ describe("project PI WEB config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("loads project-local path access config", async () => {
|
||||
await writeProjectConfig({ version: 1, pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] } });
|
||||
it("loads project-local path access and upload config", async () => {
|
||||
await writeProjectConfig({ version: 1, pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] }, uploads: { defaultFolder: "manual\\incoming" } });
|
||||
|
||||
await expect(loadProjectPiWebConfig(projectPath)).resolves.toEqual({
|
||||
path: join(projectPath, PROJECT_PI_WEB_CONFIG_PATH),
|
||||
exists: true,
|
||||
config: { version: 1, pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] } },
|
||||
config: { version: 1, pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] }, uploads: { defaultFolder: "manual/incoming" } },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,12 @@ describe("project PI WEB config", () => {
|
||||
await expect(loadProjectPiWebConfig(projectPath)).rejects.toThrow("PI WEB config pathAccess.allowedPaths must be an array of non-empty strings");
|
||||
});
|
||||
|
||||
it("reuses PI WEB upload schema validation", async () => {
|
||||
await writeProjectConfig({ version: 1, uploads: { defaultFolder: "../outside" } });
|
||||
|
||||
await expect(loadProjectPiWebConfig(projectPath)).rejects.toThrow("PI WEB config uploads.defaultFolder must not contain path traversal");
|
||||
});
|
||||
|
||||
it("merges global and project path access in order", async () => {
|
||||
await writeProjectConfig({ version: 1, pathAccess: { allowedPaths: ["/project-sdk", "/shared"] } });
|
||||
|
||||
@@ -55,6 +61,14 @@ describe("project PI WEB config", () => {
|
||||
allowedPaths: ["/global-sdk", "/shared", "/project-sdk"],
|
||||
});
|
||||
});
|
||||
|
||||
it("lets project upload defaults override global upload defaults", async () => {
|
||||
await writeProjectConfig({ version: 1, uploads: { defaultFolder: "project-uploads" } });
|
||||
|
||||
await expect(loadEffectiveProjectUploadsConfig(projectPath, { uploads: { defaultFolder: "global-uploads" } })).resolves.toEqual({
|
||||
defaultFolder: "project-uploads",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergePathAccessConfigs", () => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { parsePathAccessConfig, type PiWebConfig } from "../../config.js";
|
||||
import type { PiWebPathAccessConfig } from "../../shared/apiTypes.js";
|
||||
import { effectiveUploadsConfig, parsePathAccessConfig, parseUploadsConfig, type PiWebConfig } from "../../config.js";
|
||||
import type { PiWebPathAccessConfig, PiWebUploadsConfig } from "../../shared/apiTypes.js";
|
||||
|
||||
export const PROJECT_PI_WEB_CONFIG_PATH = ".pi-web/config.json";
|
||||
|
||||
export interface ProjectPiWebConfig {
|
||||
version?: 1;
|
||||
pathAccess?: PiWebPathAccessConfig;
|
||||
uploads?: PiWebUploadsConfig;
|
||||
}
|
||||
|
||||
export interface LoadedProjectPiWebConfig {
|
||||
@@ -33,6 +34,11 @@ export async function loadEffectiveProjectPathAccess(projectPath: string, global
|
||||
return mergePathAccessConfigs(globalConfig.pathAccess, projectConfig.config.pathAccess);
|
||||
}
|
||||
|
||||
export async function loadEffectiveProjectUploadsConfig(projectPath: string, globalConfig: PiWebConfig): Promise<PiWebUploadsConfig> {
|
||||
const projectConfig = await loadProjectPiWebConfig(projectPath);
|
||||
return effectiveUploadsConfig({ uploads: { ...(globalConfig.uploads ?? {}), ...(projectConfig.config.uploads ?? {}) } });
|
||||
}
|
||||
|
||||
export function mergePathAccessConfigs(...configs: (PiWebPathAccessConfig | undefined)[]): PiWebPathAccessConfig | undefined {
|
||||
const allowedPaths = dedupe(configs.flatMap((config) => config?.allowedPaths ?? []));
|
||||
return allowedPaths.length === 0 ? undefined : { allowedPaths };
|
||||
@@ -43,6 +49,7 @@ function parseProjectPiWebConfig(value: Record<string, unknown>, path: string):
|
||||
return {
|
||||
...(version !== undefined ? { version: parseProjectConfigVersion(version, path) } : {}),
|
||||
...(value["pathAccess"] !== undefined ? { pathAccess: parsePathAccessConfig(value["pathAccess"], path) } : {}),
|
||||
...(value["uploads"] !== undefined ? { uploads: parseUploadsConfig(value["uploads"], path) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user