import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Electron from "electron"; export const DESKTOP_HOST = "app"; export const DESKTOP_PRODUCTION_SCHEME = "t3code"; export const DESKTOP_DEVELOPMENT_SCHEME = "t3code-dev"; export function getDesktopScheme(isDevelopment: boolean): string { return isDevelopment ? DESKTOP_DEVELOPMENT_SCHEME : DESKTOP_PRODUCTION_SCHEME; } export function getDesktopOrigin(isDevelopment: boolean): string { return `${getDesktopScheme(isDevelopment)}://${DESKTOP_HOST}`; } export function getDesktopUrl(isDevelopment: boolean): string { return `${getDesktopOrigin(isDevelopment)}/`; } export class ElectronProtocolRegistrationError extends Schema.TaggedErrorClass()( "ElectronProtocolRegistrationError", { scheme: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to register ${this.scheme}: protocol.`; } } export interface DesktopProtocolRegistrationInput { readonly scheme: string; readonly targetOrigin: URL; readonly backendOrigin: URL; readonly clerkFrontendApiHostname: string | undefined; } export class ElectronProtocol extends Context.Service< ElectronProtocol, { readonly registerDesktopProtocol: ( input: DesktopProtocolRegistrationInput, ) => Effect.Effect; } >()("@t3tools/desktop/electron/ElectronProtocol") {} export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrationInput): string { const clerkOrigin = input.clerkFrontendApiHostname ? `https://${input.clerkFrontendApiHostname}` : undefined; const scriptSources = [ "'self'", "'unsafe-inline'", ...(clerkOrigin ? [clerkOrigin] : []), "https://challenges.cloudflare.com", ]; // The renderer connects directly to user-configured environments in addition to // the build-configured Clerk, relay, and OTLP endpoints. Those environment // origins are not known when this response policy is created, so restrict // connections by the network schemes the client supports instead of by host. const connectSources = ["'self'", "http:", "https:", "ws:", "wss:"]; return [ "default-src 'self'", `script-src ${scriptSources.join(" ")}`, `connect-src ${connectSources.join(" ")}`, `img-src 'self' ${input.scheme}: blob: data: http: https:`, "style-src 'self' 'unsafe-inline'", `font-src 'self' ${input.scheme}: data:`, "worker-src 'self' blob:", "frame-src 'self' https://challenges.cloudflare.com", "form-action 'self'", ].join("; "); } function withContentSecurityPolicy(response: Response, policy: string): Response { const headers = new Headers(response.headers); headers.set("Content-Security-Policy", policy); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }); } async function proxyRequest( request: Request, targetOrigin: URL, contentSecurityPolicy: string, ): Promise { const requestUrl = new URL(request.url); if (requestUrl.host !== DESKTOP_HOST) { return new Response(null, { status: 404 }); } const targetUrl = new URL(`${requestUrl.pathname}${requestUrl.search}`, targetOrigin); const init: RequestInit = { method: request.method, headers: request.headers, }; if (request.method !== "GET" && request.method !== "HEAD") { init.body = request.body; (init as RequestInit & { duplex: "half" }).duplex = "half"; } const response = await Electron.net.fetch(targetUrl.toString(), init); return withContentSecurityPolicy(response, contentSecurityPolicy); } export const make = Effect.gen(function* () { const registered = yield* Ref.make(false); const registerDesktopProtocol = Effect.fn("desktop.electron.protocol.registerDesktopProtocol")( function* (input: DesktopProtocolRegistrationInput) { if (yield* Ref.get(registered)) return; const contentSecurityPolicy = makeDesktopContentSecurityPolicy(input); yield* Effect.acquireRelease( Effect.try({ try: () => { Electron.protocol.handle(input.scheme, (request) => proxyRequest(request, input.targetOrigin, contentSecurityPolicy), ); }, catch: (cause) => new ElectronProtocolRegistrationError({ scheme: input.scheme, cause }), }).pipe(Effect.andThen(Ref.set(registered, true))), () => Effect.sync(() => { Electron.protocol.unhandle(input.scheme); }).pipe(Effect.andThen(Ref.set(registered, false))), ); }, ); return ElectronProtocol.of({ registerDesktopProtocol }); }); export const layer = Layer.effect(ElectronProtocol, make);