240 lines · 7.4 KB · TypeScript
RawDownload
import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as NodeTimersPromises from "node:timers/promises";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>()(  "ElectronProtocolRegistrationError",  {    scheme: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Failed to register Electron protocol scheme "${this.scheme}".`;  }} export class ElectronProtocolUnregistrationError extends Schema.TaggedErrorClass<ElectronProtocolUnregistrationError>()(  "ElectronProtocolUnregistrationError",  {    scheme: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Failed to unregister Electron protocol scheme "${this.scheme}".`;  }} 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<void, ElectronProtocolRegistrationError, Scope.Scope>;  }>()("@t3tools/desktop/electron/ElectronProtocol") {} export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrationInput): string {  const clerkOrigin = input.clerkFrontendApiHostname    ? `https://${input.clerkFrontendApiHostname}`    : undefined;  const scriptSources = [    "'self'",    "'unsafe-inline'",    "'wasm-unsafe-eval'",    ...(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:`,    `media-src 'self' ${input.scheme}: blob:`,    "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,  });} /** * Must run synchronously during process bootstrap, before Electron emits `ready`. */export function registerDesktopSchemePrivilegesSync(): void {  Electron.protocol.registerSchemesAsPrivileged([    {      scheme: DESKTOP_PRODUCTION_SCHEME,      privileges: {        standard: true,        secure: true,        supportFetchAPI: true,        corsEnabled: true,      },    },    {      scheme: DESKTOP_DEVELOPMENT_SCHEME,      privileges: {        standard: true,        secure: true,        supportFetchAPI: true,        corsEnabled: true,      },    },  ]);} const registerDesktopSchemePrivileges = Effect.sync(registerDesktopSchemePrivilegesSync).pipe(  Effect.withSpan("desktop.electron.protocol.registerSchemePrivileges"),); export const layerSchemePrivileges = Layer.effectDiscard(registerDesktopSchemePrivileges); async function proxyRequest(  request: Request,  targetOrigin: URL,  contentSecurityPolicy: string,): Promise<Response> {  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 headers = new Headers(request.headers);  const headersToRemove: string[] = [];  for (const name of headers.keys()) {    if (      name === "host" ||      name === "origin" ||      name === "referer" ||      name === "connection" ||      name === "content-length" ||      name === "accept-encoding" ||      name === "upgrade-insecure-requests" ||      name.startsWith("sec-fetch-")    ) {      headersToRemove.push(name);    }  }  for (const name of headersToRemove) {    headers.delete(name);  }  const init: RequestInit = {    method: request.method,    headers,  };  if (request.method !== "GET" && request.method !== "HEAD") {    init.body = request.body;    (init as RequestInit & { duplex: "half" }).duplex = "half";  }  const response =    request.method === "GET" || request.method === "HEAD"      ? await fetchWithTransientRetry(targetUrl.toString(), init)      : await Electron.net.fetch(targetUrl.toString(), init);  return withContentSecurityPolicy(response, contentSecurityPolicy);} const TRANSIENT_FETCH_RETRY_DELAYS_MS = [0, 50, 150] as const; async function fetchWithTransientRetry(url: string, init: RequestInit): Promise<Response> {  let lastError: unknown;   for (const delayMs of TRANSIENT_FETCH_RETRY_DELAYS_MS) {    if (delayMs > 0) {      await NodeTimersPromises.setTimeout(delayMs);    }     try {      return await Electron.net.fetch(url, init);    } catch (error) {      lastError = error;    }  }   throw lastError;} 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.try({            try: () => Electron.protocol.unhandle(input.scheme),            catch: (cause) =>              new ElectronProtocolUnregistrationError({                scheme: input.scheme,                cause,              }),          }).pipe(Effect.andThen(Ref.set(registered, false)), Effect.orDie),      );    },  );   return ElectronProtocol.of({ registerDesktopProtocol });}); export const layer = Layer.effect(ElectronProtocol, make);