mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(daemon): add phase 2c cli wrappers Co-authored-by: multica-agent <github@multica.ai> * fix: handle desktop-gated CLI imports Co-authored-by: multica-agent <github@multica.ai> * fix: pass sidecar ipc path to agent wrappers Co-authored-by: multica-agent <github@multica.ai> * fix: make agent wrapper env explicit Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): preserve CLI import and diff edge cases Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai>
690 lines
23 KiB
TypeScript
690 lines
23 KiB
TypeScript
export const APP_KEYS = Object.freeze({
|
|
DAEMON: "daemon",
|
|
DESKTOP: "desktop",
|
|
WEB: "web",
|
|
} as const);
|
|
|
|
export type AppKey = (typeof APP_KEYS)[keyof typeof APP_KEYS];
|
|
|
|
export const SIDECAR_MODES = Object.freeze({
|
|
DEV: "dev",
|
|
RUNTIME: "runtime",
|
|
} as const);
|
|
|
|
export type SidecarMode = (typeof SIDECAR_MODES)[keyof typeof SIDECAR_MODES];
|
|
|
|
export const SIDECAR_SOURCES = Object.freeze({
|
|
PACKAGED: "packaged",
|
|
TOOLS_DEV: "tools-dev",
|
|
TOOLS_PACK: "tools-pack",
|
|
} as const);
|
|
|
|
export type SidecarSource = (typeof SIDECAR_SOURCES)[keyof typeof SIDECAR_SOURCES];
|
|
|
|
export const SIDECAR_ENV = Object.freeze({
|
|
BASE: "OD_SIDECAR_BASE",
|
|
DAEMON_CLI_PATH: "OD_DAEMON_CLI_PATH",
|
|
DAEMON_PORT: "OD_PORT",
|
|
IPC_BASE: "OD_SIDECAR_IPC_BASE",
|
|
IPC_PATH: "OD_SIDECAR_IPC_PATH",
|
|
NAMESPACE: "OD_SIDECAR_NAMESPACE",
|
|
SOURCE: "OD_SIDECAR_SOURCE",
|
|
TOOLS_DEV_PARENT_PID: "OD_TOOLS_DEV_PARENT_PID",
|
|
WEB_DIST_DIR: "OD_WEB_DIST_DIR",
|
|
WEB_PORT: "OD_WEB_PORT",
|
|
WEB_TSCONFIG_PATH: "OD_WEB_TSCONFIG_PATH",
|
|
} as const);
|
|
|
|
export const SIDECAR_RUNTIME_ENV = Object.freeze({
|
|
base: SIDECAR_ENV.BASE,
|
|
ipcBase: SIDECAR_ENV.IPC_BASE,
|
|
ipcPath: SIDECAR_ENV.IPC_PATH,
|
|
namespace: SIDECAR_ENV.NAMESPACE,
|
|
source: SIDECAR_ENV.SOURCE,
|
|
} as const);
|
|
|
|
export const SIDECAR_STAMP_FLAGS = Object.freeze({
|
|
app: "--od-stamp-app",
|
|
ipc: "--od-stamp-ipc",
|
|
mode: "--od-stamp-mode",
|
|
namespace: "--od-stamp-namespace",
|
|
source: "--od-stamp-source",
|
|
} as const);
|
|
|
|
export const STAMP_APP_FLAG = SIDECAR_STAMP_FLAGS.app;
|
|
export const STAMP_IPC_FLAG = SIDECAR_STAMP_FLAGS.ipc;
|
|
export const STAMP_MODE_FLAG = SIDECAR_STAMP_FLAGS.mode;
|
|
export const STAMP_NAMESPACE_FLAG = SIDECAR_STAMP_FLAGS.namespace;
|
|
export const STAMP_SOURCE_FLAG = SIDECAR_STAMP_FLAGS.source;
|
|
|
|
export const SIDECAR_STAMP_FIELDS = ["app", "mode", "namespace", "ipc", "source"] as const;
|
|
|
|
export const SIDECAR_DEFAULTS = Object.freeze({
|
|
host: "127.0.0.1",
|
|
ipcBase: "/tmp/open-design/ipc",
|
|
namespace: "default",
|
|
projectTmpDirName: ".tmp",
|
|
windowsPipePrefix: "open-design",
|
|
} as const);
|
|
|
|
export const SIDECAR_MESSAGES = Object.freeze({
|
|
CLICK: "click",
|
|
CONSOLE: "console",
|
|
EVAL: "eval",
|
|
EXPORT_PDF: "export-pdf",
|
|
MINT_IMPORT_TOKEN: "mint-import-token",
|
|
REGISTER_DESKTOP_AUTH: "register-desktop-auth",
|
|
SCREENSHOT: "screenshot",
|
|
SHUTDOWN: "shutdown",
|
|
STATUS: "status",
|
|
UPDATE: "update",
|
|
} as const);
|
|
|
|
export const DESKTOP_UPDATE_ACTIONS = Object.freeze({
|
|
CHECK: "check",
|
|
DOWNLOAD: "download",
|
|
INSTALL: "install",
|
|
STATUS: "status",
|
|
} as const);
|
|
|
|
export type DesktopUpdateAction = (typeof DESKTOP_UPDATE_ACTIONS)[keyof typeof DESKTOP_UPDATE_ACTIONS];
|
|
|
|
export const DESKTOP_UPDATE_MODES = Object.freeze({
|
|
JS_INCREMENTAL: "js-incremental",
|
|
PACKAGE_LAUNCHER: "package-launcher",
|
|
} as const);
|
|
|
|
export type DesktopUpdateMode = (typeof DESKTOP_UPDATE_MODES)[keyof typeof DESKTOP_UPDATE_MODES];
|
|
|
|
export const DESKTOP_UPDATE_CHANNELS = Object.freeze({
|
|
BETA: "beta",
|
|
NIGHTLY: "nightly",
|
|
PREVIEW: "preview",
|
|
STABLE: "stable",
|
|
} as const);
|
|
|
|
export type DesktopUpdateChannel = (typeof DESKTOP_UPDATE_CHANNELS)[keyof typeof DESKTOP_UPDATE_CHANNELS];
|
|
|
|
export const DESKTOP_UPDATE_STATES = Object.freeze({
|
|
AVAILABLE: "available",
|
|
CHECKING: "checking",
|
|
DOWNLOADED: "downloaded",
|
|
DOWNLOADING: "downloading",
|
|
ERROR: "error",
|
|
IDLE: "idle",
|
|
INSTALLING: "installing",
|
|
NOT_AVAILABLE: "not-available",
|
|
UNSUPPORTED: "unsupported",
|
|
} as const);
|
|
|
|
export type DesktopUpdateState = (typeof DESKTOP_UPDATE_STATES)[keyof typeof DESKTOP_UPDATE_STATES];
|
|
|
|
export const SIDECAR_ERROR_CODES = Object.freeze({
|
|
INVALID_MESSAGE: "SIDECAR_INVALID_MESSAGE",
|
|
UNKNOWN_MESSAGE: "SIDECAR_UNKNOWN_MESSAGE",
|
|
} as const);
|
|
|
|
export type SidecarErrorCode = (typeof SIDECAR_ERROR_CODES)[keyof typeof SIDECAR_ERROR_CODES];
|
|
|
|
export class SidecarContractError extends Error {
|
|
readonly code: SidecarErrorCode;
|
|
|
|
constructor(code: SidecarErrorCode, message: string) {
|
|
super(message);
|
|
this.name = "SidecarContractError";
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
export type ServiceRuntimeState = "idle" | "running" | "starting" | "stopped" | "unknown";
|
|
|
|
export type DaemonStatusSnapshot = {
|
|
pid?: number | null;
|
|
state: ServiceRuntimeState;
|
|
trustedWebOriginPort?: number | null;
|
|
updatedAt?: string;
|
|
url: string | null;
|
|
/**
|
|
* PR #974 round 6 (mrcfps): true when the daemon's
|
|
* `/api/import/folder` route refuses tokenless requests. Surfaced
|
|
* over IPC so `tools-dev start desktop` can detect a daemon that
|
|
* was spawned without `OD_REQUIRE_DESKTOP_AUTH=1` (the split-start
|
|
* dev flow `start daemon` -> `start desktop`) and restart it
|
|
* before launching desktop main, instead of letting a renderer
|
|
* race the registration handshake. Mirrors
|
|
* `apps/daemon/src/server.ts#isDesktopAuthGateActive()` at the
|
|
* moment the STATUS request was answered.
|
|
*/
|
|
desktopAuthGateActive: boolean;
|
|
};
|
|
|
|
export type WebStatusSnapshot = {
|
|
pid?: number | null;
|
|
state: ServiceRuntimeState;
|
|
updatedAt?: string;
|
|
url: string | null;
|
|
};
|
|
|
|
export type DesktopRuntimeState = "idle" | "running" | "unknown";
|
|
|
|
export type DesktopStatusSnapshot = {
|
|
pid?: number | null;
|
|
state: DesktopRuntimeState;
|
|
title?: string | null;
|
|
update?: DesktopUpdateStatusSnapshot;
|
|
updatedAt?: string;
|
|
url?: string | null;
|
|
windowVisible?: boolean;
|
|
};
|
|
|
|
export type DesktopEvalInput = {
|
|
expression: string;
|
|
};
|
|
|
|
export type DesktopEvalResult = {
|
|
error?: string;
|
|
ok: boolean;
|
|
value?: unknown;
|
|
};
|
|
|
|
export type DesktopScreenshotInput = {
|
|
path: string;
|
|
};
|
|
|
|
export type DesktopScreenshotResult = {
|
|
path: string;
|
|
};
|
|
|
|
export type DesktopConsoleEntry = {
|
|
level: string;
|
|
text: string;
|
|
timestamp: string;
|
|
};
|
|
|
|
export type DesktopConsoleResult = {
|
|
entries: DesktopConsoleEntry[];
|
|
};
|
|
|
|
export type DesktopClickInput = {
|
|
selector: string;
|
|
};
|
|
|
|
export type DesktopClickResult = {
|
|
clicked: boolean;
|
|
found: boolean;
|
|
};
|
|
|
|
export type DesktopExportPdfInput = {
|
|
baseHref?: string;
|
|
deck: boolean;
|
|
defaultFilename: string;
|
|
html: string;
|
|
title: string;
|
|
};
|
|
|
|
export type DesktopExportPdfResult = {
|
|
canceled?: boolean;
|
|
error?: string;
|
|
ok: boolean;
|
|
path?: string;
|
|
};
|
|
|
|
export type DesktopUpdateCapabilitySet = {
|
|
canApplyInPlace: boolean;
|
|
canDownload: boolean;
|
|
canOpenInstaller: boolean;
|
|
requiresManualInstall: boolean;
|
|
};
|
|
|
|
export type DesktopUpdatePathSnapshot = {
|
|
downloadRoot?: string;
|
|
manifestPath?: string;
|
|
};
|
|
|
|
export type DesktopUpdateChecksumSnapshot = {
|
|
algorithm: "sha256" | "sha512";
|
|
url?: string;
|
|
value?: string;
|
|
};
|
|
|
|
export type DesktopUpdateArtifactSnapshot = {
|
|
name?: string;
|
|
platformKey?: string;
|
|
size?: number;
|
|
type?: string;
|
|
url: string;
|
|
};
|
|
|
|
export type DesktopUpdateProgressSnapshot = {
|
|
receivedBytes: number;
|
|
totalBytes?: number;
|
|
};
|
|
|
|
export type DesktopUpdateErrorSnapshot = {
|
|
code: string;
|
|
details?: unknown;
|
|
message: string;
|
|
};
|
|
|
|
export type DesktopUpdateInstallResult = {
|
|
dryRun?: boolean;
|
|
openedAt: string;
|
|
path: string;
|
|
};
|
|
|
|
export type DesktopUpdateReleaseSnapshot = {
|
|
arch: string;
|
|
artifact: DesktopUpdateArtifactSnapshot;
|
|
checksum: DesktopUpdateChecksumSnapshot;
|
|
channel: DesktopUpdateChannel;
|
|
downloadedAt: string;
|
|
key: string;
|
|
metadata?: Record<string, unknown>;
|
|
path: string;
|
|
platformKey: string;
|
|
version: string;
|
|
};
|
|
|
|
export type DesktopUpdateIncomingSnapshot = {
|
|
arch: string;
|
|
artifact: DesktopUpdateArtifactSnapshot;
|
|
channel: DesktopUpdateChannel;
|
|
key?: string;
|
|
metadata?: Record<string, unknown>;
|
|
progress?: DesktopUpdateProgressSnapshot;
|
|
startedAt: string;
|
|
version: string;
|
|
};
|
|
|
|
export type DesktopUpdateStatusSnapshot = {
|
|
active?: DesktopUpdateReleaseSnapshot;
|
|
arch: string;
|
|
artifact?: DesktopUpdateArtifactSnapshot;
|
|
artifactUrl?: string;
|
|
availableVersion?: string;
|
|
capabilities: DesktopUpdateCapabilitySet;
|
|
channel: DesktopUpdateChannel;
|
|
checksum?: DesktopUpdateChecksumSnapshot;
|
|
currentVersion: string;
|
|
downloadPath?: string;
|
|
enabled: boolean;
|
|
error?: DesktopUpdateErrorSnapshot;
|
|
incoming?: DesktopUpdateIncomingSnapshot;
|
|
installResult?: DesktopUpdateInstallResult;
|
|
lastCheckedAt?: string;
|
|
metadata?: Record<string, unknown>;
|
|
mode: DesktopUpdateMode;
|
|
paths?: DesktopUpdatePathSnapshot;
|
|
platform: string;
|
|
progress?: DesktopUpdateProgressSnapshot;
|
|
state: DesktopUpdateState;
|
|
supported: boolean;
|
|
};
|
|
|
|
export type DesktopUpdateInput = {
|
|
action: DesktopUpdateAction;
|
|
};
|
|
|
|
export type DesktopUpdateResult = DesktopUpdateStatusSnapshot;
|
|
|
|
export type SidecarStatusMessage = { type: typeof SIDECAR_MESSAGES.STATUS };
|
|
export type SidecarShutdownMessage = { type: typeof SIDECAR_MESSAGES.SHUTDOWN };
|
|
export type DesktopEvalMessage = { input: DesktopEvalInput; type: typeof SIDECAR_MESSAGES.EVAL };
|
|
export type DesktopScreenshotMessage = { input: DesktopScreenshotInput; type: typeof SIDECAR_MESSAGES.SCREENSHOT };
|
|
export type DesktopConsoleMessage = { type: typeof SIDECAR_MESSAGES.CONSOLE };
|
|
export type DesktopClickMessage = { input: DesktopClickInput; type: typeof SIDECAR_MESSAGES.CLICK };
|
|
export type DesktopExportPdfMessage = { input: DesktopExportPdfInput; type: typeof SIDECAR_MESSAGES.EXPORT_PDF };
|
|
export type DesktopUpdateMessage = { input: DesktopUpdateInput; type: typeof SIDECAR_MESSAGES.UPDATE };
|
|
|
|
// Sent by the desktop main process to the daemon over its sidecar IPC at
|
|
// startup, before the BrowserWindow is created. The base64 string is a
|
|
// freshly generated 32-byte secret that both processes will share for the
|
|
// lifetime of the daemon. The daemon uses this secret to verify HMAC tokens
|
|
// minted by the desktop main process for `POST /api/import/folder` calls
|
|
// (PR #974: closes the renderer→arbitrary-baseDir→openPath bypass chain).
|
|
// When the secret is registered, daemon's import-folder route requires a
|
|
// valid per-path token; when it isn't (web-only deployments), the route
|
|
// behaves as before.
|
|
export type RegisterDesktopAuthInput = {
|
|
secret: string;
|
|
};
|
|
|
|
export type RegisterDesktopAuthMessage = {
|
|
input: RegisterDesktopAuthInput;
|
|
type: typeof SIDECAR_MESSAGES.REGISTER_DESKTOP_AUTH;
|
|
};
|
|
|
|
export type RegisterDesktopAuthResult = {
|
|
accepted: true;
|
|
};
|
|
|
|
export type MintImportTokenInput = {
|
|
baseDir: string;
|
|
};
|
|
|
|
export type MintImportTokenMessage = {
|
|
input: MintImportTokenInput;
|
|
type: typeof SIDECAR_MESSAGES.MINT_IMPORT_TOKEN;
|
|
};
|
|
|
|
export type MintImportTokenResult =
|
|
| { ok: true; expiresAt: string; token: string }
|
|
| { ok: false; code: "DESKTOP_AUTH_INACTIVE"; message: string; retryable: false }
|
|
| { ok: false; code: "DESKTOP_AUTH_PENDING"; message: string; retryable: true };
|
|
|
|
export type DaemonSidecarMessage =
|
|
| SidecarStatusMessage
|
|
| SidecarShutdownMessage
|
|
| RegisterDesktopAuthMessage
|
|
| MintImportTokenMessage;
|
|
export type WebSidecarMessage = SidecarStatusMessage | SidecarShutdownMessage;
|
|
export type DesktopSidecarMessage =
|
|
| SidecarStatusMessage
|
|
| SidecarShutdownMessage
|
|
| DesktopEvalMessage
|
|
| DesktopScreenshotMessage
|
|
| DesktopConsoleMessage
|
|
| DesktopClickMessage
|
|
| DesktopExportPdfMessage
|
|
| DesktopUpdateMessage;
|
|
|
|
export type ShutdownResult = {
|
|
accepted: true;
|
|
};
|
|
|
|
export type SidecarStamp = {
|
|
app: AppKey;
|
|
ipc: string;
|
|
mode: SidecarMode;
|
|
namespace: string;
|
|
source: SidecarSource;
|
|
};
|
|
|
|
export type SidecarStampInput = Partial<Record<(typeof SIDECAR_STAMP_FIELDS)[number], unknown>>;
|
|
export type SidecarStampCriteria = Partial<SidecarStamp>;
|
|
|
|
export type OpenDesignSidecarContract = {
|
|
appKeys: typeof APP_KEYS;
|
|
defaults: typeof SIDECAR_DEFAULTS;
|
|
env: typeof SIDECAR_RUNTIME_ENV;
|
|
errorCodes: typeof SIDECAR_ERROR_CODES;
|
|
messages: typeof SIDECAR_MESSAGES;
|
|
modes: typeof SIDECAR_MODES;
|
|
normalizeApp: typeof normalizeAppKey;
|
|
normalizeNamespace: typeof normalizeNamespace;
|
|
normalizeSource: typeof normalizeSidecarSource;
|
|
normalizeStamp: typeof normalizeSidecarStamp;
|
|
normalizeStampCriteria: typeof normalizeSidecarStampCriteria;
|
|
sources: typeof SIDECAR_SOURCES;
|
|
stampFields: typeof SIDECAR_STAMP_FIELDS;
|
|
stampFlags: typeof SIDECAR_STAMP_FLAGS;
|
|
updateActions: typeof DESKTOP_UPDATE_ACTIONS;
|
|
updateChannels: typeof DESKTOP_UPDATE_CHANNELS;
|
|
updateModes: typeof DESKTOP_UPDATE_MODES;
|
|
updateStates: typeof DESKTOP_UPDATE_STATES;
|
|
};
|
|
|
|
function assertObject(value: unknown, label: string): Record<string, unknown> {
|
|
if (typeof value !== "object" || value == null || Array.isArray(value)) {
|
|
throw new Error(`${label} must be an object`);
|
|
}
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function assertKnownKeys(value: Record<string, unknown>, allowed: readonly string[], label: string): void {
|
|
const allowedSet = new Set<string>(allowed);
|
|
const unexpected = Object.keys(value).filter((key) => !allowedSet.has(key));
|
|
if (unexpected.length > 0) {
|
|
throw new Error(`${label} contains unsupported fields: ${unexpected.join(", ")}`);
|
|
}
|
|
}
|
|
|
|
function normalizeNonEmptyString(value: unknown, label: string): string {
|
|
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
if (value.length === 0) throw new Error(`${label} must not be empty`);
|
|
return value;
|
|
}
|
|
|
|
export function normalizeNamespace(namespace: unknown): string {
|
|
if (typeof namespace !== "string") throw new Error("namespace must be a string");
|
|
const value = namespace.trim();
|
|
if (value.length === 0) throw new Error("namespace must not be empty");
|
|
if (value !== namespace) throw new Error("namespace must not contain leading or trailing whitespace");
|
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) {
|
|
throw new Error(`namespace contains unsupported characters: ${value}`);
|
|
}
|
|
if (/[\\/]/.test(value)) throw new Error(`namespace must not contain path separators: ${value}`);
|
|
return value;
|
|
}
|
|
|
|
export function isSidecarMode(value: unknown): value is SidecarMode {
|
|
return Object.values(SIDECAR_MODES).includes(value as SidecarMode);
|
|
}
|
|
|
|
export function normalizeSidecarMode(mode: unknown): SidecarMode {
|
|
if (!isSidecarMode(mode)) {
|
|
throw new Error("sidecar mode must be dev or runtime");
|
|
}
|
|
return mode;
|
|
}
|
|
|
|
export function isAppKey(value: unknown): value is AppKey {
|
|
return Object.values(APP_KEYS).includes(value as AppKey);
|
|
}
|
|
|
|
export function normalizeAppKey(app: unknown): AppKey {
|
|
if (!isAppKey(app)) throw new Error(`unsupported sidecar app: ${String(app)}`);
|
|
return app;
|
|
}
|
|
|
|
export function isSidecarSource(value: unknown): value is SidecarSource {
|
|
return Object.values(SIDECAR_SOURCES).includes(value as SidecarSource);
|
|
}
|
|
|
|
export function normalizeSidecarSource(source: unknown): SidecarSource {
|
|
if (!isSidecarSource(source)) {
|
|
throw new Error(`unsupported sidecar source: ${String(source)}`);
|
|
}
|
|
return source;
|
|
}
|
|
|
|
export function isWindowsNamedPipePath(value: unknown): boolean {
|
|
return typeof value === "string" && value.startsWith("\\\\.\\pipe\\");
|
|
}
|
|
|
|
export function normalizeIpcPath(ipc: unknown): string {
|
|
if (typeof ipc !== "string") throw new Error("sidecar ipc path must be a string");
|
|
if (ipc.length === 0) throw new Error("sidecar ipc path must not be empty");
|
|
if (ipc.trim() !== ipc) throw new Error("sidecar ipc path must not contain leading or trailing whitespace");
|
|
if (ipc.includes("\0")) throw new Error("sidecar ipc path must not contain null bytes");
|
|
if (isWindowsNamedPipePath(ipc)) return ipc;
|
|
if (!ipc.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(ipc)) {
|
|
throw new Error(`sidecar ipc path must be absolute: ${ipc}`);
|
|
}
|
|
return ipc;
|
|
}
|
|
|
|
function assertKnownStampKeys(value: Record<string, unknown>, label: string): void {
|
|
assertKnownKeys(value, SIDECAR_STAMP_FIELDS, label);
|
|
}
|
|
|
|
export function normalizeSidecarStamp(input: unknown): SidecarStamp {
|
|
const value = assertObject(input, "sidecar stamp");
|
|
assertKnownStampKeys(value, "sidecar stamp");
|
|
return {
|
|
app: normalizeAppKey(value.app),
|
|
ipc: normalizeIpcPath(value.ipc),
|
|
mode: normalizeSidecarMode(value.mode),
|
|
namespace: normalizeNamespace(value.namespace),
|
|
source: normalizeSidecarSource(value.source),
|
|
};
|
|
}
|
|
|
|
export function normalizeSidecarStampCriteria(input: unknown = {}): SidecarStampCriteria {
|
|
const value = assertObject(input, "sidecar stamp criteria");
|
|
assertKnownStampKeys(value, "sidecar stamp criteria");
|
|
return {
|
|
...(value.app == null ? {} : { app: normalizeAppKey(value.app) }),
|
|
...(value.ipc == null ? {} : { ipc: normalizeIpcPath(value.ipc) }),
|
|
...(value.mode == null ? {} : { mode: normalizeSidecarMode(value.mode) }),
|
|
...(value.namespace == null ? {} : { namespace: normalizeNamespace(value.namespace) }),
|
|
...(value.source == null ? {} : { source: normalizeSidecarSource(value.source) }),
|
|
};
|
|
}
|
|
|
|
export function assertSidecarStamp(input: unknown): asserts input is SidecarStamp {
|
|
normalizeSidecarStamp(input);
|
|
}
|
|
|
|
function normalizeDesktopEvalInput(input: unknown): DesktopEvalInput {
|
|
const value = assertObject(input, "desktop eval input");
|
|
assertKnownKeys(value, ["expression"], "desktop eval input");
|
|
return { expression: normalizeNonEmptyString(value.expression, "desktop eval expression") };
|
|
}
|
|
|
|
function normalizeDesktopScreenshotInput(input: unknown): DesktopScreenshotInput {
|
|
const value = assertObject(input, "desktop screenshot input");
|
|
assertKnownKeys(value, ["path"], "desktop screenshot input");
|
|
return { path: normalizeNonEmptyString(value.path, "desktop screenshot path") };
|
|
}
|
|
|
|
function normalizeDesktopClickInput(input: unknown): DesktopClickInput {
|
|
const value = assertObject(input, "desktop click input");
|
|
assertKnownKeys(value, ["selector"], "desktop click input");
|
|
return { selector: normalizeNonEmptyString(value.selector, "desktop click selector") };
|
|
}
|
|
|
|
function normalizeRegisterDesktopAuthInput(input: unknown): RegisterDesktopAuthInput {
|
|
const value = assertObject(input, "register-desktop-auth input");
|
|
assertKnownKeys(value, ["secret"], "register-desktop-auth input");
|
|
const secret = normalizeNonEmptyString(value.secret, "register-desktop-auth secret");
|
|
// Reject anything that isn't base64-shaped — the wire format is a
|
|
// base64-encoded random buffer minted by the desktop main process. The
|
|
// daemon decodes it back to bytes for HMAC. Loose validation here, not
|
|
// length-pinned, so the encoding (base64 vs base64url) stays caller-driven.
|
|
if (!/^[A-Za-z0-9+/_=-]+$/.test(secret)) {
|
|
throw new Error("register-desktop-auth secret must be base64-encoded");
|
|
}
|
|
return { secret };
|
|
}
|
|
|
|
function normalizeMintImportTokenInput(input: unknown): MintImportTokenInput {
|
|
const value = assertObject(input, "mint-import-token input");
|
|
assertKnownKeys(value, ["baseDir"], "mint-import-token input");
|
|
return { baseDir: normalizeNonEmptyString(value.baseDir, "mint-import-token baseDir") };
|
|
}
|
|
|
|
function normalizeBoolean(value: unknown, label: string): boolean {
|
|
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
|
|
return value;
|
|
}
|
|
|
|
function normalizeDesktopExportPdfInput(input: unknown): DesktopExportPdfInput {
|
|
const value = assertObject(input, "desktop PDF export input");
|
|
assertKnownKeys(value, ["baseHref", "deck", "defaultFilename", "html", "title"], "desktop PDF export input");
|
|
return {
|
|
...(value.baseHref == null ? {} : { baseHref: normalizeNonEmptyString(value.baseHref, "desktop PDF export baseHref") }),
|
|
deck: normalizeBoolean(value.deck, "desktop PDF export deck"),
|
|
defaultFilename: normalizeNonEmptyString(value.defaultFilename, "desktop PDF export defaultFilename"),
|
|
html: normalizeNonEmptyString(value.html, "desktop PDF export html"),
|
|
title: normalizeNonEmptyString(value.title, "desktop PDF export title"),
|
|
};
|
|
}
|
|
|
|
function isDesktopUpdateAction(value: unknown): value is DesktopUpdateAction {
|
|
return Object.values(DESKTOP_UPDATE_ACTIONS).includes(value as DesktopUpdateAction);
|
|
}
|
|
|
|
function normalizeDesktopUpdateInput(input: unknown): DesktopUpdateInput {
|
|
const value = assertObject(input, "desktop update input");
|
|
assertKnownKeys(value, ["action"], "desktop update input");
|
|
if (!isDesktopUpdateAction(value.action)) {
|
|
throw new Error(`unsupported desktop update action: ${String(value.action)}`);
|
|
}
|
|
return { action: value.action };
|
|
}
|
|
|
|
function normalizeMessageType(value: unknown, label: string): string {
|
|
if (typeof value !== "string" || value.length === 0) {
|
|
throw new SidecarContractError(SIDECAR_ERROR_CODES.INVALID_MESSAGE, `${label} type must be a non-empty string`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function normalizeDaemonSidecarMessage(input: unknown): DaemonSidecarMessage {
|
|
const value = assertObject(input, "daemon sidecar message");
|
|
const type = normalizeMessageType(value.type, "daemon sidecar message");
|
|
if (type === SIDECAR_MESSAGES.STATUS || type === SIDECAR_MESSAGES.SHUTDOWN) {
|
|
assertKnownKeys(value, ["type"], "daemon sidecar message");
|
|
return { type };
|
|
}
|
|
if (type === SIDECAR_MESSAGES.REGISTER_DESKTOP_AUTH) {
|
|
assertKnownKeys(value, ["input", "type"], "daemon sidecar message");
|
|
return { input: normalizeRegisterDesktopAuthInput(value.input), type };
|
|
}
|
|
if (type === SIDECAR_MESSAGES.MINT_IMPORT_TOKEN) {
|
|
assertKnownKeys(value, ["input", "type"], "daemon sidecar message");
|
|
return { input: normalizeMintImportTokenInput(value.input), type };
|
|
}
|
|
throw new SidecarContractError(SIDECAR_ERROR_CODES.UNKNOWN_MESSAGE, `unknown daemon sidecar message: ${type}`);
|
|
}
|
|
|
|
export function normalizeWebSidecarMessage(input: unknown): WebSidecarMessage {
|
|
const value = assertObject(input, "web sidecar message");
|
|
const type = normalizeMessageType(value.type, "web sidecar message");
|
|
if (type === SIDECAR_MESSAGES.STATUS || type === SIDECAR_MESSAGES.SHUTDOWN) {
|
|
assertKnownKeys(value, ["type"], "web sidecar message");
|
|
return { type };
|
|
}
|
|
throw new SidecarContractError(SIDECAR_ERROR_CODES.UNKNOWN_MESSAGE, `unknown web sidecar message: ${type}`);
|
|
}
|
|
|
|
export function normalizeDesktopSidecarMessage(input: unknown): DesktopSidecarMessage {
|
|
const value = assertObject(input, "desktop sidecar message");
|
|
const type = normalizeMessageType(value.type, "desktop sidecar message");
|
|
switch (type) {
|
|
case SIDECAR_MESSAGES.STATUS:
|
|
case SIDECAR_MESSAGES.SHUTDOWN:
|
|
case SIDECAR_MESSAGES.CONSOLE:
|
|
assertKnownKeys(value, ["type"], "desktop sidecar message");
|
|
return { type };
|
|
case SIDECAR_MESSAGES.EVAL:
|
|
assertKnownKeys(value, ["input", "type"], "desktop sidecar message");
|
|
return { input: normalizeDesktopEvalInput(value.input), type };
|
|
case SIDECAR_MESSAGES.SCREENSHOT:
|
|
assertKnownKeys(value, ["input", "type"], "desktop sidecar message");
|
|
return { input: normalizeDesktopScreenshotInput(value.input), type };
|
|
case SIDECAR_MESSAGES.CLICK:
|
|
assertKnownKeys(value, ["input", "type"], "desktop sidecar message");
|
|
return { input: normalizeDesktopClickInput(value.input), type };
|
|
case SIDECAR_MESSAGES.EXPORT_PDF:
|
|
assertKnownKeys(value, ["input", "type"], "desktop sidecar message");
|
|
return { input: normalizeDesktopExportPdfInput(value.input), type };
|
|
case SIDECAR_MESSAGES.UPDATE:
|
|
assertKnownKeys(value, ["input", "type"], "desktop sidecar message");
|
|
return { input: normalizeDesktopUpdateInput(value.input), type };
|
|
default:
|
|
throw new SidecarContractError(SIDECAR_ERROR_CODES.UNKNOWN_MESSAGE, `unknown desktop sidecar message: ${type}`);
|
|
}
|
|
}
|
|
|
|
export const OPEN_DESIGN_SIDECAR_CONTRACT = Object.freeze({
|
|
appKeys: APP_KEYS,
|
|
defaults: SIDECAR_DEFAULTS,
|
|
env: SIDECAR_RUNTIME_ENV,
|
|
errorCodes: SIDECAR_ERROR_CODES,
|
|
messages: SIDECAR_MESSAGES,
|
|
modes: SIDECAR_MODES,
|
|
normalizeApp: normalizeAppKey,
|
|
normalizeNamespace,
|
|
normalizeSource: normalizeSidecarSource,
|
|
normalizeStamp: normalizeSidecarStamp,
|
|
normalizeStampCriteria: normalizeSidecarStampCriteria,
|
|
sources: SIDECAR_SOURCES,
|
|
stampFields: SIDECAR_STAMP_FIELDS,
|
|
stampFlags: SIDECAR_STAMP_FLAGS,
|
|
updateActions: DESKTOP_UPDATE_ACTIONS,
|
|
updateChannels: DESKTOP_UPDATE_CHANNELS,
|
|
updateModes: DESKTOP_UPDATE_MODES,
|
|
updateStates: DESKTOP_UPDATE_STATES,
|
|
} as const satisfies OpenDesignSidecarContract);
|