Skip to content
Open
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
22 changes: 21 additions & 1 deletion apps/desktop/src/app/DesktopClerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const makeDesktopClerkLayer = (isDevelopment = true) => {
isDevelopment,
} as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);

return DesktopClerk.layer.pipe(
return DesktopClerk.makeDesktopClerkLayer(true).pipe(
Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)),
);
};
Expand All @@ -53,6 +53,26 @@ describe("DesktopClerk", () => {
assert.equal(DesktopClerk.resolveDesktopClerkFrontendApiHostname("invalid"), undefined);
});

it.effect("skips acquiring the SDK bridge when Clerk is disabled", () => {
const environment = DesktopEnvironment.DesktopEnvironment.of({
stateDir: "/tmp/t3-state",
isDevelopment: true,
} as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);

return Effect.gen(function* () {
yield* Effect.scoped(
Layer.build(
DesktopClerk.makeDesktopClerkLayer(false).pipe(
Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)),
),
),
);

assert.deepEqual(storageMock.mock.calls, []);
assert.deepEqual(createClerkBridgeMock.mock.calls, []);
});
});

it.effect("acquires and releases the SDK bridge with the layer", () => {
const cleanup = vi.fn();
storageMock.mockReturnValue(storageAdapter);
Expand Down
104 changes: 56 additions & 48 deletions apps/desktop/src/app/DesktopClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export const desktopClerkFrontendApiHostname = resolveDesktopClerkFrontendApiHos
: __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__,
);

export const desktopClerkBridgeEnabled = Boolean(desktopClerkFrontendApiHostname);

export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolean) {
return createClerkBridge({
storage: storage({ path: stateDir }),
Expand All @@ -82,54 +84,60 @@ export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolea
});
}

export const make = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
yield* Effect.acquireRelease(
Effect.try({
try: () => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment),
catch: (cause) =>
new DesktopClerkBridgeInitializationError({
stateDir: environment.stateDir,
isDevelopment: environment.isDevelopment,
cause,
export function makeDesktopClerkLayer(enabled = desktopClerkBridgeEnabled) {
const make = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
if (enabled) {
yield* Effect.acquireRelease(
Effect.try({
try: () => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment),
catch: (cause) =>
new DesktopClerkBridgeInitializationError({
stateDir: environment.stateDir,
isDevelopment: environment.isDevelopment,
cause,
}),
}),
}),
(bridge) =>
Effect.try({
try: () => bridge.cleanup(),
catch: (cause) =>
new DesktopClerkBridgeCleanupError({
stateDir: environment.stateDir,
isDevelopment: environment.isDevelopment,
cause,
}),
}).pipe(Effect.orDie),
);

return DesktopClerk.of({
configure: Effect.gen(function* () {
const electronApp = yield* ElectronApp.ElectronApp;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const context = yield* Effect.context<ElectronWindow.ElectronWindow>();
const runPromise = Effect.runPromiseWith(context);

if (!(yield* electronApp.requestSingleInstanceLock)) {
yield* electronApp.quit;
return yield* Effect.interrupt;
}

yield* electronApp.on("second-instance", () => {
void runPromise(
Effect.gen(function* () {
const mainWindow = yield* electronWindow.currentMainOrFirst;
if (Option.isSome(mainWindow)) {
yield* electronWindow.reveal(mainWindow.value);
}
}),
);
});
}).pipe(Effect.withSpan("desktop.clerk.configure")),
(bridge) =>
Effect.try({
try: () => bridge.cleanup(),
catch: (cause) =>
new DesktopClerkBridgeCleanupError({
stateDir: environment.stateDir,
isDevelopment: environment.isDevelopment,
cause,
}),
}).pipe(Effect.orDie),
);
}

return DesktopClerk.of({
configure: Effect.gen(function* () {
const electronApp = yield* ElectronApp.ElectronApp;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const context = yield* Effect.context<ElectronWindow.ElectronWindow>();
const runPromise = Effect.runPromiseWith(context);

if (!(yield* electronApp.requestSingleInstanceLock)) {
yield* electronApp.quit;
return yield* Effect.interrupt;
}

yield* electronApp.on("second-instance", () => {
void runPromise(
Effect.gen(function* () {
const mainWindow = yield* electronWindow.currentMainOrFirst;
if (Option.isSome(mainWindow)) {
yield* electronWindow.reveal(mainWindow.value);
}
}),
);
});
}).pipe(Effect.withSpan("desktop.clerk.configure")),
});
});
});

export const layer = Layer.effect(DesktopClerk, make);
return Layer.effect(DesktopClerk, make);
}

export const layer = makeDesktopClerkLayer();
132 changes: 118 additions & 14 deletions apps/desktop/src/electron/ElectronProtocol.test.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,83 @@
import { assert, describe, it } from "@effect/vitest";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
import { beforeEach, vi } from "vite-plus/test";

const { handleMock, netFetchMock, unhandleMock } = vi.hoisted(() => ({
const { handleMock, registerSchemesAsPrivilegedMock, unhandleMock } = vi.hoisted(() => ({
handleMock: vi.fn(),
netFetchMock: vi.fn(),
registerSchemesAsPrivilegedMock: vi.fn(),
unhandleMock: vi.fn(),
}));

vi.mock("electron", () => ({
net: { fetch: netFetchMock },
protocol: { handle: handleMock, unhandle: unhandleMock },
protocol: {
handle: handleMock,
registerSchemesAsPrivileged: registerSchemesAsPrivilegedMock,
unhandle: unhandleMock,
},
}));

import * as ElectronProtocol from "./ElectronProtocol.ts";

function makeHttpClientLayer(
handler: (
request: HttpClientRequest.HttpClientRequest,
) => Effect.Effect<HttpClientResponse.HttpClientResponse>,
) {
return Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) => handler(request)),
);
}

const unexpectedHttpClientLayer = makeHttpClientLayer(() =>
Effect.die("unexpected Electron protocol proxy request"),
);

describe("ElectronProtocol", () => {
beforeEach(() => {
handleMock.mockReset();
netFetchMock.mockReset();
registerSchemesAsPrivilegedMock.mockReset();
unhandleMock.mockReset();
});

it("registers the desktop scheme as a secure standard scheme", () => {
ElectronProtocol.registerDesktopSchemePrivileges(true);

assert.deepEqual(registerSchemesAsPrivilegedMock.mock.calls, [
[
[
{
scheme: "t3code-dev",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
},
},
],
],
]);
});

it.effect("proxies the stable renderer origin to the current app server", () =>
Effect.gen(function* () {
let handler: ((request: Request) => Promise<Response>) | undefined;
const requestUrls: string[] = [];
handleMock.mockImplementation((_scheme, nextHandler) => {
handler = nextHandler;
});
netFetchMock.mockResolvedValue(new Response("ok"));
const httpClientLayer = makeHttpClientLayer((request) =>
Effect.gen(function* () {
const webRequest = yield* HttpClientRequest.toWeb(request).pipe(Effect.orDie);
requestUrls.push(webRequest.url);
return HttpClientResponse.fromWeb(request, new Response("ok"));
}),
);

yield* Effect.scoped(
Effect.gen(function* () {
Expand Down Expand Up @@ -63,23 +111,79 @@ describe("ElectronProtocol", () => {
"font-src 'self' t3code-dev: data:",
);
}),
);
).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(httpClientLayer))));

assert.deepEqual(
handleMock.mock.calls.map((call) => call[0]),
["t3code-dev"],
);
assert.equal(netFetchMock.mock.calls[0]?.[0], "http://127.0.0.1:3773/api/health?verbose=1");
assert.equal(requestUrls[0], "http://127.0.0.1:3773/api/health?verbose=1");
assert.deepEqual(unhandleMock.mock.calls, [["t3code-dev"]]);
}).pipe(Effect.provide(ElectronProtocol.layer)),
}),
);

it.effect("returns proxied response bodies without buffering them first", () =>
Effect.gen(function* () {
let handler: ((request: Request) => Promise<Response>) | undefined;
handleMock.mockImplementation((_scheme, nextHandler) => {
handler = nextHandler;
});
const chunk = new TextEncoder().encode("streamed");
const httpClientLayer = makeHttpClientLayer((request) =>
Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(chunk);
},
}),
),
),
),
);

const response = yield* Effect.scoped(
Effect.gen(function* () {
const protocol = yield* ElectronProtocol.ElectronProtocol;
yield* protocol.registerDesktopProtocol({
scheme: "t3code-dev",
targetOrigin: new URL("http://127.0.0.1:3773/"),
backendOrigin: new URL("http://127.0.0.1:3774/"),
clerkFrontendApiHostname: undefined,
});
assert.isDefined(handler);
return yield* Effect.raceFirst(
Effect.promise(() => handler!(new Request("t3code-dev://app/large-asset"))),
Effect.sleep("250 millis").pipe(
Effect.andThen(Effect.die(new Error("proxy response was buffered"))),
),
);
}),
).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(httpClientLayer))));

const reader = response.body?.getReader();
assert.isDefined(reader);
const firstChunk = yield* Effect.promise(() => reader!.read());
assert.deepEqual(firstChunk.value, chunk);
yield* Effect.promise(() => reader!.cancel());
}),
);

it.effect("rejects custom protocol requests for another host", () =>
Effect.gen(function* () {
let handler: ((request: Request) => Promise<Response>) | undefined;
const requests: HttpClientRequest.HttpClientRequest[] = [];
handleMock.mockImplementation((_scheme, nextHandler) => {
handler = nextHandler;
});
const httpClientLayer = makeHttpClientLayer((request) =>
Effect.sync(() => {
requests.push(request);
return HttpClientResponse.fromWeb(request, new Response("unexpected"));
}),
);

const response = yield* Effect.scoped(
Effect.gen(function* () {
Expand All @@ -92,11 +196,11 @@ describe("ElectronProtocol", () => {
});
return yield* Effect.promise(() => handler!(new Request("t3code://other/")));
}),
);
).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(httpClientLayer))));

assert.equal(response.status, 404);
assert.equal(netFetchMock.mock.calls.length, 0);
}).pipe(Effect.provide(ElectronProtocol.layer)),
assert.equal(requests.length, 0);
}),
);

it.effect("preserves protocol registration failures", () =>
Expand All @@ -120,7 +224,7 @@ describe("ElectronProtocol", () => {
assert.equal(error.scheme, "t3code-dev");
assert.strictEqual(error.cause, cause);
assert.equal(error.message, 'Failed to register Electron protocol scheme "t3code-dev".');
}).pipe(Effect.provide(ElectronProtocol.layer)),
}).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(unexpectedHttpClientLayer)))),
);

it.effect("preserves protocol unregistration failures", () =>
Expand Down Expand Up @@ -150,7 +254,7 @@ describe("ElectronProtocol", () => {
assert.strictEqual(error.cause, cause);
assert.equal(error.message, 'Failed to unregister Electron protocol scheme "t3code".');
}
}).pipe(Effect.provide(ElectronProtocol.layer)),
}).pipe(Effect.provide(ElectronProtocol.layer.pipe(Layer.provide(unexpectedHttpClientLayer)))),
);

it("keeps executable sources host-restricted while allowing runtime network resources", () => {
Expand Down
Loading
Loading