diff --git a/package-lock.json b/package-lock.json index 28d7f7ec..6e9afbcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,6 +72,7 @@ "version": "7.28.5", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -3676,6 +3677,7 @@ "version": "7.20.5", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -3777,6 +3779,7 @@ "version": "22.19.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3851,6 +3854,7 @@ "integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cac": "^6.7.14", "colorette": "^2.0.20", @@ -3933,6 +3937,7 @@ "version": "6.12.6", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4135,7 +4140,6 @@ "version": "5.3.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", @@ -4153,7 +4157,6 @@ "version": "2.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", @@ -4174,7 +4177,6 @@ "version": "2.3.8", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4188,14 +4190,12 @@ "node_modules/archiver-utils/node_modules/safe-buffer": { "version": "5.1.2", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/archiver-utils/node_modules/string_decoder": { "version": "1.1.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -4509,7 +4509,6 @@ "version": "4.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -4573,6 +4572,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5063,7 +5063,6 @@ "version": "4.1.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", @@ -5193,7 +5192,6 @@ "version": "1.2.2", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "crc32": "bin/crc32.njs" }, @@ -5205,7 +5203,6 @@ "version": "4.0.3", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" @@ -5571,6 +5568,7 @@ "version": "24.13.3", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", @@ -5737,7 +5735,6 @@ "version": "24.13.3", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "archiver": "^5.3.1", @@ -5749,7 +5746,6 @@ "version": "10.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -5763,7 +5759,6 @@ "version": "6.2.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -5775,7 +5770,6 @@ "version": "2.0.1", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -6493,8 +6487,7 @@ "node_modules/fs-constants": { "version": "1.0.0", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fs-extra": { "version": "8.1.0", @@ -7711,8 +7704,7 @@ "node_modules/isarray": { "version": "1.0.0", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.6", @@ -7762,6 +7754,7 @@ "version": "1.21.7", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -7893,7 +7886,6 @@ "version": "1.0.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readable-stream": "^2.0.5" }, @@ -7905,7 +7897,6 @@ "version": "2.3.8", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -7919,14 +7910,12 @@ "node_modules/lazystream/node_modules/safe-buffer": { "version": "5.1.2", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -7991,26 +7980,22 @@ "node_modules/lodash.defaults": { "version": "4.2.0", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.difference": { "version": "4.5.0", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.flatten": { "version": "4.4.0", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.sortby": { "version": "4.7.0", @@ -8022,8 +8007,7 @@ "node_modules/lodash.union": { "version": "4.6.0", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lowercase-keys": { "version": "2.0.0", @@ -8827,6 +8811,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8974,8 +8959,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/process-warning": { "version": "3.0.0", @@ -9224,7 +9208,6 @@ "version": "3.6.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -9238,7 +9221,6 @@ "version": "1.1.3", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "minimatch": "^5.1.0" } @@ -9541,6 +9523,7 @@ "version": "4.52.5", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9764,6 +9747,7 @@ "node_modules/seroval": { "version": "1.3.2", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -10087,6 +10071,7 @@ "node_modules/solid-js": { "version": "1.9.10", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", @@ -10227,7 +10212,6 @@ "version": "1.3.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -10561,7 +10545,6 @@ "version": "2.2.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -10754,6 +10737,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11003,6 +10987,7 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11350,6 +11335,7 @@ "version": "5.4.21", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -11834,6 +11820,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12028,6 +12015,7 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -12316,7 +12304,6 @@ "version": "4.1.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", @@ -12330,7 +12317,6 @@ "version": "3.0.4", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", @@ -12350,6 +12336,7 @@ "node_modules/zod": { "version": "3.25.76", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/packages/electron-app/electron/main/user-shell.ts b/packages/electron-app/electron/main/user-shell.ts index 95640da8..ee49e7c4 100644 --- a/packages/electron-app/electron/main/user-shell.ts +++ b/packages/electron-app/electron/main/user-shell.ts @@ -20,10 +20,24 @@ function getDefaultShellPath(): string { return "/bin/bash" } +function wrapCommandForShell(command: string, shellPath: string): string { + const shellName = path.basename(shellPath) + + if (shellName.includes("bash")) { + return 'if [ -f ~/.bashrc ]; then source ~/.bashrc >/dev/null 2>&1; fi; ' + command + } + + if (shellName.includes("zsh")) { + return 'if [ -f ~/.zshrc ]; then source ~/.zshrc >/dev/null 2>&1; fi; ' + command + } + + return command +} + function buildShellArgs(shellPath: string): string[] { const shellName = path.basename(shellPath) - if (shellName.includes("zsh") || shellName.includes("bash")) { - return ["-i", "-l", "-c"] + if (shellName.includes("zsh")) { + return ["-l", "-i", "-c"] } return ["-l", "-c"] } @@ -45,11 +59,12 @@ export function buildUserShellCommand(userCommand: string): ShellCommand { } const shellPath = getDefaultShellPath() + const script = wrapCommandForShell(userCommand, shellPath) const args = buildShellArgs(shellPath) return { command: shellPath, - args: [...args, userCommand], + args: [...args, script], } } diff --git a/packages/server/src/api-types.ts b/packages/server/src/api-types.ts index f46796b4..108ac071 100644 --- a/packages/server/src/api-types.ts +++ b/packages/server/src/api-types.ts @@ -337,6 +337,16 @@ export interface RemoteServerProbeResponse { errorCode?: string } +export interface RemoteProxySessionCreateRequest { + baseUrl: string + skipTlsVerify?: boolean +} + +export interface RemoteProxySessionCreateResponse { + sessionId: string + windowUrl: string +} + export type WorkspaceEventType = | "workspace.created" | "workspace.started" diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 72c98425..c021d220 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -21,6 +21,7 @@ import { launchInBrowser } from "./launcher" import { resolveUi } from "./ui/remote-ui" import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_COOKIE_NAME, DEFAULT_AUTH_USERNAME } from "./auth/manager" import { resolveHttpsOptions } from "./server/tls" +import { RemoteProxySessionManager } from "./server/remote-proxy" import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/network-addresses" import { startDevReleaseMonitor } from "./releases/dev-release-monitor" import { SpeechService } from "./speech/service" @@ -375,14 +376,15 @@ async function main() { }) : null - if (uiResolution.uiDevServerUrl && options.https) { - throw new InvalidArgumentError("UI dev proxy is only supported with --https=false --http=true") - } - const remoteAccessEnabled = options.host === "0.0.0.0" || !isLoopbackHost(options.host) const clientConnectionManager = new ClientConnectionManager(logger.child({ component: "client-connections" })) const pluginChannel = new PluginChannelManager(logger.child({ component: "plugin-channel" })) + const remoteProxySessionManager = new RemoteProxySessionManager({ + authManager, + logger: logger.child({ component: "remote-proxy" }), + httpsOptions: tlsResolution?.httpsOptions, + }) const voiceModeManager = new VoiceModeManager({ connections: clientConnectionManager, channel: pluginChannel, @@ -422,6 +424,7 @@ async function main() { clientConnectionManager, pluginChannel, voiceModeManager, + remoteProxySessionManager, uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR, uiDevServerUrl: uiResolution.uiDevServerUrl, logger, @@ -447,6 +450,7 @@ async function main() { clientConnectionManager, pluginChannel, voiceModeManager, + remoteProxySessionManager, uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR, uiDevServerUrl: undefined, logger, diff --git a/packages/server/src/server/__tests__/remote-proxy.test.ts b/packages/server/src/server/__tests__/remote-proxy.test.ts new file mode 100644 index 00000000..5daac01a --- /dev/null +++ b/packages/server/src/server/__tests__/remote-proxy.test.ts @@ -0,0 +1,248 @@ +import assert from "node:assert/strict" +import { after, afterEach, describe, it } from "node:test" +import fs from "node:fs" +import http, { type IncomingMessage, type ServerResponse } from "node:http" +import os from "node:os" +import path from "node:path" + +import { Agent, fetch } from "undici" + +import type { AuthManager } from "../../auth/manager" +import type { Logger } from "../../logger" +import { RemoteProxySessionManager } from "../remote-proxy" +import { resolveHttpsOptions } from "../tls" + +const sharedTempDir = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-remote-proxy-test-")) +const sharedTls = resolveHttpsOptions({ + enabled: true, + configDir: sharedTempDir, + host: "127.0.0.1", + logger: createStubLogger(), +}) + +if (!sharedTls) { + throw new Error("Failed to generate HTTPS options for remote proxy tests") +} + +const sharedHttpsOptions = sharedTls.httpsOptions + +const httpsDispatcher = new Agent({ connect: { rejectUnauthorized: false } }) +const managers = new Set() + +afterEach(async () => { + for (const manager of managers) { + await disposeManager(manager) + } + managers.clear() +}) + +after(() => { + fs.rmSync(sharedTempDir, { recursive: true, force: true }) + httpsDispatcher.close().catch(() => {}) +}) + +describe("RemoteProxySessionManager", () => { + it("blocks proxying before activation and keeps bootstrap tokens scoped per session", async () => { + await withUpstreamServer(async (upstreamBaseUrl) => { + const manager = createSessionManager() + const session1 = await createSession(manager, `${upstreamBaseUrl}/base`) + const session2 = await createSession(manager, `${upstreamBaseUrl}/base`) + + const blocked = await proxyFetch(`${session1.proxyOrigin}/status`) + assert.equal(blocked.status, 403) + + const wrongTokenResponse = await proxyFetch(`${session1.proxyOrigin}/__codenomad/api/auth/token`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ token: session2.token }), + }) + assert.equal(wrongTokenResponse.status, 401) + + assert.equal(await activateSession(session1), true) + assert.equal(await activateSession(session2), true) + }, (req, res) => { + res.writeHead(200, { "content-type": "text/plain" }) + res.end(req.url ?? "") + }) + }) + + it("preserves remote base paths and rewrites same-origin redirects to the local proxy origin", async () => { + await withUpstreamServer(async (upstreamBaseUrl) => { + const manager = createSessionManager() + const session = await createSession(manager, `${upstreamBaseUrl}/base`) + + await activateSession(session) + + const apiResponse = await proxyFetch(`${session.proxyOrigin}/api/auth/status?foo=bar`) + assert.equal(apiResponse.status, 200) + assert.equal(await apiResponse.text(), "/base/api/auth/status?foo=bar") + + const redirectResponse = await proxyFetch(`${session.proxyOrigin}/redirect`, { redirect: "manual" }) + assert.equal(redirectResponse.status, 302) + assert.equal(redirectResponse.headers.get("location"), `${session.proxyOrigin}/base/after?ok=1`) + }, (req, res) => { + const requestUrl = req.url ?? "" + if (requestUrl === "/base/redirect") { + res.writeHead(302, { location: "/base/after?ok=1" }) + res.end() + return + } + + res.writeHead(200, { "content-type": "text/plain" }) + res.end(requestUrl) + }) + }) + + it("rewrites set-cookie names for the proxy and restores cookie names on proxied requests", async () => { + await withUpstreamServer(async (upstreamBaseUrl) => { + const manager = createSessionManager() + const session = await createSession(manager, `${upstreamBaseUrl}/base`) + + await activateSession(session) + + const loginResponse = await proxyFetch(`${session.proxyOrigin}/login`) + assert.equal(loginResponse.status, 200) + const setCookie = getSetCookie(loginResponse)[0] + + assert.match(setCookie, /^cnrp_[0-9a-f]+_session=abc123/i) + assert.doesNotMatch(setCookie, /domain=/i) + + const cookieHeader = setCookie.split(";", 1)[0] + const whoamiResponse = await proxyFetch(`${session.proxyOrigin}/whoami`, { + headers: { cookie: cookieHeader }, + }) + + assert.equal(await whoamiResponse.text(), "session=abc123") + }, (req, res) => { + const requestUrl = req.url ?? "" + if (requestUrl === "/base/login") { + res.writeHead(200, { + "content-type": "text/plain", + "set-cookie": "session=abc123; Path=/; Secure; HttpOnly; Domain=127.0.0.1", + }) + res.end("ok") + return + } + + if (requestUrl === "/base/whoami") { + res.writeHead(200, { "content-type": "text/plain" }) + res.end(req.headers.cookie ?? "") + return + } + + res.writeHead(404, { "content-type": "text/plain" }) + res.end(requestUrl) + }) + }) + + it("supports explicit deletion and idle cleanup of sessions", async () => { + await withUpstreamServer(async (upstreamBaseUrl) => { + const manager = createSessionManager() + const session = await createSession(manager, `${upstreamBaseUrl}/base`) + + assert.equal(await manager.deleteSession(session.sessionId), true) + assert.equal(await manager.deleteSession(session.sessionId), false) + + const session3 = await createSession(manager, `${upstreamBaseUrl}/base`) + const internalSessions = (manager as any).sessions as Map + const internalCleanup = (manager as any).cleanupExpiredSessions as () => Promise + + internalSessions.get(session3.sessionId)!.lastAccessAt = Date.now() - 31 * 60_000 + await internalCleanup.call(manager) + + assert.equal(internalSessions.has(session3.sessionId), false) + assert.equal(await manager.deleteSession(session3.sessionId), false) + }, (_req, res) => { + res.writeHead(200, { "content-type": "text/plain" }) + res.end("ok") + }) + }) +}) + +function createSessionManager() { + const manager = new RemoteProxySessionManager({ + authManager: { + isLoopbackRequest: () => true, + } as unknown as AuthManager, + logger: createStubLogger(), + httpsOptions: sharedHttpsOptions, + }) + managers.add(manager) + return manager +} + +async function createSession(manager: RemoteProxySessionManager, baseUrl: string) { + const created = await manager.createSession(baseUrl, false) + const windowUrl = new URL(created.windowUrl) + return { + sessionId: created.sessionId, + windowUrl, + proxyOrigin: windowUrl.origin, + token: decodeURIComponent(windowUrl.hash.replace(/^#/, "")), + } +} + +async function activateSession(session: { proxyOrigin: string; token: string }) { + const response = await proxyFetch(`${session.proxyOrigin}/__codenomad/api/auth/token`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ token: session.token }), + }) + if (!response.ok) { + return false + } + const body = (await response.json()) as { ok?: boolean } + return body.ok === true +} + +function getSetCookie(response: Awaited>): string[] { + const values = (response.headers as any).getSetCookie?.() as string[] | undefined + if (Array.isArray(values) && values.length > 0) { + return values + } + const fallback = response.headers.get("set-cookie") + return fallback ? [fallback] : [] +} + +async function proxyFetch(url: string, init?: Parameters[1]) { + return fetch(url, { dispatcher: httpsDispatcher, ...init }) +} + +async function disposeManager(manager: RemoteProxySessionManager) { + const sessions = Array.from(((manager as any).sessions as Map).keys()) + for (const sessionId of sessions) { + await manager.deleteSession(sessionId) + } + clearInterval((manager as any).cleanupTimer as NodeJS.Timeout) +} + +async function withUpstreamServer( + callback: (baseUrl: string) => Promise, + handler: (req: IncomingMessage, res: ServerResponse) => void, +) { + const server = http.createServer(handler) + await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve())) + + try { + const address = server.address() + if (!address || typeof address === "string") { + throw new Error("Failed to resolve upstream server address") + } + await callback(`http://127.0.0.1:${address.port}`) + } finally { + await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))) + } +} + +function createStubLogger(): Logger { + const logger = { + info() {}, + warn() {}, + error() {}, + child() { + return logger + }, + } + + return logger as unknown as Logger +} diff --git a/packages/server/src/server/http-server.ts b/packages/server/src/server/http-server.ts index 9d4e0e1a..cf7dae36 100644 --- a/packages/server/src/server/http-server.ts +++ b/packages/server/src/server/http-server.ts @@ -26,6 +26,7 @@ import { registerBackgroundProcessRoutes } from "./routes/background-processes" import { registerWorktreeRoutes } from "./routes/worktrees" import { registerSpeechRoutes } from "./routes/speech" import { registerRemoteServerRoutes } from "./routes/remote-servers" +import { registerRemoteProxyRoutes } from "./routes/remote-proxy" import { registerSideCarRoutes } from "./routes/sidecars" import { ServerMeta } from "../api-types" import { InstanceStore } from "../storage/instance-store" @@ -38,6 +39,7 @@ import { ClientConnectionManager } from "../clients/connection-manager" import { PluginChannelManager } from "../plugins/channel" import { VoiceModeManager } from "../plugins/voice-mode" import type { SideCarManager } from "../sidecars/manager" +import type { RemoteProxySessionManager } from "./remote-proxy" interface HttpServerDeps { bindHost: string @@ -58,6 +60,7 @@ interface HttpServerDeps { clientConnectionManager: ClientConnectionManager pluginChannel: PluginChannelManager voiceModeManager: VoiceModeManager + remoteProxySessionManager: RemoteProxySessionManager uiStaticDir: string uiDevServerUrl?: string logger: Logger @@ -199,7 +202,12 @@ export function createHttpServer(deps: HttpServerDeps) { publicPagePaths.add("/auth/token") } - if (publicApiPaths.has(pathname) || publicPagePaths.has(pathname)) { + const isLoopbackRemoteProxyDelete = + request.method === "DELETE" && + pathname.startsWith("/api/remote-proxy/sessions/") && + deps.authManager.isLoopbackRequest(request) + + if (publicApiPaths.has(pathname) || publicPagePaths.has(pathname) || isLoopbackRemoteProxyDelete) { done() return } @@ -274,6 +282,7 @@ export function createHttpServer(deps: HttpServerDeps) { workspaceManager: deps.workspaceManager, }) registerRemoteServerRoutes(app, { logger: apiLogger }) + registerRemoteProxyRoutes(app, { logger: proxyLogger, sessionManager: deps.remoteProxySessionManager }) registerSpeechRoutes(app, { speechService: deps.speechService }) registerSideCarRoutes(app, { sidecarManager: deps.sidecarManager }) registerSideCarProxyRoutes(app, { sidecarManager: deps.sidecarManager, logger: proxyLogger }) diff --git a/packages/server/src/server/remote-proxy.ts b/packages/server/src/server/remote-proxy.ts new file mode 100644 index 00000000..93e6ab5e --- /dev/null +++ b/packages/server/src/server/remote-proxy.ts @@ -0,0 +1,566 @@ +import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify" +import { randomBytes, randomUUID } from "crypto" +import { Readable } from "stream" +import { pipeline } from "stream/promises" +import { Agent, fetch } from "undici" +import type { AuthManager } from "../auth/manager" +import type { Logger } from "../logger" + +const LOOPBACK_HOST = "127.0.0.1" +const BOOTSTRAP_PAGE_PATH = "/__codenomad/auth/token" +const BOOTSTRAP_EXCHANGE_PATH = "/__codenomad/api/auth/token" +const SESSION_IDLE_TTL_MS = 30 * 60_000 + +interface RemoteProxySession { + id: string + bootstrapToken: string + targetBaseUrl: URL + skipTlsVerify: boolean + localBaseUrl: URL + entryUrl: URL + bootstrapUrl: string + activated: boolean + cookiePrefix: string + app: FastifyInstance + dispatcher?: Agent + createdAt: number + lastAccessAt: number +} + +export interface RemoteProxySessionManagerOptions { + authManager: AuthManager + logger: Logger + httpsOptions?: { key: string | Buffer; cert: string | Buffer; ca?: string | Buffer } +} + +export interface RemoteProxySessionCreateResult { + sessionId: string + windowUrl: string +} + +export class RemoteProxySessionManager { + private readonly sessions = new Map() + private readonly cleanupTimer: NodeJS.Timeout + + constructor(private readonly options: RemoteProxySessionManagerOptions) { + this.cleanupTimer = setInterval(() => { + void this.cleanupExpiredSessions() + }, 60_000) + this.cleanupTimer.unref() + } + + async createSession(baseUrl: string, skipTlsVerify: boolean): Promise { + if (!this.options.httpsOptions) { + throw new Error("Local HTTPS is required for remote proxy sessions") + } + + const targetBaseUrl = normalizeBaseUrl(baseUrl) + const sessionId = randomUUID() + const bootstrapToken = randomBytes(32).toString("base64url") + const dispatcher = skipTlsVerify ? new Agent({ connect: { rejectUnauthorized: false } }) : undefined + const app = Fastify({ logger: false, https: this.options.httpsOptions }) + let session: RemoteProxySession | null = null + + app.removeAllContentTypeParsers() + // Preserve raw request bodies for proxying while still letting token JSON parse from Buffer. + app.addContentTypeParser("*", { parseAs: "buffer" }, (_req, body, done) => done(null, body)) + + app.get(BOOTSTRAP_PAGE_PATH, async (request, reply) => { + if (!this.options.authManager.isLoopbackRequest(request)) { + reply.code(404).send({ error: "Not found" }) + return + } + + reply.header("Cache-Control", "no-store") + reply.header("Pragma", "no-cache") + reply.header("Expires", "0") + reply.type("text/html").send(buildBootstrapPageHtml()) + }) + + app.post(BOOTSTRAP_EXCHANGE_PATH, async (request, reply) => { + if (!this.options.authManager.isLoopbackRequest(request)) { + reply.code(404).send({ error: "Not found" }) + return + } + + if (!session) { + reply.code(503).send({ error: "Remote proxy session is unavailable" }) + return + } + + const body = parseTokenBody(request.body) + if (body.token !== session.bootstrapToken) { + reply.code(401).send({ error: "Invalid token" }) + return + } + + session.activated = true + session.lastAccessAt = Date.now() + reply.send({ ok: true }) + }) + + app.all("/*", async (request, reply) => { + if (!session) { + reply.code(503).send({ error: "Remote proxy session is unavailable" }) + return + } + + if (!session.activated) { + reply.code(403).send({ error: "Remote proxy session is not activated" }) + return + } + + session.lastAccessAt = Date.now() + await proxyRequest({ request, reply, session, logger: this.options.logger }) + }) + + app.setNotFoundHandler(async (request, reply) => { + if (!session) { + reply.code(503).send({ error: "Remote proxy session is unavailable" }) + return + } + + if (!session.activated) { + reply.code(403).send({ error: "Remote proxy session is not activated" }) + return + } + + session.lastAccessAt = Date.now() + await proxyRequest({ request, reply, session, logger: this.options.logger }) + }) + + const addressInfo = await app.listen({ host: LOOPBACK_HOST, port: 0 }) + const address = new URL(addressInfo) + const localBaseUrl = new URL(`https://${LOOPBACK_HOST}:${address.port}`) + const entryUrl = new URL(targetBaseUrl.pathname || "/", localBaseUrl) + const returnTo = buildReturnToTarget(entryUrl) + + session = { + id: sessionId, + bootstrapToken, + targetBaseUrl, + skipTlsVerify, + localBaseUrl, + entryUrl, + bootstrapUrl: `${localBaseUrl.origin}${BOOTSTRAP_PAGE_PATH}?returnTo=${encodeURIComponent(returnTo)}#${encodeURIComponent(bootstrapToken)}`, + activated: false, + cookiePrefix: `cnrp_${randomBytes(6).toString("hex")}_`, + app, + dispatcher, + createdAt: Date.now(), + lastAccessAt: Date.now(), + } + + this.sessions.set(sessionId, session) + this.options.logger.info( + { sessionId, targetBaseUrl: targetBaseUrl.toString(), localBaseUrl: localBaseUrl.toString() }, + "Created remote proxy session", + ) + + return { sessionId, windowUrl: session.bootstrapUrl } + } + + async deleteSession(sessionId: string): Promise { + return this.disposeSession(sessionId) + } + + private async cleanupExpiredSessions() { + const now = Date.now() + for (const session of Array.from(this.sessions.values())) { + if (now - session.lastAccessAt <= SESSION_IDLE_TTL_MS) { + continue + } + await this.disposeSession(session.id) + } + } + + private async disposeSession(sessionId: string): Promise { + const session = this.sessions.get(sessionId) + if (!session) { + return false + } + + this.sessions.delete(sessionId) + session.dispatcher?.close().catch(() => {}) + await session.app.close().catch(() => {}) + this.options.logger.info({ sessionId }, "Disposed remote proxy session") + return true + } +} + +function normalizeBaseUrl(input: string): URL { + const parsed = new URL(input.trim()) + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error("Server URL must use http:// or https://") + } + + parsed.hash = "" + parsed.search = "" + parsed.pathname = parsed.pathname === "/" ? "/" : parsed.pathname.replace(/\/+$/, "") || "/" + return parsed +} + +function buildReturnToTarget(entryUrl: URL): string { + const query = entryUrl.search ? entryUrl.search : "" + return `${entryUrl.pathname || "/"}${query}` +} + +function buildBootstrapPageHtml(): string { + return ` + + + + + CodeNomad + + + +
+

Connecting...

+

Finalizing local authentication.

+
+
+ + +` +} + +function parseTokenBody(body: unknown): { token: string } { + const value = normalizeJsonBody(body) as { token?: unknown } | null | undefined + const token = typeof value?.token === "string" ? value.token.trim() : "" + if (!token) { + throw new Error("Missing bootstrap token") + } + return { token } +} + +function normalizeJsonBody(body: unknown): unknown { + if (Buffer.isBuffer(body)) { + return JSON.parse(body.toString("utf-8")) + } + if (typeof body === "string") { + return JSON.parse(body) + } + return body +} + +function toRequestBody(body: unknown): any { + if (body == null) { + return undefined + } + if (Buffer.isBuffer(body) || typeof body === "string" || body instanceof Uint8Array) { + return body + } + return JSON.stringify(body) +} + +async function proxyRequest(args: { + request: FastifyRequest + reply: FastifyReply + session: RemoteProxySession + logger: Logger +}) { + const { request, reply, session, logger } = args + const upstreamUrl = buildUpstreamUrl(session.targetBaseUrl, request.raw.url ?? request.url) + const headers = filterRequestHeaders(request.headers, session) + + const init: any = { + method: request.method, + headers, + dispatcher: session.dispatcher, + redirect: "manual", + } + + if (request.method !== "GET" && request.method !== "HEAD") { + const body = toRequestBody(request.body) + if (body !== undefined) { + init.body = body + init.duplex = "half" + } + } + + try { + const response = await fetch(upstreamUrl, init as any) + reply.code(response.status) + applyResponseHeaders(reply, response, session) + + if (!response.body || request.method === "HEAD") { + reply.send() + return + } + + reply.hijack() + reply.raw.writeHead(reply.statusCode, toOutgoingHeaders(reply.getHeaders())) + await pipeline(Readable.fromWeb(response.body as any), reply.raw) + } catch (error) { + logger.error({ err: error, upstreamUrl }, "Failed to proxy remote session request") + if (!reply.sent) { + reply.code(502).send({ error: "Remote proxy request failed" }) + } + } +} + +function buildUpstreamUrl(baseUrl: URL, rawUrl: string): string { + const parsed = new URL(rawUrl, "https://localhost") + const url = new URL(baseUrl.toString()) + url.pathname = rewriteRequestPath(baseUrl, parsed.pathname) + url.search = stripInternalQuery(parsed.search) + url.hash = "" + return url.toString() +} + +function rewriteRequestPath(baseUrl: URL, requestPath: string): string { + const basePath = normalizedBasePath(baseUrl) + if (basePath === "/") { + return requestPath + } + + if (requestPath === "/") { + return basePath + } + + if (pathHasBasePrefix(basePath, requestPath)) { + return requestPath + } + + return `${basePath}${requestPath}` +} + +function normalizedBasePath(baseUrl: URL): string { + return baseUrl.pathname || "/" +} + +function pathHasBasePrefix(basePath: string, requestPath: string): boolean { + return requestPath === basePath || requestPath.startsWith(`${basePath}/`) +} + +function stripInternalQuery(search: string): string { + if (!search || search === "?") { + return "" + } + return search +} + +function filterRequestHeaders( + headers: FastifyRequest["headers"], + session: RemoteProxySession, +): Record { + const next: Record = {} + for (const [key, value] of Object.entries(headers ?? {})) { + if (!value) continue + const lower = key.toLowerCase() + if ( + isHopByHopHeader(lower) || + lower === "host" || + lower === "content-length" || + lower === "accept-encoding" + ) { + continue + } + if (lower === "origin") { + next[key] = session.targetBaseUrl.origin + continue + } + if (lower === "referer") { + const rewritten = rewriteRefererHeader(Array.isArray(value) ? value[0] : value, session.targetBaseUrl) + if (rewritten) { + next[key] = rewritten + } + continue + } + if (lower === "cookie") { + const rewritten = rewriteRequestCookieHeader(Array.isArray(value) ? value.join("; ") : value, session.cookiePrefix) + if (rewritten) { + next[key] = rewritten + } + continue + } + next[key] = Array.isArray(value) ? value.join(",") : value + } + + next.host = session.targetBaseUrl.port ? `${session.targetBaseUrl.hostname}:${session.targetBaseUrl.port}` : session.targetBaseUrl.hostname + if (!next.origin) { + next.origin = session.targetBaseUrl.origin + } + return next +} + +function rewriteRefererHeader(referer: string | undefined, targetBaseUrl: URL): string | null { + if (!referer) { + return null + } + + try { + const parsed = new URL(referer) + const rewritten = new URL(targetBaseUrl.toString()) + rewritten.pathname = rewriteRequestPath(targetBaseUrl, parsed.pathname) + rewritten.search = parsed.search + rewritten.hash = parsed.hash + return rewritten.toString() + } catch { + return null + } +} + +function applyResponseHeaders(reply: FastifyReply, response: any, session: RemoteProxySession) { + const setCookie = (response.headers as any).getSetCookie?.() as string[] | undefined + if (Array.isArray(setCookie)) { + for (const cookie of setCookie) { + reply.header("set-cookie", rewriteSetCookie(cookie, session.cookiePrefix)) + } + } + + response.headers.forEach((value: string, key: string) => { + const lower = key.toLowerCase() + if ( + isHopByHopHeader(lower) || + lower === "set-cookie" || + lower === "content-length" || + lower === "content-encoding" + ) { + return + } + + if (lower === "location") { + reply.header(key, rewriteLocation(value, session.targetBaseUrl, session.localBaseUrl)) + return + } + + reply.header(key, value) + }) +} + +function toOutgoingHeaders(headers: ReturnType): Record { + const next: Record = {} + for (const [key, value] of Object.entries(headers)) { + if (value === undefined) { + continue + } + next[key] = Array.isArray(value) ? value.map(String) : String(value) + } + return next +} + +function rewriteSetCookie(cookie: string, cookiePrefix: string): string { + const parts = cookie.split(";").map((part) => part.trim()) + const first = parts.shift() ?? "" + const separator = first.indexOf("=") + if (separator <= 0) { + return cookie + } + + const name = first.slice(0, separator).trim() + const value = first.slice(separator + 1) + const rewritten = [`${cookiePrefix}${name}=${value}`] + for (const part of parts) { + if (part.slice(0, 7).toLowerCase().startsWith("domain=")) { + continue + } + rewritten.push(part) + } + return rewritten.join("; ") +} + +function rewriteRequestCookieHeader(cookieHeader: string, cookiePrefix: string): string { + const next: string[] = [] + for (const rawPart of cookieHeader.split(";")) { + const part = rawPart.trim() + if (!part) continue + const separator = part.indexOf("=") + if (separator <= 0) continue + const name = part.slice(0, separator).trim() + const value = part.slice(separator + 1) + if (!name.startsWith(cookiePrefix)) { + continue + } + next.push(`${name.slice(cookiePrefix.length)}=${value}`) + } + return next.join("; ") +} + +function rewriteLocation(location: string, targetBaseUrl: URL, localBaseUrl: URL): string { + try { + const parsed = new URL(location, targetBaseUrl) + if (parsed.origin !== targetBaseUrl.origin) { + return location + } + + const rewritten = new URL(localBaseUrl.toString()) + rewritten.pathname = parsed.pathname + rewritten.search = parsed.search + rewritten.hash = parsed.hash + return rewritten.toString() + } catch { + return location + } +} + +function isHopByHopHeader(name: string): boolean { + return new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + ]).has(name) +} diff --git a/packages/server/src/server/routes/remote-proxy.ts b/packages/server/src/server/routes/remote-proxy.ts new file mode 100644 index 00000000..a26cdc10 --- /dev/null +++ b/packages/server/src/server/routes/remote-proxy.ts @@ -0,0 +1,54 @@ +import type { FastifyInstance } from "fastify" +import { z } from "zod" +import type { RemoteProxySessionCreateResponse } from "../../api-types" +import { isLoopbackAddress } from "../../auth/http-auth" +import type { Logger } from "../../logger" +import type { RemoteProxySessionManager } from "../remote-proxy" + +interface RouteDeps { + logger: Logger + sessionManager: RemoteProxySessionManager +} + +const CreateSessionSchema = z.object({ + baseUrl: z.string().min(1), + skipTlsVerify: z.boolean().optional(), +}) + +const SessionParamsSchema = z.object({ + id: z.string().uuid(), +}) + +export function registerRemoteProxyRoutes(app: FastifyInstance, deps: RouteDeps) { + app.post("/api/remote-proxy/sessions", async (request, reply): Promise => { + try { + const body = CreateSessionSchema.parse(request.body ?? {}) + return await deps.sessionManager.createSession(body.baseUrl, Boolean(body.skipTlsVerify)) + } catch (error) { + deps.logger.warn({ err: error }, "Failed to create remote proxy session") + reply.code(400) + return { error: error instanceof Error ? error.message : "Failed to create remote proxy session" } + } + }) + + app.delete("/api/remote-proxy/sessions/:id", async (request, reply): Promise<{ ok: boolean } | { error: string }> => { + if (!isLoopbackAddress(request.socket.remoteAddress)) { + reply.code(404) + return { error: "Not found" } + } + + try { + const params = SessionParamsSchema.parse(request.params ?? {}) + const deleted = await deps.sessionManager.deleteSession(params.id) + if (!deleted) { + reply.code(404) + return { error: "Remote proxy session not found" } + } + return { ok: true } + } catch (error) { + deps.logger.warn({ err: error }, "Failed to delete remote proxy session") + reply.code(400) + return { error: error instanceof Error ? error.message : "Failed to delete remote proxy session" } + } + }) +} diff --git a/packages/tauri-app/Cargo.lock b/packages/tauri-app/Cargo.lock index a16d13f4..e4a73d97 100644 --- a/packages/tauri-app/Cargo.lock +++ b/packages/tauri-app/Cargo.lock @@ -213,6 +213,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.21.7" @@ -408,6 +430,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -444,6 +468,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -456,17 +486,28 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "codenomad-tauri" version = "0.14.0" dependencies = [ "anyhow", + "base64 0.22.1", "dirs 5.0.1", "keepawake", "libc", - "once_cell", "parking_lot", "regex", + "reqwest 0.12.28", + "rustls", "serde", "serde_json", "serde_yaml", @@ -476,8 +517,8 @@ dependencies = [ "tauri-plugin-global-shortcut", "tauri-plugin-notification", "tauri-plugin-opener", - "thiserror 1.0.69", "url", + "webkit2gtk", "which", "windows-sys 0.59.0", ] @@ -969,6 +1010,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -1139,6 +1189,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futf" version = "0.1.5" @@ -1379,8 +1435,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1390,9 +1448,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1574,6 +1634,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1699,6 +1778,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1710,6 +1790,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1999,6 +2096,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -2157,6 +2264,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -2995,6 +3108,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -3212,6 +3380,50 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -3242,7 +3454,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] @@ -3270,6 +3482,20 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -3311,6 +3537,44 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3531,6 +3795,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.18.0" @@ -3792,6 +4068,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3943,7 +4225,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4367,6 +4649,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.50.0" @@ -4381,6 +4678,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4691,6 +4998,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4902,6 +5215,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -4937,6 +5263,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.3" @@ -4993,6 +5329,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -5286,6 +5631,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5927,6 +6281,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/packages/tauri-app/scripts/prebuild.js b/packages/tauri-app/scripts/prebuild.js index 76554a13..a234af46 100644 --- a/packages/tauri-app/scripts/prebuild.js +++ b/packages/tauri-app/scripts/prebuild.js @@ -37,6 +37,12 @@ const braceExpansionPath = path.join( "package.json", ) +const serverBuildDependencyPaths = [ + path.join(serverRoot, "node_modules", "typescript", "package.json"), + path.join(serverRoot, "node_modules", "@types", "node-forge", "package.json"), + path.join(serverRoot, "node_modules", "@types", "yauzl", "package.json"), +] + const viteBinPath = path.join(uiRoot, "node_modules", ".bin", "vite") async function ensureMonacoAssets() { @@ -98,7 +104,7 @@ function syncServerUiBundle() { } function ensureServerDevDependencies() { - if (fs.existsSync(braceExpansionPath)) { + if (serverBuildDependencyPaths.every((filePath) => fs.existsSync(filePath))) { return } diff --git a/packages/tauri-app/src-tauri/Cargo.toml b/packages/tauri-app/src-tauri/Cargo.toml index e3b61de3..940e9fbf 100644 --- a/packages/tauri-app/src-tauri/Cargo.toml +++ b/packages/tauri-app/src-tauri/Cargo.toml @@ -12,10 +12,11 @@ tauri = { version = "2.5.2", features = [ "devtools"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" +base64 = "0.22" +rustls = { version = "0.23", features = ["ring"] } +reqwest = { version = "0.12", default-features = false, features = ["http2", "charset", "json", "stream", "rustls-tls"] } regex = "1" -once_cell = "1" parking_lot = "0.12" -thiserror = "1" anyhow = "1" which = "4" libc = "0.2" @@ -28,4 +29,7 @@ url = "2" tauri-plugin-notification = "2" [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects"] } +windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security_Cryptography", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects"] } + +[target.'cfg(target_os = "linux")'.dependencies] +webkit2gtk = "2.0.2" diff --git a/packages/tauri-app/src-tauri/gen/schemas/windows-schema.json b/packages/tauri-app/src-tauri/gen/schemas/windows-schema.json new file mode 100644 index 00000000..f7ab8174 --- /dev/null +++ b/packages/tauri-app/src-tauri/gen/schemas/windows-schema.json @@ -0,0 +1,2807 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "if": { + "properties": { + "identifier": { + "anyOf": [ + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + } + ] + } + } + }, + "then": { + "properties": { + "allow": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + }, + "deny": { + "items": { + "title": "OpenerScopeEntry", + "description": "Opener scope entry.", + "anyOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "app": { + "description": "An application to open this url with, for example: firefox.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "url": { + "description": "A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "app": { + "description": "An application to open this path with, for example: xdg-open.", + "allOf": [ + { + "$ref": "#/definitions/Application" + } + ] + }, + "path": { + "description": "A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "type": "string" + } + } + } + ] + } + } + } + }, + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + }, + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope." + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope." + }, + { + "description": "Denies the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." + }, + { + "description": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n", + "type": "string", + "const": "global-shortcut:default", + "markdownDescription": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n" + }, + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Enables the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register-all", + "markdownDescription": "Enables the register_all command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Enables the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister-all", + "markdownDescription": "Enables the unregister_all command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Denies the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register-all", + "markdownDescription": "Denies the register_all command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, + { + "description": "Denies the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister-all", + "markdownDescription": "Denies the unregister_all command without any pre-configured scope." + }, + { + "description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`", + "type": "string", + "const": "notification:default", + "markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`" + }, + { + "description": "Enables the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-batch", + "markdownDescription": "Enables the batch command without any pre-configured scope." + }, + { + "description": "Enables the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-cancel", + "markdownDescription": "Enables the cancel command without any pre-configured scope." + }, + { + "description": "Enables the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-check-permissions", + "markdownDescription": "Enables the check_permissions command without any pre-configured scope." + }, + { + "description": "Enables the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-create-channel", + "markdownDescription": "Enables the create_channel command without any pre-configured scope." + }, + { + "description": "Enables the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-delete-channel", + "markdownDescription": "Enables the delete_channel command without any pre-configured scope." + }, + { + "description": "Enables the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-active", + "markdownDescription": "Enables the get_active command without any pre-configured scope." + }, + { + "description": "Enables the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-get-pending", + "markdownDescription": "Enables the get_pending command without any pre-configured scope." + }, + { + "description": "Enables the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-is-permission-granted", + "markdownDescription": "Enables the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Enables the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-list-channels", + "markdownDescription": "Enables the list_channels command without any pre-configured scope." + }, + { + "description": "Enables the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-notify", + "markdownDescription": "Enables the notify command without any pre-configured scope." + }, + { + "description": "Enables the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-permission-state", + "markdownDescription": "Enables the permission_state command without any pre-configured scope." + }, + { + "description": "Enables the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-action-types", + "markdownDescription": "Enables the register_action_types command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-remove-active", + "markdownDescription": "Enables the remove_active command without any pre-configured scope." + }, + { + "description": "Enables the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-request-permission", + "markdownDescription": "Enables the request_permission command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "notification:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Denies the batch command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-batch", + "markdownDescription": "Denies the batch command without any pre-configured scope." + }, + { + "description": "Denies the cancel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-cancel", + "markdownDescription": "Denies the cancel command without any pre-configured scope." + }, + { + "description": "Denies the check_permissions command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-check-permissions", + "markdownDescription": "Denies the check_permissions command without any pre-configured scope." + }, + { + "description": "Denies the create_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-create-channel", + "markdownDescription": "Denies the create_channel command without any pre-configured scope." + }, + { + "description": "Denies the delete_channel command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-delete-channel", + "markdownDescription": "Denies the delete_channel command without any pre-configured scope." + }, + { + "description": "Denies the get_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-active", + "markdownDescription": "Denies the get_active command without any pre-configured scope." + }, + { + "description": "Denies the get_pending command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-get-pending", + "markdownDescription": "Denies the get_pending command without any pre-configured scope." + }, + { + "description": "Denies the is_permission_granted command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-is-permission-granted", + "markdownDescription": "Denies the is_permission_granted command without any pre-configured scope." + }, + { + "description": "Denies the list_channels command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-list-channels", + "markdownDescription": "Denies the list_channels command without any pre-configured scope." + }, + { + "description": "Denies the notify command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-notify", + "markdownDescription": "Denies the notify command without any pre-configured scope." + }, + { + "description": "Denies the permission_state command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-permission-state", + "markdownDescription": "Denies the permission_state command without any pre-configured scope." + }, + { + "description": "Denies the register_action_types command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-action-types", + "markdownDescription": "Denies the register_action_types command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_active command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-remove-active", + "markdownDescription": "Denies the remove_active command without any pre-configured scope." + }, + { + "description": "Denies the request_permission command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-request-permission", + "markdownDescription": "Denies the request_permission command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "notification:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`", + "type": "string", + "const": "opener:default", + "markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`" + }, + { + "description": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.", + "type": "string", + "const": "opener:allow-default-urls", + "markdownDescription": "This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application." + }, + { + "description": "Enables the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-path", + "markdownDescription": "Enables the open_path command without any pre-configured scope." + }, + { + "description": "Enables the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-open-url", + "markdownDescription": "Enables the open_url command without any pre-configured scope." + }, + { + "description": "Enables the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:allow-reveal-item-in-dir", + "markdownDescription": "Enables the reveal_item_in_dir command without any pre-configured scope." + }, + { + "description": "Denies the open_path command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-path", + "markdownDescription": "Denies the open_path command without any pre-configured scope." + }, + { + "description": "Denies the open_url command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-open-url", + "markdownDescription": "Denies the open_url command without any pre-configured scope." + }, + { + "description": "Denies the reveal_item_in_dir command without any pre-configured scope.", + "type": "string", + "const": "opener:deny-reveal-item-in-dir", + "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "Application": { + "description": "Opener scope application.", + "anyOf": [ + { + "description": "Open in default application.", + "type": "null" + }, + { + "description": "If true, allow open with any application.", + "type": "boolean" + }, + { + "description": "Allow specific application to open with.", + "type": "string" + } + ] + } + } +} \ No newline at end of file diff --git a/packages/tauri-app/src-tauri/src/cert_manager.rs b/packages/tauri-app/src-tauri/src/cert_manager.rs new file mode 100644 index 00000000..aea7ec55 --- /dev/null +++ b/packages/tauri-app/src-tauri/src/cert_manager.rs @@ -0,0 +1,449 @@ +use base64::Engine; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json"; +const TLS_DIR_NAME: &str = "tls"; +const CA_CERT_FILE: &str = "ca-cert.pem"; +const SERVER_CERT_FILE: &str = "server-cert.pem"; +const SERVER_KEY_FILE: &str = "server-key.pem"; +const TRUSTED_MARKER: &str = "server-ca.trusted"; +#[cfg(windows)] +const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client"; + +/// Holds the PEM-encoded certificate/key pair used by the local HTTPS proxy, +/// plus the CA certificate DER used for trust-store installation. +pub struct LocalCert { + pub cert_pem: String, + pub key_pem: String, + pub ca_cert_der: Vec, +} + +struct TlsAssetPaths { + cert_path: PathBuf, + key_path: PathBuf, + trust_path: PathBuf, + append_ca_to_cert: bool, +} + +/// Loads the TLS assets already managed by `packages/server`. +pub fn ensure_local_cert() -> Result { + let assets = resolve_tls_asset_paths()?; + let mut cert_pem = read_pem_file(&assets.cert_path)?; + let key_pem = read_pem_file(&assets.key_path)?; + let trust_pem = read_pem_file(&assets.trust_path)?; + + if assets.append_ca_to_cert { + cert_pem = format!("{}\n{}\n", cert_pem.trim(), trust_pem.trim()); + } + + let ca_cert_der = pem_to_der(&trust_pem)?; + + Ok(LocalCert { + cert_pem, + key_pem, + ca_cert_der, + }) +} + +fn read_pem_file(path: &Path) -> Result { + fs::read_to_string(path).map_err(|e| format!("Failed to read {}: {e}", path.display())) +} + +fn server_tls_dir() -> Result { + Ok(resolve_server_config_base_dir()?.join(TLS_DIR_NAME)) +} + +fn resolve_tls_asset_paths() -> Result { + let tls_key_path = env::var("CLI_TLS_KEY") + .ok() + .filter(|value| !value.trim().is_empty()) + .map(|value| resolve_path_like_server(&value)) + .transpose()?; + let tls_cert_path = env::var("CLI_TLS_CERT") + .ok() + .filter(|value| !value.trim().is_empty()) + .map(|value| resolve_path_like_server(&value)) + .transpose()?; + let tls_ca_path = env::var("CLI_TLS_CA") + .ok() + .filter(|value| !value.trim().is_empty()) + .map(|value| resolve_path_like_server(&value)) + .transpose()?; + + match (tls_key_path, tls_cert_path) { + (Some(key_path), Some(cert_path)) => { + let append_ca_to_cert = tls_ca_path.is_some(); + let trust_path = tls_ca_path.unwrap_or_else(|| cert_path.clone()); + Ok(TlsAssetPaths { + cert_path, + key_path, + trust_path, + append_ca_to_cert, + }) + } + (Some(_), None) | (None, Some(_)) => Err( + "CLI_TLS_KEY and CLI_TLS_CERT must both be set when using custom TLS files" + .to_string(), + ), + (None, None) => { + let tls_dir = server_tls_dir()?; + Ok(TlsAssetPaths { + cert_path: tls_dir.join(SERVER_CERT_FILE), + key_path: tls_dir.join(SERVER_KEY_FILE), + trust_path: tls_dir.join(CA_CERT_FILE), + append_ca_to_cert: true, + }) + } + } +} + +fn resolve_server_config_base_dir() -> Result { + let raw = env::var("CLI_CONFIG") + .ok() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string()); + let expanded = resolve_path_like_server(&raw)?; + let lower = raw.trim().to_lowercase(); + + if lower.ends_with(".yaml") || lower.ends_with(".yml") || lower.ends_with(".json") { + return expanded + .parent() + .map(Path::to_path_buf) + .ok_or_else(|| format!("Failed to determine config base dir from {}", expanded.display())); + } + + Ok(expanded) +} + +fn resolve_path_like_server(path: &str) -> Result { + if path.starts_with("~/") { + let home = dirs::home_dir().or_else(|| env::var("HOME").ok().map(PathBuf::from)); + let home = home.ok_or_else(|| "Cannot determine home directory".to_string())?; + return Ok(home.join(path.trim_start_matches("~/"))); + } + + let path = PathBuf::from(path); + if path.is_absolute() { + return Ok(path); + } + + let cwd = env::current_dir().map_err(|e| format!("Failed to read current dir: {e}"))?; + Ok(cwd.join(path)) +} + +fn trusted_marker_path() -> Result { + let base = dirs::data_local_dir() + .ok_or_else(|| "Cannot determine local app data directory".to_string())?; + + #[cfg(windows)] + { + return Ok(base.join(WINDOWS_APP_USER_MODEL_ID).join(TRUSTED_MARKER)); + } + + #[cfg(not(windows))] + { + Ok(base.join("codenomad").join(TRUSTED_MARKER)) + } +} + +fn trusted_marker_value(cert_der: &[u8]) -> String { + cert_der.iter().map(|byte| format!("{byte:02x}")).collect() +} + +fn trusted_marker_file_suffix(cert_der: &[u8]) -> String { + trusted_marker_value(cert_der).chars().take(16).collect() +} + +fn has_matching_trusted_marker(cert_der: &[u8]) -> bool { + trusted_marker_path() + .ok() + .and_then(|path| fs::read_to_string(path).ok()) + .map(|value| value.trim() == trusted_marker_value(cert_der)) + .unwrap_or(false) +} + +fn write_trusted_marker(cert_der: &[u8]) -> Result<(), String> { + let path = trusted_marker_path()?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create trust state dir {}: {e}", parent.display()))?; + } + fs::write(path, trusted_marker_value(cert_der)) + .map_err(|e| format!("Failed to write trust marker: {e}")) +} + +#[cfg(windows)] +pub fn needs_trust_in_store(cert_der: &[u8]) -> Result { + Ok(!windows_cert_is_trusted(cert_der)?) +} + +#[cfg(windows)] +pub fn trust_cert_in_store(cert_der: &[u8]) -> Result<(), String> { + use windows_sys::Win32::Security::Cryptography::{ + CertAddEncodedCertificateToStore, CertCloseStore, CertOpenSystemStoreW, + CERT_STORE_ADD_REPLACE_EXISTING, PKCS_7_ASN_ENCODING, X509_ASN_ENCODING, + }; + + if !needs_trust_in_store(cert_der)? { + return Ok(()); + } + + let store_name: Vec = "Root\0".encode_utf16().collect(); + + unsafe { + let store = CertOpenSystemStoreW(0, store_name.as_ptr()); + if store.is_null() { + return Err("Failed to open CurrentUser\\Root certificate store".into()); + } + + let encoding = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; + let result = CertAddEncodedCertificateToStore( + store, + encoding, + cert_der.as_ptr(), + cert_der.len() as u32, + CERT_STORE_ADD_REPLACE_EXISTING, + std::ptr::null_mut(), + ); + + CertCloseStore(store, 0); + + if result == 0 { + return Err( + "Failed to add certificate to trust store. The user may have declined the security dialog." + .into(), + ); + } + } + + write_trusted_marker(cert_der)?; + Ok(()) +} + +#[cfg(target_os = "macos")] +pub fn needs_trust_in_store(cert_der: &[u8]) -> Result { + Ok(!(has_matching_trusted_marker(cert_der) && macos_cert_is_trusted(cert_der)?)) +} + +#[cfg(target_os = "macos")] +pub fn trust_cert_in_store(cert_der: &[u8]) -> Result<(), String> { + use std::process::Command; + + if !needs_trust_in_store(cert_der)? { + return Ok(()); + } + + let temp_path = env::temp_dir().join(format!( + "codenomad-server-ca-{}.cer", + trusted_marker_file_suffix(cert_der) + )); + fs::write(&temp_path, cert_der) + .map_err(|e| format!("Failed to write temporary certificate {}: {e}", temp_path.display()))?; + + let keychain_path = resolve_macos_user_keychain()?; + + let mut command = Command::new("/usr/bin/security"); + command.args(["add-trusted-cert", "-r", "trustRoot", "-k"]); + command.arg(&keychain_path); + + let output = command.arg(&temp_path).output().map_err(|e| { + format!( + "Failed to launch macOS security tool to trust the local CA certificate: {e}" + ) + })?; + + let _ = fs::remove_file(&temp_path); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let detail = if stderr.is_empty() { + format!("security exited with status {}", output.status) + } else { + stderr + }; + return Err(format!( + "Failed to add the local CodeNomad CA certificate to the macOS trust settings: {detail}" + )); + } + + if !macos_cert_is_trusted(cert_der)? { + return Err(format!( + "Added the local CodeNomad CA certificate to {} but could not verify that macOS trusts it", + keychain_path.display() + )); + } + + write_trusted_marker(cert_der)?; + Ok(()) +} + +#[cfg(windows)] +fn windows_cert_is_trusted(cert_der: &[u8]) -> Result { + use windows_sys::Win32::Security::Cryptography::{ + CertCloseStore, CertEnumCertificatesInStore, CertOpenSystemStoreW, + }; + + let store_name: Vec = "Root\0".encode_utf16().collect(); + + unsafe { + let store = CertOpenSystemStoreW(0, store_name.as_ptr()); + if store.is_null() { + return Err("Failed to open CurrentUser\\Root certificate store".into()); + } + + let mut context = CertEnumCertificatesInStore(store, std::ptr::null()); + while !context.is_null() { + let encoded = std::slice::from_raw_parts( + (*context).pbCertEncoded, + (*context).cbCertEncoded as usize, + ); + if encoded == cert_der { + CertCloseStore(store, 0); + return Ok(true); + } + + context = CertEnumCertificatesInStore(store, context); + } + + CertCloseStore(store, 0); + Ok(false) + } +} + +#[cfg(target_os = "macos")] +fn resolve_macos_user_keychain() -> Result { + let output = std::process::Command::new("/usr/bin/security") + .args(["default-keychain", "-d", "user"]) + .output() + .map_err(|e| format!("Failed to resolve macOS default user keychain: {e}"))?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let trimmed = stdout.trim().trim_matches('"'); + if !trimmed.is_empty() { + return Ok(PathBuf::from(trimmed)); + } + } + + let home = dirs::home_dir().or_else(|| env::var("HOME").ok().map(PathBuf::from)); + let home = home.ok_or_else(|| "Cannot determine home directory for macOS keychain lookup".to_string())?; + Ok(home.join("Library/Keychains/login.keychain-db")) +} + +#[cfg(target_os = "macos")] +fn macos_cert_is_trusted(cert_der: &[u8]) -> Result { + use std::process::Command; + + let temp_path = env::temp_dir().join(format!( + "codenomad-server-ca-verify-{}.cer", + trusted_marker_file_suffix(cert_der) + )); + fs::write(&temp_path, cert_der) + .map_err(|e| format!("Failed to write temporary certificate {}: {e}", temp_path.display()))?; + + let keychain_path = resolve_macos_user_keychain()?; + let fingerprint = macos_cert_sha256(&temp_path)?; + let find_output = Command::new("/usr/bin/security") + .args(["find-certificate", "-a", "-Z", "-c", "CodeNomad Local CA"]) + .arg(&keychain_path) + .output() + .map_err(|e| format!("Failed to query macOS keychain certificates: {e}"))?; + + if !find_output.status.success() { + let _ = fs::remove_file(&temp_path); + let stderr = String::from_utf8_lossy(&find_output.stderr).trim().to_string(); + let detail = if stderr.is_empty() { + format!("security exited with status {}", find_output.status) + } else { + stderr + }; + return Err(format!( + "Failed to inspect the macOS keychain for the local CodeNomad CA certificate: {detail}" + )); + } + + let stdout = String::from_utf8_lossy(&find_output.stdout); + if !stdout.to_ascii_uppercase().contains(&fingerprint) { + let _ = fs::remove_file(&temp_path); + return Ok(false); + } + + let verify_output = Command::new("/usr/bin/security") + .args(["verify-cert", "-q", "-L", "-l", "-p", "basic", "-c"]) + .arg(&temp_path) + .args(["-k"]) + .arg(&keychain_path) + .output() + .map_err(|e| format!("Failed to verify macOS trust for the local CodeNomad CA certificate: {e}"))?; + + let _ = fs::remove_file(&temp_path); + Ok(verify_output.status.success()) +} + +#[cfg(target_os = "macos")] +fn macos_cert_sha256(cert_path: &Path) -> Result { + let output = std::process::Command::new("/usr/bin/shasum") + .args(["-a", "256"]) + .arg(cert_path) + .output() + .map_err(|e| format!("Failed to compute SHA-256 for {}: {e}", cert_path.display()))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let detail = if stderr.is_empty() { + format!("shasum exited with status {}", output.status) + } else { + stderr + }; + return Err(format!( + "Failed to compute SHA-256 for {}: {detail}", + cert_path.display() + )); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let hash = stdout + .split_whitespace() + .next() + .ok_or_else(|| format!("Failed to parse SHA-256 output for {}", cert_path.display()))?; + Ok(hash.to_ascii_uppercase()) +} + +#[cfg(all(not(windows), not(target_os = "macos")))] +pub fn needs_trust_in_store(_cert_der: &[u8]) -> Result { + Ok(false) +} + +#[cfg(all(not(windows), not(target_os = "macos")))] +pub fn trust_cert_in_store(_cert_der: &[u8]) -> Result<(), String> { + // Non-Windows platforms use native webview-specific handling instead of OS trust-store writes. + Ok(()) +} + +fn pem_to_der(pem: &str) -> Result, String> { + let mut body = String::new(); + let mut in_block = false; + + for line in pem.lines() { + if line.starts_with("-----BEGIN CERTIFICATE-----") { + in_block = true; + continue; + } + if line.starts_with("-----END CERTIFICATE-----") { + break; + } + if in_block { + body.push_str(line.trim()); + } + } + + if body.is_empty() { + return Err("No certificate found in PEM file".to_string()); + } + + base64::engine::general_purpose::STANDARD + .decode(body) + .map_err(|e| format!("Failed to decode certificate PEM: {e}")) +} diff --git a/packages/tauri-app/src-tauri/src/cli_manager.rs b/packages/tauri-app/src-tauri/src/cli_manager.rs index d89247af..a0efd38a 100644 --- a/packages/tauri-app/src-tauri/src/cli_manager.rs +++ b/packages/tauri-app/src-tauri/src/cli_manager.rs @@ -1094,7 +1094,8 @@ impl CliEntry { ]; if dev { - // Dev: plain HTTP + Vite dev server proxy. + // Dev: keep loopback HTTP for the Vite proxy, but also enable HTTPS so + // remote proxy sessions can still spin up secure local windows. let ui_dev_server = std::env::var("VITE_DEV_SERVER_URL") .ok() .filter(|value| !value.trim().is_empty()) @@ -1111,7 +1112,7 @@ impl CliEntry { .unwrap_or_else(|| "info".to_string()); args.push("--https".to_string()); - args.push("false".to_string()); + args.push("true".to_string()); args.push("--http".to_string()); args.push("true".to_string()); args.push("--http-port".to_string()); diff --git a/packages/tauri-app/src-tauri/src/linux_tls.rs b/packages/tauri-app/src-tauri/src/linux_tls.rs new file mode 100644 index 00000000..1a4865d2 --- /dev/null +++ b/packages/tauri-app/src-tauri/src/linux_tls.rs @@ -0,0 +1,88 @@ +use crate::AppState; +use tauri::{AppHandle, Manager, WebviewWindow}; +use url::Url; +use webkit2gtk::{WebContextExt, WebView, WebViewExt}; + +pub fn should_bootstrap_tls_navigation(target_url: &Url, allow_tls_certificate: bool) -> bool { + allow_tls_certificate && target_url.scheme() == "https" +} + +pub fn ensure_remote_window_tls_handler( + window: &WebviewWindow, + app_handle: &AppHandle, + window_label: &str, +) -> Result<(), String> { + { + let state = app_handle.state::(); + let mut handlers = state + .remote_tls_handlers + .lock() + .map_err(|err| err.to_string())?; + if !handlers.insert(window_label.to_string()) { + return Ok(()); + } + } + + let app_handle = app_handle.clone(); + let window_label = window_label.to_string(); + window + .with_webview(move |platform_webview| { + let webview = platform_webview.inner(); + let app_handle = app_handle.clone(); + let window_label = window_label.clone(); + webview.connect_load_failed_with_tls_errors(move |view, failing_uri, certificate, _| { + allow_remote_tls_certificate( + &app_handle, + &window_label, + view, + failing_uri, + certificate, + ) + }); + }) + .map_err(|err| err.to_string()) +} + +fn allow_remote_tls_certificate( + app_handle: &AppHandle, + window_label: &str, + view: &WebView, + failing_uri: &str, + certificate: &webkit2gtk::gio::TlsCertificate, +) -> bool { + let Ok(parsed_uri) = Url::parse(failing_uri) else { + return false; + }; + let Some(host) = parsed_uri.host_str() else { + return false; + }; + + let state = app_handle.state::(); + let skip_tls_verify = state + .remote_skip_tls_verify + .lock() + .ok() + .and_then(|values| values.get(window_label).copied()) + .unwrap_or(false); + if !skip_tls_verify { + return false; + } + + let expected_origin = state + .remote_origins + .lock() + .ok() + .and_then(|origins| origins.get(window_label).cloned()); + let parsed_origin = parsed_uri.origin().ascii_serialization(); + if expected_origin.as_deref() != Some(parsed_origin.as_str()) { + return false; + } + + let Some(context) = view.context() else { + return false; + }; + + context.allow_tls_certificate_for_host(certificate, host); + view.load_uri(failing_uri); + true +} diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index 47a91b17..d8204059 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -1,12 +1,16 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#[allow(dead_code)] +mod cert_manager; mod cli_manager; +#[cfg(target_os = "linux")] +mod linux_tls; use cli_manager::{CliProcessManager, CliStatus}; use keepawake::KeepAwake; use serde::Deserialize; use serde_json::json; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; @@ -16,6 +20,7 @@ use tauri::webview::Webview; use tauri::{ AppHandle, Emitter, Manager, Runtime, WebviewUrl, WebviewWindowBuilder, WindowEvent, Wry, }; +use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; use tauri_plugin_global_shortcut::{ Code as ShortcutCode, GlobalShortcutExt, Shortcut, ShortcutState, }; @@ -45,6 +50,9 @@ pub struct AppState { pub wake_lock: Mutex>, pub zoom_level: Mutex, pub remote_origins: Mutex>, + pub remote_proxy_sessions: Mutex>, + pub remote_skip_tls_verify: Mutex>, + pub remote_tls_handlers: Mutex>, } #[derive(Debug, Deserialize)] @@ -53,9 +61,87 @@ struct RemoteWindowPayload { id: String, name: String, base_url: String, + entry_url: Option, + proxy_session_id: Option, + #[allow(dead_code)] skip_tls_verify: bool, } +fn schedule_remote_proxy_session_cleanup(app: AppHandle, session_id: String) { + tauri::async_runtime::spawn(async move { + if let Err(err) = cleanup_remote_proxy_session(&app, &session_id).await { + eprintln!( + "[tauri] failed to clean up remote proxy session {}: {}", + session_id, err + ); + } + }); +} + +async fn confirm_local_certificate_install(app: &AppHandle) -> Result { + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + + let mut dialog = app + .dialog() + .message( + "CodeNomad needs to install a local certificate to open self-signed HTTPS remote windows. This certificate is only used for local desktop proxy traffic on your machine. Your operating system may show a second certificate prompt after this.", + ) + .title("Install Local Certificate") + .kind(MessageDialogKind::Warning) + .buttons(MessageDialogButtons::OkCancelCustom( + "Continue".into(), + "Cancel".into(), + )); + + if let Some(window) = app.get_webview_window("main") { + dialog = dialog.parent(&window); + } + + dialog.show(move |accepted| { + let _ = sender.send(accepted); + }); + + tauri::async_runtime::spawn_blocking(move || receiver.recv().unwrap_or(false)) + .await + .map_err(|err| err.to_string()) +} + +async fn cleanup_remote_proxy_session(app: &AppHandle, session_id: &str) -> Result<(), String> { + let status = app.state::().manager.status(); + let Some(base_url) = status.url else { + return Ok(()); + }; + + let mut cleanup_url = Url::parse(&base_url).map_err(|err| err.to_string())?; + cleanup_url.set_path(&format!("/api/remote-proxy/sessions/{session_id}")); + cleanup_url.set_query(None); + cleanup_url.set_fragment(None); + + let client = if cleanup_url.scheme() == "https" { + let local_cert = cert_manager::ensure_local_cert()?; + let ca_cert = reqwest::Certificate::from_der(&local_cert.ca_cert_der) + .map_err(|err| err.to_string())?; + reqwest::Client::builder() + .add_root_certificate(ca_cert) + .build() + .map_err(|err| err.to_string())? + } else { + reqwest::Client::new() + }; + + let response = client + .delete(cleanup_url.as_str()) + .send() + .await + .map_err(|err| err.to_string())?; + + if response.status().is_success() || response.status() == reqwest::StatusCode::NOT_FOUND { + return Ok(()); + } + + Err(format!("unexpected status {}", response.status())) +} + #[derive(Debug, Default, Deserialize)] #[serde(default, rename_all = "camelCase")] struct WakeLockConfig { @@ -119,7 +205,7 @@ fn is_dev_mode() -> bool { fn should_allow_internal(url: &Url) -> bool { match url.scheme() { - "tauri" | "asset" | "file" => true, + "tauri" | "asset" | "file" | "about" => true, // On Windows/WebView2, Tauri serves the app assets from `tauri.localhost`. // This must be treated as an internal origin or the navigation guard will // redirect it to the system browser and the app will appear blank. @@ -167,25 +253,61 @@ fn intercept_navigation(webview: &Webview, url: &Url) -> bool { false } -#[tauri::command] -fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<(), String> { - if payload.skip_tls_verify && payload.base_url.starts_with("https://") { - return Err( - "Tauri cannot bypass self-signed HTTPS certificates automatically yet. Trust the certificate in your OS first, then reconnect, or use the CodeNomad Electron app." - .to_string(), - ); - } - - let parsed = Url::parse(&payload.base_url).map_err(|err| err.to_string())?; +async fn open_remote_window_impl( + app: AppHandle, + payload: RemoteWindowPayload, +) -> Result<(), String> { + let entry_url = payload.entry_url.as_deref().unwrap_or(payload.base_url.as_str()); + let parsed = Url::parse(entry_url).map_err(|err| err.to_string())?; let label = format!("remote-{}", payload.id); let title = format!( "{} - {}", payload.name, - parsed.host_str().unwrap_or(payload.base_url.as_str()) + Url::parse(&payload.base_url) + .ok() + .and_then(|url| url.host_str().map(str::to_string)) + .unwrap_or_else(|| payload.base_url.clone()) ); + let window_url = parsed.clone(); + + let allow_linux_tls_certificate = + parsed.scheme() == "https" && (payload.proxy_session_id.is_some() || payload.skip_tls_verify); + + app.state::() + .remote_origins + .lock() + .map_err(|err| err.to_string())? + .insert(label.clone(), window_url.origin().ascii_serialization()); + app.state::() + .remote_skip_tls_verify + .lock() + .map_err(|err| err.to_string())? + .insert(label.clone(), allow_linux_tls_certificate); + + let replaced_session = { + let state = app.state::(); + let mut sessions = state + .remote_proxy_sessions + .lock() + .map_err(|err| err.to_string())?; + match payload.proxy_session_id.clone() { + Some(session_id) => sessions.insert(label.clone(), session_id), + None => sessions.remove(&label), + } + }; + + if let Some(previous) = replaced_session { + if payload.proxy_session_id.as_deref() != Some(previous.as_str()) { + schedule_remote_proxy_session_cleanup(app.clone(), previous); + } + } + if let Some(existing) = app.get_webview_window(&label) { - let _ = existing.navigate(parsed.clone()); + #[cfg(target_os = "linux")] + linux_tls::ensure_remote_window_tls_handler(&existing, &app, &label)?; + + let _ = existing.navigate(window_url.clone()); let _ = existing.set_title(&title); let _ = existing.show(); let _ = existing.unminimize(); @@ -193,25 +315,51 @@ fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<() return Ok(()); } - app.state::() - .remote_origins - .lock() - .map_err(|err| err.to_string())? - .insert(label.clone(), parsed.origin().ascii_serialization()); + #[cfg(target_os = "linux")] + let initial_url = if linux_tls::should_bootstrap_tls_navigation( + &window_url, + allow_linux_tls_certificate, + ) { + Url::parse("about:blank").map_err(|err| err.to_string())? + } else { + window_url.clone() + }; - let window = - WebviewWindowBuilder::new(&app, label.clone(), WebviewUrl::External(parsed.clone())) - .title(title) - .inner_size(1400.0, 900.0) - .min_inner_size(800.0, 600.0) - .build() - .map_err(|err| err.to_string())?; + #[cfg(not(target_os = "linux"))] + let initial_url = window_url.clone(); + + let window = WebviewWindowBuilder::new(&app, label.clone(), WebviewUrl::External(initial_url.clone())) + .title(title) + .inner_size(1400.0, 900.0) + .min_inner_size(800.0, 600.0) + .build() + .map_err(|err| err.to_string())?; + + #[cfg(target_os = "linux")] + { + linux_tls::ensure_remote_window_tls_handler(&window, &app, &label)?; + if initial_url != window_url { + let _ = window.navigate(window_url.clone()); + } + } let app_handle = app.clone(); + let label_for_cleanup = label.clone(); window.on_window_event(move |event| { if let WindowEvent::Destroyed = event { if let Ok(mut origins) = app_handle.state::().remote_origins.lock() { - origins.remove(&label); + origins.remove(&label_for_cleanup); + } + if let Ok(mut sessions) = app_handle.state::().remote_proxy_sessions.lock() { + if let Some(session_id) = sessions.remove(&label_for_cleanup) { + schedule_remote_proxy_session_cleanup(app_handle.clone(), session_id); + } + } + if let Ok(mut values) = app_handle.state::().remote_skip_tls_verify.lock() { + values.remove(&label_for_cleanup); + } + if let Ok(mut handlers) = app_handle.state::().remote_tls_handlers.lock() { + handlers.remove(&label_for_cleanup); } } }); @@ -219,6 +367,40 @@ fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<() Ok(()) } +#[tauri::command] +async fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<(), String> { + #[cfg(not(target_os = "linux"))] + { + let entry_url = payload.entry_url.as_deref().unwrap_or(payload.base_url.as_str()); + let parsed = Url::parse(entry_url).map_err(|err| err.to_string())?; + if payload.proxy_session_id.is_some() && parsed.scheme() == "https" { + let local_cert = cert_manager::ensure_local_cert().map_err(|err| { + format!( + "Failed to load the local HTTPS certificate for the remote proxy window: {err}" + ) + })?; + if cert_manager::needs_trust_in_store(&local_cert.ca_cert_der).map_err(|err| { + format!("Failed to inspect the local CodeNomad certificate trust state: {err}") + })? { + let accepted = confirm_local_certificate_install(&app).await?; + if !accepted { + return Err( + "CodeNomad needs the local certificate to be trusted before it can open self-signed HTTPS remote windows." + .to_string(), + ); + } + } + if let Err(err) = cert_manager::trust_cert_in_store(&local_cert.ca_cert_der) { + return Err(format!( + "Failed to trust the local CodeNomad CA certificate. Accept the certificate installation prompt and try again: {err}" + )); + } + } + } + + open_remote_window_impl(app, payload).await +} + fn collect_directory_paths(paths: &[std::path::PathBuf]) -> Vec { paths .iter() @@ -346,6 +528,8 @@ fn set_windows_app_user_model_id() { fn set_windows_app_user_model_id() {} fn main() { + let _ = rustls::crypto::ring::default_provider().install_default(); + let navigation_guard: TauriPlugin = PluginBuilder::new("external-link-guard") .on_navigation(|webview, url| intercept_navigation(webview, url)) .build(); @@ -373,6 +557,9 @@ fn main() { wake_lock: Mutex::new(None), zoom_level: Mutex::new(DEFAULT_ZOOM_LEVEL), remote_origins: Mutex::new(HashMap::new()), + remote_proxy_sessions: Mutex::new(HashMap::new()), + remote_skip_tls_verify: Mutex::new(HashMap::new()), + remote_tls_handlers: Mutex::new(HashSet::new()), }) .setup(|app| { set_windows_app_user_model_id(); diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx index 90459ada..c9a62ddc 100644 --- a/packages/ui/src/components/folder-selection-view.tsx +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -16,6 +16,7 @@ import { showAlertDialog } from "../stores/alerts" import { openSettings, settingsOpen } from "../stores/settings-screen" import { openExternalUrl } from "../lib/external-url" import { serverApi } from "../lib/api-client" +import { runtimeEnv } from "../lib/runtime-env" import { openRemoteServerWindow } from "../lib/native/remote-window" const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href @@ -332,7 +333,23 @@ const FolderSelectionView: Component = (props) => { }) if (openWindow) { - await openRemoteServerWindow(profile) + const remoteProxySession = + runtimeEnv.host === "tauri" && profile.skipTlsVerify && profile.baseUrl.startsWith("https://") + ? await serverApi.createRemoteProxySession({ + baseUrl: profile.baseUrl, + skipTlsVerify: profile.skipTlsVerify, + }) + : undefined + + try { + await openRemoteServerWindow(profile, remoteProxySession?.windowUrl, remoteProxySession?.sessionId) + } catch (error) { + if (remoteProxySession) { + void serverApi.deleteRemoteProxySession(remoteProxySession.sessionId).catch(() => {}) + } + throw error + } + await markRemoteServerConnected(profile.id) } diff --git a/packages/ui/src/lib/api-client.ts b/packages/ui/src/lib/api-client.ts index 96dad79e..9719375a 100644 --- a/packages/ui/src/lib/api-client.ts +++ b/packages/ui/src/lib/api-client.ts @@ -12,6 +12,8 @@ import type { SpeechTranscriptionResponse, SideCar, ServerMeta, + RemoteProxySessionCreateRequest, + RemoteProxySessionCreateResponse, RemoteServerProbeRequest, RemoteServerProbeResponse, VoiceModeStateResponse, @@ -256,6 +258,15 @@ export const serverApi = { body: JSON.stringify(payload), }) }, + createRemoteProxySession(payload: RemoteProxySessionCreateRequest): Promise { + return request("/api/remote-proxy/sessions", { + method: "POST", + body: JSON.stringify(payload), + }) + }, + deleteRemoteProxySession(id: string): Promise { + return request(`/api/remote-proxy/sessions/${encodeURIComponent(id)}`, { method: "DELETE" }) + }, fetchAuthStatus(): Promise<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean }> { return request<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean }>("/api/auth/status") }, diff --git a/packages/ui/src/lib/native/remote-window.ts b/packages/ui/src/lib/native/remote-window.ts index 96b36cd5..8412c6b3 100644 --- a/packages/ui/src/lib/native/remote-window.ts +++ b/packages/ui/src/lib/native/remote-window.ts @@ -6,14 +6,22 @@ export interface RemoteWindowOpenPayload { id: string name: string baseUrl: string + entryUrl?: string + proxySessionId?: string skipTlsVerify: boolean } -export async function openRemoteServerWindow(profile: Pick): Promise { +export async function openRemoteServerWindow( + profile: Pick, + entryUrl?: string, + proxySessionId?: string, +): Promise { const payload: RemoteWindowOpenPayload = { id: profile.id, name: profile.name, baseUrl: profile.baseUrl, + entryUrl, + proxySessionId, skipTlsVerify: profile.skipTlsVerify, } diff --git a/packages/ui/src/types/global.d.ts b/packages/ui/src/types/global.d.ts index 8e6a475e..0d10cfa3 100644 --- a/packages/ui/src/types/global.d.ts +++ b/packages/ui/src/types/global.d.ts @@ -37,6 +37,8 @@ declare global { id: string name: string baseUrl: string + entryUrl?: string + proxySessionId?: string skipTlsVerify: boolean }) => Promise<{ ok: boolean }> }