feat: harden self-managed reconciliation
This commit is contained in:
+311
-66
@@ -18,6 +18,7 @@ import {
|
||||
type Role,
|
||||
} from "./authorization";
|
||||
import type { AuditStore } from "./audit-store";
|
||||
import { validateTrust, type TrustStore } from "./trust-store";
|
||||
import {
|
||||
BuildConflictError,
|
||||
BuildNotFoundError,
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
OperationConflictError,
|
||||
OperationNotFoundError,
|
||||
OperationValidationError,
|
||||
sanitizeOperationError,
|
||||
sanitizeOperationResult,
|
||||
type Operation,
|
||||
type OperationStore,
|
||||
@@ -55,6 +57,7 @@ import {
|
||||
type Workspace,
|
||||
type WorkspaceStore,
|
||||
} from "./workspace-store";
|
||||
import { redactString } from "./redact";
|
||||
|
||||
const API_PREFIX = "/api/v2";
|
||||
const RUNTIME_SESSION_MS = 24 * 60 * 60 * 1000;
|
||||
@@ -62,6 +65,8 @@ const PERSISTENT_SESSION_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
const LOGIN_WINDOW_MS = 5 * 60 * 1000;
|
||||
const MAX_LOGIN_FAILURES = 5;
|
||||
const DEFAULT_JSON_LIMIT = 1024 * 1024;
|
||||
const WORKSPACE_LEASE_TTL_MS = 30_000;
|
||||
const WORKSPACE_LEASE_RENEW_INTERVAL_MS = WORKSPACE_LEASE_TTL_MS / 3;
|
||||
|
||||
export interface ApiWorkspaceStore extends WorkspaceStore {
|
||||
delete?(id: string): Promise<boolean>;
|
||||
@@ -77,6 +82,7 @@ export type AppOptions = {
|
||||
workspaceStore?: ApiWorkspaceStore;
|
||||
operationStore?: OperationStore;
|
||||
auditStore?: AuditStore;
|
||||
trustStore?: TrustStore;
|
||||
management?: ManagementService;
|
||||
builds?: BuildController;
|
||||
logs?: LogService;
|
||||
@@ -240,7 +246,7 @@ export function createApp(
|
||||
type: `https://kuber.astrxl.dev/problems/${error.code.toLowerCase()}`,
|
||||
title: error.title,
|
||||
status: error.status,
|
||||
detail: error.message,
|
||||
detail: redactString(error.message),
|
||||
code: error.code,
|
||||
requestId,
|
||||
...(error.operationId && { operationId: error.operationId }),
|
||||
@@ -508,6 +514,39 @@ export function createApp(
|
||||
return options.management;
|
||||
}
|
||||
|
||||
function requireTrustStore(): TrustStore {
|
||||
if (!options.trustStore)
|
||||
throw new HttpError(
|
||||
503,
|
||||
"Service unavailable",
|
||||
"TRUST_STORE_UNAVAILABLE",
|
||||
"Namespace trust service is unavailable. Contact your Kuber administrator.",
|
||||
);
|
||||
return options.trustStore;
|
||||
}
|
||||
|
||||
async function requireTrust(
|
||||
request: Request,
|
||||
project: string,
|
||||
): Promise<void> {
|
||||
const fingerprint = request.headers.get("x-kuber-trust-fingerprint");
|
||||
const suppliedProject = request.headers.get("x-kuber-trust-project");
|
||||
if (!fingerprint || suppliedProject !== project)
|
||||
throw new HttpError(
|
||||
428,
|
||||
"Trusted workspace required",
|
||||
"TRUST_REQUIRED",
|
||||
"This workspace reconciliation requires trust. Run kuber trust and retry.",
|
||||
);
|
||||
if (!(await requireTrustStore().has(project, fingerprint)))
|
||||
throw new HttpError(
|
||||
403,
|
||||
"Trusted workspace rejected",
|
||||
"TRUST_REQUIRED",
|
||||
"This directory is not registered for this namespace.",
|
||||
);
|
||||
}
|
||||
|
||||
function requireAdoption(): WorkspaceAdoptionService {
|
||||
const workspaceStore = options.workspaceStore;
|
||||
const adoption =
|
||||
@@ -556,6 +595,12 @@ export function createApp(
|
||||
},
|
||||
status: {
|
||||
...operation.status,
|
||||
...(operation.status.error && {
|
||||
error: sanitizeOperationError(
|
||||
operation.status.error,
|
||||
operation.spec.action,
|
||||
),
|
||||
}),
|
||||
...(operation.status.result !== undefined && {
|
||||
result: sanitizeOperationResult(
|
||||
operation.status.result,
|
||||
@@ -568,15 +613,58 @@ export function createApp(
|
||||
|
||||
function operationBody(operation: Operation, result: unknown) {
|
||||
const visible = publicOperation(operation);
|
||||
return isRecord(result)
|
||||
? { ...result, operationId: operation.metadata.name, operation: visible }
|
||||
: Array.isArray(result)
|
||||
const safeResult = sanitizeOperationResult(result, operation.spec.action);
|
||||
return isRecord(safeResult)
|
||||
? {
|
||||
...safeResult,
|
||||
operationId: operation.metadata.name,
|
||||
operation: visible,
|
||||
}
|
||||
: Array.isArray(safeResult)
|
||||
? {
|
||||
deployments: result,
|
||||
deployments: safeResult,
|
||||
operationId: operation.metadata.name,
|
||||
operation: visible,
|
||||
}
|
||||
: { result, operationId: operation.metadata.name, operation: visible };
|
||||
: {
|
||||
result: safeResult,
|
||||
operationId: operation.metadata.name,
|
||||
operation: visible,
|
||||
};
|
||||
}
|
||||
|
||||
async function transitionOperationToFailure(
|
||||
operationId: string,
|
||||
error: NonNullable<Operation["status"]["error"]>,
|
||||
): Promise<Operation> {
|
||||
try {
|
||||
return await options.operationStore!.transition(operationId, "failed", {
|
||||
error,
|
||||
});
|
||||
} catch (transitionError) {
|
||||
if (!(transitionError instanceof OperationConflictError))
|
||||
throw transitionError;
|
||||
const latest = await options.operationStore!.get(operationId);
|
||||
if (latest?.status.state === "failed") return latest;
|
||||
throw transitionError;
|
||||
}
|
||||
}
|
||||
|
||||
function operationFailureError(operation: Operation): HttpError {
|
||||
const failure = operation.status.error
|
||||
? sanitizeOperationError(operation.status.error, operation.spec.action)
|
||||
: { code: "OPERATION_FAILED", message: "Operation failed" };
|
||||
return new HttpError(
|
||||
failure.code === "WORKSPACE_BUSY" ||
|
||||
failure.code === "WORKSPACE_LEASE_LOST"
|
||||
? 409
|
||||
: 500,
|
||||
"Operation failed",
|
||||
failure.code,
|
||||
failure.message,
|
||||
undefined,
|
||||
operation.metadata.name,
|
||||
);
|
||||
}
|
||||
|
||||
async function runOperation(
|
||||
@@ -585,7 +673,7 @@ export function createApp(
|
||||
workspace: Workspace,
|
||||
action: string,
|
||||
input: unknown,
|
||||
execute: () => Promise<unknown>,
|
||||
execute: (signal: AbortSignal) => Promise<unknown>,
|
||||
): Promise<Response> {
|
||||
if (!options.operationStore)
|
||||
throw new HttpError(
|
||||
@@ -594,7 +682,7 @@ export function createApp(
|
||||
"OPERATION_STORE_UNAVAILABLE",
|
||||
"Operation storage is not configured",
|
||||
);
|
||||
const operation = await options.operationStore.create({
|
||||
const { operation } = await options.operationStore.createOrReuse({
|
||||
workspaceId: workspace.metadata.name,
|
||||
workspaceUid: workspace.metadata.uid,
|
||||
action,
|
||||
@@ -602,14 +690,7 @@ export function createApp(
|
||||
request: input,
|
||||
});
|
||||
if (operation.status.state === "failed")
|
||||
throw new HttpError(
|
||||
operation.status.error?.code === "WORKSPACE_BUSY" ? 409 : 500,
|
||||
"Operation failed",
|
||||
operation.status.error?.code ?? "OPERATION_FAILED",
|
||||
operation.status.error?.message ?? "Operation failed",
|
||||
undefined,
|
||||
operation.metadata.name,
|
||||
);
|
||||
throw operationFailureError(operation);
|
||||
if (operation.status.state === "cancelled")
|
||||
throw new HttpError(
|
||||
409,
|
||||
@@ -640,16 +721,6 @@ export function createApp(
|
||||
)
|
||||
: undefined;
|
||||
if (options.leases && !lease) {
|
||||
await options.operationStore.transition(
|
||||
operation.metadata.name,
|
||||
"failed",
|
||||
{
|
||||
error: {
|
||||
code: "WORKSPACE_BUSY",
|
||||
message: "Another workspace operation is running",
|
||||
},
|
||||
},
|
||||
);
|
||||
throw new HttpError(
|
||||
409,
|
||||
"Conflict",
|
||||
@@ -659,9 +730,76 @@ export function createApp(
|
||||
operation.metadata.name,
|
||||
);
|
||||
}
|
||||
await options.operationStore.transition(operation.metadata.name, "running");
|
||||
if (!lease && options.leases) throw new Error("Unreachable lease state");
|
||||
|
||||
let operationStarted = false;
|
||||
let leaseRenewalTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let leaseOwnershipLost = false;
|
||||
let renewalInFlight: Promise<boolean> | undefined;
|
||||
const executionController = new AbortController();
|
||||
const renewLease = async (): Promise<boolean> => {
|
||||
if (!lease || leaseOwnershipLost) return false;
|
||||
if (renewalInFlight) return renewalInFlight;
|
||||
renewalInFlight = lease
|
||||
.renew(WORKSPACE_LEASE_TTL_MS)
|
||||
.catch(() => false)
|
||||
.then((renewed) => {
|
||||
if (!renewed) {
|
||||
leaseOwnershipLost = true;
|
||||
executionController.abort("Workspace lease ownership was lost");
|
||||
}
|
||||
return renewed;
|
||||
})
|
||||
.finally(() => {
|
||||
renewalInFlight = undefined;
|
||||
});
|
||||
return renewalInFlight;
|
||||
};
|
||||
const scheduleLeaseRenewal = () => {
|
||||
if (!lease || leaseOwnershipLost) return;
|
||||
leaseRenewalTimer = setTimeout(() => {
|
||||
void renewLease().finally(scheduleLeaseRenewal);
|
||||
}, WORKSPACE_LEASE_RENEW_INTERVAL_MS);
|
||||
};
|
||||
const requireLeaseOwnership = async () => {
|
||||
if (lease && !(await renewLease()))
|
||||
throw new HttpError(
|
||||
409,
|
||||
"Conflict",
|
||||
"WORKSPACE_LEASE_LOST",
|
||||
"Workspace operation lease ownership was lost",
|
||||
undefined,
|
||||
operation.metadata.name,
|
||||
);
|
||||
};
|
||||
try {
|
||||
const result = await execute();
|
||||
const claimed = await options.operationStore.claimExecution(
|
||||
operation.metadata.name,
|
||||
);
|
||||
if (!claimed) {
|
||||
const latest = await options.operationStore.get(
|
||||
operation.metadata.name,
|
||||
);
|
||||
if (latest?.status.state === "succeeded")
|
||||
return response(operationBody(latest, latest.status.result), 200, {
|
||||
location: `${API_PREFIX}/operations/${operation.metadata.name}`,
|
||||
});
|
||||
if (latest?.status.state === "failed")
|
||||
throw operationFailureError(latest);
|
||||
return response(
|
||||
{
|
||||
operationId: operation.metadata.name,
|
||||
operation: publicOperation(latest ?? operation),
|
||||
},
|
||||
202,
|
||||
{ location: `${API_PREFIX}/operations/${operation.metadata.name}` },
|
||||
);
|
||||
}
|
||||
operationStarted = true;
|
||||
scheduleLeaseRenewal();
|
||||
await requireLeaseOwnership();
|
||||
const result = await execute(executionController.signal);
|
||||
await requireLeaseOwnership();
|
||||
const completed = await options.operationStore.transition(
|
||||
operation.metadata.name,
|
||||
"succeeded",
|
||||
@@ -687,21 +825,30 @@ export function createApp(
|
||||
location: `${API_PREFIX}/operations/${operation.metadata.name}`,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!operationStarted) throw error;
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await options.operationStore.transition(
|
||||
const leaseLost =
|
||||
leaseOwnershipLost ||
|
||||
(error instanceof HttpError && error.code === "WORKSPACE_LEASE_LOST");
|
||||
const failed = await transitionOperationToFailure(
|
||||
operation.metadata.name,
|
||||
"failed",
|
||||
{
|
||||
error: { code: "OPERATION_FAILED", message },
|
||||
code: leaseLost ? "WORKSPACE_LEASE_LOST" : "OPERATION_FAILED",
|
||||
message: leaseLost
|
||||
? "Workspace operation lease ownership was lost"
|
||||
: message,
|
||||
},
|
||||
);
|
||||
const failure = failed.status.error;
|
||||
const failureCode = failure?.code ?? "OPERATION_FAILED";
|
||||
const failureMessage = failure?.message ?? message;
|
||||
try {
|
||||
await audit(
|
||||
identity,
|
||||
request,
|
||||
action,
|
||||
"failure",
|
||||
{ error: message },
|
||||
{ error: failureMessage },
|
||||
workspace.metadata.name,
|
||||
operation.metadata.name,
|
||||
);
|
||||
@@ -712,14 +859,17 @@ export function createApp(
|
||||
);
|
||||
}
|
||||
throw new HttpError(
|
||||
500,
|
||||
"Operation failed",
|
||||
"OPERATION_FAILED",
|
||||
message,
|
||||
failureCode === "WORKSPACE_LEASE_LOST" ? 409 : 500,
|
||||
failureCode === "WORKSPACE_LEASE_LOST"
|
||||
? "Conflict"
|
||||
: "Operation failed",
|
||||
failureCode,
|
||||
failureMessage,
|
||||
undefined,
|
||||
operation.metadata.name,
|
||||
);
|
||||
} finally {
|
||||
if (leaseRenewalTimer !== undefined) clearTimeout(leaseRenewalTimer);
|
||||
try {
|
||||
await lease?.release();
|
||||
} catch (error) {
|
||||
@@ -810,12 +960,89 @@ export function createApp(
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
const trustMatch = new RegExp(
|
||||
`^${API_PREFIX}/workspaces/([^/]+)/trust$`,
|
||||
).exec(path);
|
||||
if (trustMatch && options.trustStore) {
|
||||
const project = pathPart(trustMatch[1]!);
|
||||
if (request.method === "GET") {
|
||||
await requireCapability(identity, request, "kubernetes:read");
|
||||
return response({
|
||||
fingerprints: await options.trustStore.list(project),
|
||||
});
|
||||
}
|
||||
if (request.method === "POST") {
|
||||
await requireCapability(identity, request, "kubernetes:write");
|
||||
const body = await readJson(request);
|
||||
if (typeof body.fingerprint !== "string")
|
||||
throw new HttpError(
|
||||
400,
|
||||
"Invalid trust request",
|
||||
"TRUST_INVALID",
|
||||
"fingerprint is required",
|
||||
);
|
||||
try {
|
||||
validateTrust(project, body.fingerprint);
|
||||
} catch (error) {
|
||||
throw new HttpError(
|
||||
400,
|
||||
"Invalid trust request",
|
||||
"TRUST_INVALID",
|
||||
error instanceof Error ? error.message : "Invalid trust request",
|
||||
);
|
||||
}
|
||||
await options.trustStore.grant(project, body.fingerprint);
|
||||
await audit(
|
||||
identity,
|
||||
request,
|
||||
"trust.grant",
|
||||
"success",
|
||||
undefined,
|
||||
project,
|
||||
);
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
if (request.method === "DELETE") {
|
||||
await requireCapability(identity, request, "kubernetes:write");
|
||||
const fingerprint = url.searchParams.get("fingerprint");
|
||||
if (!fingerprint)
|
||||
throw new HttpError(
|
||||
400,
|
||||
"Invalid trust request",
|
||||
"TRUST_INVALID",
|
||||
"fingerprint is required",
|
||||
);
|
||||
if (!(await options.trustStore.revoke(project, fingerprint)))
|
||||
throw new HttpError(
|
||||
404,
|
||||
"Not found",
|
||||
"TRUST_REGISTRATION_MISSING",
|
||||
"Trust registration was not found",
|
||||
);
|
||||
await audit(
|
||||
identity,
|
||||
request,
|
||||
"trust.revoke",
|
||||
"success",
|
||||
undefined,
|
||||
project,
|
||||
);
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
}
|
||||
|
||||
if (path === `${API_PREFIX}/builds` && request.method === "POST") {
|
||||
await requireCapability(identity, request, "kubernetes:write");
|
||||
const body = await readJson(request);
|
||||
if (typeof body.project !== "string" || !body.project)
|
||||
throw new HttpError(
|
||||
400,
|
||||
"Invalid build request",
|
||||
"BUILD_INVALID",
|
||||
"project is required",
|
||||
);
|
||||
return response(
|
||||
await requireBuilds().submitBuild(
|
||||
(await readJson(request)) as unknown as BuildRequest,
|
||||
),
|
||||
await requireBuilds().submitBuild(body as unknown as BuildRequest),
|
||||
202,
|
||||
);
|
||||
}
|
||||
@@ -826,6 +1053,13 @@ export function createApp(
|
||||
) {
|
||||
await requireCapability(identity, request, "kubernetes:write");
|
||||
const body = await readJson(request);
|
||||
if (typeof body.project !== "string" || !body.project)
|
||||
throw new HttpError(
|
||||
400,
|
||||
"Invalid snapshot request",
|
||||
"BUILD_INVALID",
|
||||
"project is required",
|
||||
);
|
||||
return response(
|
||||
await requireBuilds().negotiateSnapshot(body.workspace as Sha256Digest),
|
||||
);
|
||||
@@ -886,6 +1120,14 @@ export function createApp(
|
||||
).exec(path);
|
||||
if (blobMatch) {
|
||||
await requireCapability(identity, request, "kubernetes:write");
|
||||
const project = url.searchParams.get("project");
|
||||
if (!project)
|
||||
throw new HttpError(
|
||||
400,
|
||||
"Invalid upload request",
|
||||
"BUILD_INVALID",
|
||||
"project is required",
|
||||
);
|
||||
const digest = `sha256:${blobMatch[2]!.toLowerCase()}` as Sha256Digest;
|
||||
if (blobMatch[3] === "complete" && request.method === "POST")
|
||||
return response(await requireBuilds().completeBlobUpload(digest));
|
||||
@@ -1151,13 +1393,16 @@ export function createApp(
|
||||
workspace,
|
||||
"workspace.delete",
|
||||
{ full: true },
|
||||
async () => {
|
||||
async (signal) => {
|
||||
const result = options.management
|
||||
? await options.management.down(
|
||||
workspaceIdentity(workspace),
|
||||
true,
|
||||
{ signal },
|
||||
)
|
||||
: undefined;
|
||||
if (signal.aborted)
|
||||
throw new Error("Workspace operation execution was cancelled");
|
||||
if (!requireWorkspaceStore().delete)
|
||||
throw new Error("Workspace store does not support deletion");
|
||||
await requireWorkspaceStore().delete!(id);
|
||||
@@ -1274,24 +1519,30 @@ export function createApp(
|
||||
workspace,
|
||||
`workspace.${lifecycleAction}`,
|
||||
body,
|
||||
async () => {
|
||||
async (signal) => {
|
||||
const management = requireManagement();
|
||||
const names = Array.isArray(body.services)
|
||||
? (body.services as string[])
|
||||
: undefined;
|
||||
if (lifecycleAction === "stop")
|
||||
return management.stop(workspaceIdentity(workspace), names);
|
||||
return management.stop(workspaceIdentity(workspace), names, {
|
||||
signal,
|
||||
});
|
||||
if (lifecycleAction === "restart")
|
||||
return management.restart(workspaceIdentity(workspace), names);
|
||||
return management.restart(workspaceIdentity(workspace), names, {
|
||||
signal,
|
||||
});
|
||||
if (lifecycleAction === "rollback")
|
||||
return management.rollback(
|
||||
workspaceIdentity(workspace),
|
||||
names,
|
||||
typeof body.timeoutMs === "number" ? body.timeoutMs : undefined,
|
||||
{ signal },
|
||||
);
|
||||
return management.down(
|
||||
workspaceIdentity(workspace),
|
||||
body.full === true,
|
||||
{ signal },
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1322,13 +1573,14 @@ export function createApp(
|
||||
) {
|
||||
await requireCapability(identity, request, "kubernetes:write");
|
||||
const body = await readJson(request);
|
||||
if (subpath === "resources/apply") await requireTrust(request, id);
|
||||
return runOperation(
|
||||
identity,
|
||||
request,
|
||||
workspace,
|
||||
subpath!,
|
||||
body,
|
||||
async () => {
|
||||
async (signal) => {
|
||||
const management = requireManagement();
|
||||
if (subpath === "resources/apply") {
|
||||
if (!Array.isArray(body.resources))
|
||||
@@ -1336,6 +1588,7 @@ export function createApp(
|
||||
return management.applyResources(
|
||||
workspaceIdentity(workspace),
|
||||
body.resources as KubernetesObject[],
|
||||
{ signal },
|
||||
);
|
||||
}
|
||||
if (subpath === "resources/wait") {
|
||||
@@ -1345,6 +1598,7 @@ export function createApp(
|
||||
workspaceIdentity(workspace),
|
||||
body.deployments as string[],
|
||||
typeof body.timeoutMs === "number" ? body.timeoutMs : undefined,
|
||||
{ signal },
|
||||
);
|
||||
return { ready: true };
|
||||
}
|
||||
@@ -1353,6 +1607,7 @@ export function createApp(
|
||||
await management.deleteResources(
|
||||
workspaceIdentity(workspace),
|
||||
body.resources as unknown as ResourceIdentity[],
|
||||
{ signal },
|
||||
);
|
||||
return { deleted: body.resources.length };
|
||||
},
|
||||
@@ -1369,10 +1624,11 @@ export function createApp(
|
||||
workspace,
|
||||
"databases.reconcile",
|
||||
body,
|
||||
() =>
|
||||
(signal) =>
|
||||
requireManagement().reconcileDatabases(
|
||||
workspaceIdentity(workspace),
|
||||
compose,
|
||||
{ signal },
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1429,10 +1685,11 @@ export function createApp(
|
||||
workspace,
|
||||
"storage.reconcile",
|
||||
body,
|
||||
() =>
|
||||
(signal) =>
|
||||
requireManagement().reconcileStorage(
|
||||
workspaceIdentity(workspace),
|
||||
compose,
|
||||
{ signal },
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1634,9 +1891,7 @@ export function createApp(
|
||||
};
|
||||
}
|
||||
|
||||
const EXEC_UPGRADE_PATH = new RegExp(
|
||||
`^${API_PREFIX}/workspaces/([^/]+)/exec$`,
|
||||
);
|
||||
const EXEC_UPGRADE_PATH = new RegExp(`^${API_PREFIX}/workspaces/([^/]+)/exec$`);
|
||||
|
||||
export function execUpgradeMatch(url: URL): string | undefined {
|
||||
const match = EXEC_UPGRADE_PATH.exec(url.pathname);
|
||||
@@ -1709,10 +1964,7 @@ export async function authorizeExecConnection(
|
||||
return { identity, workspace };
|
||||
}
|
||||
|
||||
export function execProblem(
|
||||
error: unknown,
|
||||
requestId = "",
|
||||
): Response {
|
||||
export function execProblem(error: unknown, requestId = ""): Response {
|
||||
let httpError: HttpError;
|
||||
if (error instanceof HttpError) httpError = error;
|
||||
else {
|
||||
@@ -1782,9 +2034,7 @@ const DEFAULT_EXEC_MAX_FRAME_BYTES = 64 * 1024;
|
||||
|
||||
export class WireExecSession {
|
||||
private readonly controller = new AbortController();
|
||||
private session?: Awaited<
|
||||
ReturnType<ExecService["openInteractive"]>
|
||||
>;
|
||||
private session?: Awaited<ReturnType<ExecService["openInteractive"]>>;
|
||||
private readonly maxFrameBytes: number;
|
||||
private started = false;
|
||||
private done: Promise<void> = Promise.resolve();
|
||||
@@ -1796,8 +2046,7 @@ export class WireExecSession {
|
||||
private readonly connection: ExecConnection,
|
||||
options: ExecLinkOptions = {},
|
||||
) {
|
||||
this.maxFrameBytes =
|
||||
options.maxFrameBytes ?? DEFAULT_EXEC_MAX_FRAME_BYTES;
|
||||
this.maxFrameBytes = options.maxFrameBytes ?? DEFAULT_EXEC_MAX_FRAME_BYTES;
|
||||
}
|
||||
|
||||
private send(frame: ExecServerWireFrame) {
|
||||
@@ -1851,14 +2100,10 @@ export class WireExecSession {
|
||||
private toClientFrame(frame: ExecClientWireFrame): ExecClientFrame {
|
||||
switch (frame.type) {
|
||||
case "stdin": {
|
||||
if (
|
||||
frame.encoding !== "base64" ||
|
||||
typeof frame.data !== "string"
|
||||
)
|
||||
if (frame.encoding !== "base64" || typeof frame.data !== "string")
|
||||
throw new Error("Invalid stdin frame");
|
||||
const data = base64ToBytes(frame.data);
|
||||
if (!data)
|
||||
throw new Error("Invalid stdin frame");
|
||||
if (!data) throw new Error("Invalid stdin frame");
|
||||
return {
|
||||
type: "stdin",
|
||||
data,
|
||||
@@ -1946,7 +2191,7 @@ export class WireExecSession {
|
||||
error instanceof Error &&
|
||||
"code" in error &&
|
||||
typeof (error as { code?: unknown }).code === "string"
|
||||
? ((error as { code: string }).code)
|
||||
? (error as { code: string }).code
|
||||
: "EXEC_FAILED",
|
||||
message:
|
||||
error instanceof Error ? error.message : "Exec failed to start",
|
||||
@@ -1985,7 +2230,7 @@ export class WireExecSession {
|
||||
error instanceof Error &&
|
||||
"code" in error &&
|
||||
typeof (error as { code?: unknown }).code === "string"
|
||||
? ((error as { code: string }).code)
|
||||
? (error as { code: string }).code
|
||||
: "EXEC_FAILED";
|
||||
this.send({
|
||||
type: "error",
|
||||
|
||||
Reference in New Issue
Block a user