import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import type { InstallationConfig } from "../configuration/customer.ts"; import type { Secret } from "../configuration/secrets.ts"; import { bodyJson, withResponse } from "../server/http.ts"; import { verifyOwnerSession } from "./session.ts"; const TIMEOUT_MS = 2_500; const MAX_BODY_BYTES = 8_192; class UpdateUnavailable extends Data.TaggedError("UpdateUnavailable") {} function isHtmlNavigation(request: Request) { if (request.method !== "GET") return false; const url = new URL(request.url); if ( url.pathname === "/flarebot-health.txt" || url.pathname.startsWith("/api") || url.pathname.startsWith("/agents/") || url.pathname.startsWith("/auth/") || request.headers.get("Upgrade")?.toLowerCase() === "websocket" ) return false; const destination = request.headers.get("Sec-Fetch-Dest"); return ( destination === "document" || (!destination && request.headers.get("Accept")?.includes("text/html")) ); } const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value); /** Redirect only when a bounded publisher reply proves a newer release. */ export function updateOnOwnerVisit( request: Request, installation: InstallationConfig, secret: Secret, production: boolean, network: typeof fetch = fetch, ): Effect.Effect { return Effect.gen(function* () { const release = installation.release; if ( !production || !installation.bridge || !release || new URL(request.url).origin !== installation.runtimeOrigin || !isHtmlNavigation(request) || !(yield* verifyOwnerSession(request, secret, installation)) ) return null; return yield* withResponse( network, new URL("/api/releases/latest", installation.controlPlaneOrigin), { headers: { Accept: "application/json" }, signal: request.signal }, TIMEOUT_MS, new UpdateUnavailable(), (response) => Effect.gen(function* () { if (!response.ok) return null; const data = yield* bodyJson( response, MAX_BODY_BYTES, new UpdateUnavailable(), ); if ( !data || typeof data !== "object" || !("artifactDigest" in data) || !digest(data.artifactDigest) || !("version" in data) || typeof data.version !== "string" || !("sourceRevision" in data) || typeof data.sourceRevision !== "string" || data.artifactDigest === release.artifactDigest ) return null; const destination = new URL( "/connect", installation.controlPlaneOrigin, ); destination.searchParams.set( "updateInstallation", installation.installationId, ); return new Response(null, { status: 303, headers: { Location: destination.href, "Cache-Control": "no-store", }, }); }), ).pipe(Effect.catchTag("UpdateUnavailable", () => Effect.succeed(null))); }); }