mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(server): authorize active workspace config
This commit is contained in:
@@ -838,6 +838,7 @@ export class Session {
|
||||
emit: (msg) => this.emit(msg),
|
||||
},
|
||||
projectRegistry: this.projectRegistry,
|
||||
workspaceRegistry: this.workspaceRegistry,
|
||||
logger: this.sessionLogger,
|
||||
});
|
||||
this.daemonSession = new DaemonSession({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import pino from "pino";
|
||||
import { ProjectConfigSession, type ProjectConfigSessionHost } from "./project-config-session.js";
|
||||
import type { PersistedProjectRecord } from "../../workspace-registry.js";
|
||||
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "../../workspace-registry.js";
|
||||
import type { SessionOutboundMessage } from "../../messages.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
@@ -33,12 +33,37 @@ function projectRecord(rootPath: string, archivedAt: string | null = null): Pers
|
||||
};
|
||||
}
|
||||
|
||||
function makeSubsystem(records: PersistedProjectRecord[]) {
|
||||
function workspaceRecord(
|
||||
projectId: string,
|
||||
cwd: string,
|
||||
archivedAt: string | null = null,
|
||||
): PersistedWorkspaceRecord {
|
||||
return {
|
||||
workspaceId: `workspace:${cwd}`,
|
||||
projectId,
|
||||
cwd,
|
||||
kind: "worktree",
|
||||
displayName: "Workspace",
|
||||
branch: "feature",
|
||||
worktreeRoot: cwd,
|
||||
isPaseoOwnedWorktree: true,
|
||||
mainRepoRoot: null,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
archivedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function makeSubsystem(
|
||||
records: PersistedProjectRecord[],
|
||||
workspaces: PersistedWorkspaceRecord[] = [],
|
||||
) {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const host: ProjectConfigSessionHost = { emit: (msg) => emitted.push(msg) };
|
||||
const subsystem = new ProjectConfigSession({
|
||||
host,
|
||||
projectRegistry: { list: async () => records },
|
||||
workspaceRegistry: { list: async () => workspaces },
|
||||
logger: pino({ level: "silent" }),
|
||||
});
|
||||
return { subsystem, emitted };
|
||||
@@ -179,6 +204,92 @@ describe("ProjectConfigSession", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("read and write accept an active workspace belonging to an active project", async () => {
|
||||
const repoRoot = makeRoot();
|
||||
const workspaceRoot = makeRoot();
|
||||
const project = projectRecord(repoRoot);
|
||||
const { subsystem, emitted } = makeSubsystem(
|
||||
[project],
|
||||
[workspaceRecord(project.projectId, workspaceRoot)],
|
||||
);
|
||||
|
||||
await subsystem.handleWriteProjectConfigRequest({
|
||||
type: "write_project_config_request",
|
||||
requestId: "workspace-write-1",
|
||||
repoRoot: workspaceRoot,
|
||||
config: { worktree: { setup: "npm ci" } },
|
||||
expectedRevision: null,
|
||||
});
|
||||
await subsystem.handleReadProjectConfigRequest({
|
||||
type: "read_project_config_request",
|
||||
requestId: "workspace-read-1",
|
||||
repoRoot: workspaceRoot,
|
||||
});
|
||||
|
||||
expect(emitted).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "write_project_config_response",
|
||||
payload: expect.objectContaining({ ok: true, repoRoot: workspaceRoot }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "read_project_config_response",
|
||||
payload: expect.objectContaining({
|
||||
ok: true,
|
||||
repoRoot: workspaceRoot,
|
||||
config: { worktree: { setup: "npm ci" } },
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("read rejects archived workspaces and workspaces owned by archived projects", async () => {
|
||||
const activeRepoRoot = makeRoot();
|
||||
const archivedRepoRoot = makeRoot();
|
||||
const archivedWorkspaceRoot = makeRoot();
|
||||
const orphanedWorkspaceRoot = makeRoot();
|
||||
const activeProject = projectRecord(activeRepoRoot);
|
||||
const archivedProject = projectRecord(archivedRepoRoot, "2026-01-02T00:00:00.000Z");
|
||||
const { subsystem, emitted } = makeSubsystem(
|
||||
[activeProject, archivedProject],
|
||||
[
|
||||
workspaceRecord(activeProject.projectId, archivedWorkspaceRoot, "2026-01-02T00:00:00.000Z"),
|
||||
workspaceRecord(archivedProject.projectId, orphanedWorkspaceRoot),
|
||||
],
|
||||
);
|
||||
|
||||
for (const [requestId, repoRoot] of [
|
||||
["archived-workspace", archivedWorkspaceRoot],
|
||||
["archived-owner", orphanedWorkspaceRoot],
|
||||
] as const) {
|
||||
await subsystem.handleReadProjectConfigRequest({
|
||||
type: "read_project_config_request",
|
||||
requestId,
|
||||
repoRoot,
|
||||
});
|
||||
}
|
||||
|
||||
expect(emitted).toEqual([
|
||||
{
|
||||
type: "read_project_config_response",
|
||||
payload: {
|
||||
requestId: "archived-workspace",
|
||||
repoRoot: archivedWorkspaceRoot,
|
||||
ok: false,
|
||||
error: { code: "project_not_found" },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "read_project_config_response",
|
||||
payload: {
|
||||
requestId: "archived-owner",
|
||||
repoRoot: orphanedWorkspaceRoot,
|
||||
ok: false,
|
||||
error: { code: "project_not_found" },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("write rejects a stale revision and an unknown root with their inline domain failures", async () => {
|
||||
const staleRoot = makeRoot();
|
||||
writeFileSync(join(staleRoot, "paseo.json"), JSON.stringify({ worktree: { setup: "old" } }));
|
||||
|
||||
@@ -2,7 +2,7 @@ import { realpathSync } from "node:fs";
|
||||
import { resolve, sep } from "path";
|
||||
import type pino from "pino";
|
||||
import type { SessionInboundMessage, SessionOutboundMessage } from "../../messages.js";
|
||||
import type { ProjectRegistry } from "../../workspace-registry.js";
|
||||
import type { ProjectRegistry, WorkspaceRegistry } from "../../workspace-registry.js";
|
||||
import {
|
||||
readPaseoConfigForEdit,
|
||||
writePaseoConfigForEdit,
|
||||
@@ -16,24 +16,27 @@ export interface ProjectConfigSessionHost {
|
||||
export interface ProjectConfigSessionOptions {
|
||||
host: ProjectConfigSessionHost;
|
||||
projectRegistry: Pick<ProjectRegistry, "list">;
|
||||
workspaceRegistry: Pick<WorkspaceRegistry, "list">;
|
||||
logger: pino.Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* A client's read/write surface for a project's on-disk paseo.json. Resolves the
|
||||
* request's repoRoot against the known (non-archived) project roots — accepting a
|
||||
* trailing slash or a symlink via realpath — then reads or writes the config
|
||||
* substrate and emits the matching response. Reaches no state beyond the injected
|
||||
* project registry and the outbound channel.
|
||||
* request's repoRoot against known active project and workspace roots — accepting
|
||||
* a trailing slash or a symlink via realpath — then reads or writes the config
|
||||
* substrate and emits the matching response. Workspace roots remain authorized
|
||||
* only while both the workspace and its owning project are active.
|
||||
*/
|
||||
export class ProjectConfigSession {
|
||||
private readonly host: ProjectConfigSessionHost;
|
||||
private readonly projectRegistry: Pick<ProjectRegistry, "list">;
|
||||
private readonly workspaceRegistry: Pick<WorkspaceRegistry, "list">;
|
||||
private readonly logger: pino.Logger;
|
||||
|
||||
constructor(options: ProjectConfigSessionOptions) {
|
||||
this.host = options.host;
|
||||
this.projectRegistry = options.projectRegistry;
|
||||
this.workspaceRegistry = options.workspaceRegistry;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
@@ -153,15 +156,27 @@ export class ProjectConfigSession {
|
||||
private async resolveKnownProjectRoot(repoRoot: string): Promise<string | null> {
|
||||
const requestedRoot = canonicalizeConfigRoot(repoRoot);
|
||||
const projects = await this.projectRegistry.list();
|
||||
const activeProjectIds = new Set<string>();
|
||||
for (const project of projects) {
|
||||
if (project.archivedAt !== null) {
|
||||
continue;
|
||||
}
|
||||
activeProjectIds.add(project.projectId);
|
||||
const projectRoot = canonicalizeConfigRoot(project.rootPath);
|
||||
if (requestedRoot === projectRoot) {
|
||||
return projectRoot;
|
||||
}
|
||||
}
|
||||
const workspaces = await this.workspaceRegistry.list();
|
||||
for (const workspace of workspaces) {
|
||||
if (workspace.archivedAt !== null || !activeProjectIds.has(workspace.projectId)) {
|
||||
continue;
|
||||
}
|
||||
const workspaceRoot = canonicalizeConfigRoot(workspace.cwd);
|
||||
if (requestedRoot === workspaceRoot) {
|
||||
return workspaceRoot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user