Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/desktop-app/shared/app-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export {
getAppUrl,
getTemplateGatewayAppUrl,
getTemplateGatewayUrl,
normalizeProtectedPreviewOrigin,
authorizeProtectedPreviewLaunch,
resolveFrameOAuthCallbackTarget,
resolveProtectedPreviewOAuthRelay,
getAppById,
toAppDefinition,
generateAppId,
Expand Down
14 changes: 14 additions & 0 deletions packages/desktop-app/shared/ipc-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const IPC = {
APPS_UPDATE_CREATION_SETTINGS: "apps:update-creation-settings",
APPS_CREATE_FROM_PROMPT: "apps:create-from-prompt",
APPS_SHOW_CONTEXT_MENU: "apps:show-context-menu",
PROTECTED_PREVIEW_GET: "protected-preview:get",
PROTECTED_PREVIEW_SAVE: "protected-preview:save",
PROTECTED_PREVIEW_CLEAR: "protected-preview:clear",

/** Hosted Plan app local-file sync (Plan webview ↔ main) */
PLAN_FILES_GET_FOLDER: "plan-files:get-folder",
Expand Down Expand Up @@ -109,6 +112,7 @@ export const IPC = {

/** Deep links (main → renderer) */
DEEP_LINK_OPEN: "deep-link:open",
DEEP_LINK_READY: "deep-link:ready",

/** Local desktop app-launch shortcuts (renderer ↔ main) */
SHORTCUTS_ACTIVATE: "shortcuts:activate",
Expand Down Expand Up @@ -171,6 +175,15 @@ export interface DesktopAppCreationSettings {
appsRoot: string;
}

export interface ProtectedPreviewAccessStatus {
available: boolean;
configured: boolean;
origin?: string;
kind?: "shareable-link";
restoreApp?: import("@agent-native/shared-app-config").AppConfig;
error?: string;
}

export interface DesktopCreateAppRequest {
prompt: string;
appsRoot?: string;
Expand Down Expand Up @@ -811,6 +824,7 @@ export interface DesktopOpenRequest {
app?: string;
goalId?: string;
path?: string;
previewUrl?: string;
softOpen?: boolean;
runId?: string;
}
Expand Down
84 changes: 82 additions & 2 deletions packages/desktop-app/src/main/app-store.privacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const electronState = vi.hoisted(() => ({
userData: "",
decryptString: vi.fn(() => "sk-test-example"),
encryptionAvailable: true,
decryptString: vi.fn((value: Buffer) => value.toString()),
}));

vi.mock("electron", () => ({
Expand All @@ -15,16 +16,20 @@ vi.mock("electron", () => ({
getPath: () => electronState.userData,
},
safeStorage: {
isEncryptionAvailable: vi.fn(() => true),
isEncryptionAvailable: vi.fn(() => electronState.encryptionAvailable),
decryptString: electronState.decryptString,
encryptString: vi.fn((value: string) => Buffer.from(value)),
},
}));

import {
clearProtectedPreviewAccess,
getCodeAgentProviderSettingsStatus,
getProtectedPreviewAccessStatus,
loadCodeAgentProviderCredentials,
loadProtectedPreviewAccess,
loadRemoteConnectorSettings,
saveProtectedPreviewAccess,
} from "./app-store";

describe("desktop privacy-safe status reads", () => {
Expand All @@ -33,6 +38,7 @@ describe("desktop privacy-safe status reads", () => {
path.join(os.tmpdir(), "agent-native-privacy-"),
);
electronState.decryptString.mockClear();
electronState.encryptionAvailable = true;
fs.writeFileSync(
path.join(electronState.userData, "code-agent-providers.json"),
JSON.stringify({
Expand Down Expand Up @@ -70,4 +76,78 @@ describe("desktop privacy-safe status reads", () => {
it("defaults the background connector to disabled", () => {
expect(loadRemoteConnectorSettings()).toEqual({ enabled: false });
});

it("rejects project-wide automation bypass credentials", () => {
const origin = "https://candidate.example.test";

expect(
saveProtectedPreviewAccess(
"content",
`${origin}/review`,
"example-project-wide-secret",
),
).toMatchObject({ configured: false });
});

it("extracts a deployment share secret without storing the bearer URL", () => {
const origin = "https://candidate.example.test";
const secret = "example-share-secret";
const shareUrl = `${origin}/?_vercel_share=${secret}`;

expect(saveProtectedPreviewAccess("content", origin, shareUrl)).toEqual({
available: true,
configured: true,
origin,
kind: "shareable-link",
});

const stored = fs.readFileSync(
path.join(electronState.userData, "protected-preview-access.json"),
"utf-8",
);
expect(stored).toContain('"kind": "shareable-link"');
expect(stored).not.toContain("_vercel_share");
expect(stored).not.toContain(secret);
expect(loadProtectedPreviewAccess("content")).toEqual({
origin,
kind: "shareable-link",
secret,
});
expect(getProtectedPreviewAccessStatus("content")).toMatchObject({
available: true,
configured: true,
origin,
kind: "shareable-link",
});

expect(clearProtectedPreviewAccess("content")).toEqual({
available: true,
configured: false,
});
expect(loadProtectedPreviewAccess("content")).toBeNull();
});

it("rejects a Shareable Link for a different deployment origin", () => {
expect(
saveProtectedPreviewAccess(
"content",
"https://candidate.example.test",
"https://other-candidate.example.test/?_vercel_share=secret",
),
).toMatchObject({ configured: false });
});

it("fails closed when operating-system encryption is unavailable", () => {
electronState.encryptionAvailable = false;
expect(
saveProtectedPreviewAccess(
"content",
"https://candidate.example.test",
"https://candidate.example.test/?_vercel_share=example-share-secret",
),
).toMatchObject({
available: false,
configured: false,
});
});
});
Loading
Loading