import { assertBodySize, defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getHeader, getCookie, setCookie, deleteCookie, getRequestURL, getRequestIP, readRawBody, } from "h3"; import type { H3Event } from "h3"; import { readMultipartFormData } from "h3"; import { DEFAULT_MODEL } from "../agent/default-model.js"; import { registerBuiltinEngines } from "../agent/engine/builtin.js"; import { OPENAI_BASE_URL_ENV_VAR, PROVIDER_ENV_META, } from "../agent/engine/provider-env-vars.js"; import type { AgentEngineEntry } from "../agent/engine/registry.js"; import { isAgentEngineSettingConfigured, getAgentEngineEntry, detectEngineFromEnv, detectEngineFromUserSecrets, isStoredEngineUsableForRequest, normalizeModelForEngine, } from "../agent/engine/registry.js"; import { canUpdateAgentLoopSettings, readAgentLoopSettings, resetAgentLoopSettings, validateMaxIterationsInput, writeAgentLoopSettings, } from "../agent/loop-settings.js"; import { getState, putState, deleteState, listComposeDrafts, getComposeDraft, putComposeDraft, deleteComposeDraft, deleteAllComposeDrafts, } from "../application-state/handlers.js"; import { mountBrowserSessionRoutes } from "../browser-sessions/routes.js"; import { mountDbAdminRoutes } from "../db-admin/routes.js"; import { getDbExec } from "../db/client.js"; import { getDatabaseRuntimeFingerprint, getRuntimeDebugFingerprint, runDatabaseSchemaHealthCheck, type DatabaseSchemaHealthResult, } from "../db/runtime-diagnostics.js"; import { ssrfSafeFetch } from "../extensions/url-safety.js"; import { uploadFile, getActiveFileUploadProviderForRequest, listFileUploadProviders, } from "../file-upload/index.js"; import { handleMcpConnect } from "../mcp/connect-route.js"; import { handleMcpOAuth, handleMcpOAuthAuthorizationServerMetadata, handleMcpOAuthProtectedResourceMetadata, } from "../mcp/oauth-route.js"; import { MCP_ROUTE_PREFIXES } from "../mcp/route-paths.js"; import { registerBuiltinNotificationChannels } from "../notifications/channels.js"; import { createNotificationsHandler } from "../notifications/routes.js"; import { getOrgContext } from "../org/context.js"; import { createProgressHandler } from "../progress/routes.js"; import { registerFrameworkSecrets } from "../secrets/register-framework-secrets.js"; import { createListSecretsHandler, createWriteSecretHandler, createTestSecretHandler, createAdHocSecretHandler, } from "../secrets/routes.js"; import { getSetting, putSetting, deleteSetting } from "../settings/store.js"; import { getUserSetting, putUserSetting, deleteUserSetting, } from "../settings/user-settings.js"; import { EMPTY_SPECULATION_RULES, resolveSsrCacheHeaders, } from "../shared/cache-control.js"; import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js"; import { llmConnectionTrackingProperties } from "../shared/llm-connection.js"; import { EMBED_TRANSPLANT_HEADER, isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js"; import { captureException } from "../tracking/error-capture.js"; import { track } from "../tracking/index.js"; import { registerBuiltinProviders } from "../tracking/providers.js"; import { validateTrackPayload } from "../tracking/route.js"; import { createAutomationsHandler } from "../triggers/routes.js"; import { createAgentEngineApiKeyHandler } from "./agent-engine-api-key-route.js"; import { getConfiguredAppBasePath, stripAppBasePath } from "./app-base-path.js"; import { getAppName } from "./app-name.js"; import { getSession, type AuthSession } from "./auth.js"; import { BUILDER_CONNECT_PARAM, BUILDER_CONNECT_OWNER_COOKIE, BUILDER_ENV_KEYS, BUILDER_OPENER_PARAM, BUILDER_RELAY_FLOW_HEADER, BUILDER_RELAY_SIGNATURE_HEADER, BUILDER_RELAY_STATE_PARAM, BUILDER_RELAY_TIMESTAMP_HEADER, BUILDER_STATE_PARAM, appendBuilderConnectToken, builderConnectTrackingProperties, buildBuilderCliAuthUrl, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, createBuilderRelayRequest, getBuilderConnectTrackingParams, getBuilderCliAuthCallbackOriginForEvent, getBuilderBrowserOriginForEvent, resolveBuilderCallbackReturnUrl, getBuilderBrowserStatusForEvent, resolveBuilderBranchProjectId, resolveBuilderPreviewRelayParentOrigin, resolveBuilderPreviewRelayTargetOrigin, resolveSafePreviewUrl, runBuilderAgent, signBuilderCallbackState, signBuilderPreviewRelayState, verifyBuilderRelayRequest, verifyBuilderPreviewRelayStateForCallback, verifyBuilderConnectTokenAndGetOwner, verifyBuilderCallbackStateAndGetOwner, signBuilderConnectToken, type BuilderConnectTrackingParams, type BuilderRelayCredentials, type BuilderPreviewRelayState, } from "./builder-browser.js"; import { captureError, registerErrorCaptureProvider } from "./capture-error.js"; import { getAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js"; import type { EnvKeyConfig } from "./create-server.js"; import { canUseDeployCredentialFallbackForRequest, readDeployCredentialEnv, resolveSecret, } from "./credential-provider.js"; import { createEmbedStartRouteHandler } from "./embed-route.js"; import { getH3App, awaitBootstrap, markDefaultPluginProvided, trackPluginInit, } from "./framework-request-handler.js"; import { createGatewayAccessCheckHandler } from "./gateway-access-check.js"; import { getAppBasePath, getOrigin } from "./google-oauth.js"; import { createGoogleRealtimeSessionHandler } from "./google-realtime-session.js"; import { readBody, DEFAULT_UPLOAD_MAX_FILE_BYTES, isAllowedUploadMimeType, } from "./h3-helpers.js"; import { isIdentitySsoEnabled } from "./identity-sso-store.js"; import { handleIdentitySso } from "./identity-sso.js"; import { createOpenRouteHandler } from "./open-route.js"; import { createPollEventsHandler } from "./poll-events.js"; import { createPollHandler } from "./poll.js"; import { createRealtimeTokenHandler } from "./realtime-token.js"; import { runWithRequestContext } from "./request-context.js"; import { findUnsupportedScopedKeyNames, saveKeyValuesToScopedSecrets, ScopedKeyStorageError, type ScopedKeySaveRequestScope, } from "./scoped-key-storage.js"; import { createTranscribeVoiceHandler } from "./transcribe-voice.js"; import { createVoiceProvidersStatusHandler } from "./voice-providers-status.js"; import { createWorkspaceProviderOAuthHandler } from "./workspace-provider-oauth.js"; /** * The base path prefix for all framework-level routes. * All agent-native core routes live under this namespace to avoid * collisions with template-specific `/api/*` routes. */ export const FRAMEWORK_ROUTE_PREFIX = "/_agent-native"; export const FRAMEWORK_EVENTS_ROUTE = `${FRAMEWORK_ROUTE_PREFIX}/events`; export const LEGACY_FRAMEWORK_EVENTS_ROUTE = `${FRAMEWORK_ROUTE_PREFIX}/poll-events`; export function normalizeAgentEngineStatusModel( entry: | { name: string; defaultModel: string; supportedModels: readonly string[] } | undefined, model: string | null | undefined, ): string { if (!entry) return model ?? DEFAULT_MODEL; return normalizeModelForEngine(entry, model ?? entry.defaultModel); } type AgentEngineStatusEntry = { name: string; defaultModel: string; supportedModels: readonly string[]; requiredEnvVars: readonly string[]; }; export interface AgentEngineStatusResult { configured: boolean; engine?: string; model?: string; source?: "settings" | "env" | "app_secrets"; envVar?: string; openAiBaseUrlConfigured?: boolean; } export interface AgentEngineStatusDeps< E extends AgentEngineStatusEntry = AgentEngineStatusEntry, > { readStoredEngine: () => Promise<{ engine?: string; model?: string } | null>; readOpenAiBaseUrlConfigured: () => boolean | Promise; isStoredEngineUsable: ( stored: unknown, entry: E, ) => boolean | Promise; detectFromUserSecrets: () => Promise; detectFromEnv: () => E | null | Promise; lookupEntry?: (engine: string) => E | undefined; } /** * Resolve "does this request have a usable AI provider" for one identity. * * Every call site pays for these lookups on a user-visible path (the agent * composer blocks on the status probe), so the two identity-independent reads * start together and the expensive `app_secrets` sweep only runs when the * cheaper sources have not already answered. */ export async function resolveAgentEngineStatus< E extends AgentEngineStatusEntry, >(deps: AgentEngineStatusDeps): Promise { const lookupEntry = (deps.lookupEntry ?? getAgentEngineEntry) as ( engine: string, ) => E | undefined; const [stored, openAiBaseUrlConfigured] = await Promise.all([ deps.readStoredEngine(), deps.readOpenAiBaseUrlConfigured(), ]); if (isAgentEngineSettingConfigured(stored)) { const engine = (stored as { engine: string }).engine; const entry = lookupEntry(engine); return { configured: true, engine, model: normalizeAgentEngineStatusModel(entry, stored?.model), source: "settings", openAiBaseUrlConfigured, }; } const envEntry = process.env.AGENT_ENGINE ? lookupEntry(process.env.AGENT_ENGINE) : undefined; if (envEntry) { if (!(await deps.isStoredEngineUsable({ engine: envEntry.name }, envEntry))) return { configured: false, openAiBaseUrlConfigured }; return { configured: true, engine: envEntry.name, model: envEntry.defaultModel ?? DEFAULT_MODEL, source: "env", envVar: "AGENT_ENGINE", openAiBaseUrlConfigured, }; } // Stored provider selections win over an existing Builder connection, so // this is checked before the app_secrets sweep — and the sweep is skipped // entirely when it answers. if (stored && typeof stored.engine === "string") { const entry = lookupEntry(stored.engine); if (entry && (await deps.isStoredEngineUsable(stored, entry))) { return { configured: true, engine: stored.engine, model: normalizeAgentEngineStatusModel(entry, stored.model), source: "env", envVar: entry.requiredEnvVars[0], openAiBaseUrlConfigured, }; } } // Per-user app_secrets — a user who connected Builder (or pasted their own // provider key) may not have any deploy-level env vars set. const detectedFromUser = await deps.detectFromUserSecrets(); if (detectedFromUser) { return { configured: true, engine: detectedFromUser.name, model: detectedFromUser.defaultModel ?? DEFAULT_MODEL, source: "app_secrets", envVar: detectedFromUser.requiredEnvVars[0], openAiBaseUrlConfigured, }; } const detected = await deps.detectFromEnv(); if (detected) { return { configured: true, engine: detected.name, model: detected.defaultModel ?? DEFAULT_MODEL, source: "env", envVar: detected.requiredEnvVars[0], openAiBaseUrlConfigured, }; } return { configured: false, openAiBaseUrlConfigured }; } const _agentEngineStatusInFlight = new Map< string, Promise >(); /** * Share one in-flight status resolution between concurrent probes of the same * identity. Several client surfaces probe this route on mount and the client * retries after its own timeout; without this each probe re-ran the whole * credential sweep. The entry is dropped as soon as the lookup settles, so a * joiner never sees an answer older than one lookup — no TTL, nothing to * invalidate when a provider is added or removed. The key carries the identity * that decides the answer, so no tenant can read another's result. */ export function shareAgentEngineStatusLookup( identityKey: string, compute: () => Promise, ): Promise { const existing = _agentEngineStatusInFlight.get(identityKey); if (existing) return existing; const started = compute().finally(() => { _agentEngineStatusInFlight.delete(identityKey); }); _agentEngineStatusInFlight.set(identityKey, started); return started; } export function agentEngineStatusIdentityKey( userEmail: string | undefined, orgId: string | undefined, ): string { return `${userEmail ?? ""}\u0000${orgId ?? ""}`; } function requestAgentEngineStatusDeps(): AgentEngineStatusDeps { return { readStoredEngine: async () => (await getSetting("agent-engine")) as { engine?: string; model?: string; } | null, readOpenAiBaseUrlConfigured: async () => { try { if (await resolveSecret(OPENAI_BASE_URL_ENV_VAR)) return true; } catch { /* fall through to deployment env when allowed */ } return ( canUseDeployCredentialFallbackForRequest(OPENAI_BASE_URL_ENV_VAR) && !!readDeployCredentialEnv(OPENAI_BASE_URL_ENV_VAR) ); }, isStoredEngineUsable: isStoredEngineUsableForRequest, detectFromUserSecrets: detectEngineFromUserSecrets, detectFromEnv: detectEngineFromEnv, }; } /** * Resolve the identity the status answer depends on. Both lookups memoize per * request inside their own helpers, so repeating them here stays cheap. */ async function resolveAgentEngineStatusIdentity( event: H3Event, ): Promise<{ userEmail: string | undefined; orgId: string | undefined }> { const session = await getSession(event).catch(() => null); const userEmail = session?.email; if (!userEmail) return { userEmail: undefined, orgId: undefined }; try { const orgCtx = await getOrgContext(event); return { userEmail, orgId: orgCtx.orgId ?? undefined }; } catch { /* org module not present in this template */ return { userEmail, orgId: undefined }; } } export function getFrameworkEnvKeys(): EnvKeyConfig[] { return [ { key: "ENABLE_BUILDER", label: "Enable Builder.io features" }, { key: "AGENT_ENGINE_PREFER_BYO_KEY", label: "Prefer BYO LLM key over Builder gateway (default: false — gateway wins)", }, { key: "RESEND_API_KEY", label: "Resend API key", helpText: "Enables transactional email, including password resets, invitations, share notifications, and dashboard reports.", }, { key: "SENDGRID_API_KEY", label: "SendGrid API key", helpText: "Enables transactional email, including password resets, invitations, share notifications, and dashboard reports.", }, { key: "EMAIL_FROM", label: "Email from address", helpText: "Sender address for transactional email. Required when using SendGrid.", }, ...Object.values(PROVIDER_ENV_META).map(({ envVar, label }) => ({ key: envVar, label, })), ]; } /** Result of the `/_agent-native/health` liveness + DB-warmup probe. */ export interface DbHealthProbeResult { /** The serverless function is live and served the request. */ ok: true; /** Database + optional schema readiness for stricter production monitors. */ ready: boolean; /** A trivial `SELECT 1` reached the database (false = no DB or unreachable). */ db: boolean; /** Round-trip time of the probe in milliseconds. */ ms: number; /** Redacted database routing details useful for deploy/runtime checks. */ database: { configured: boolean; source: string; dialect: string; urlHash?: string; appName?: string; authTokenConfigured: boolean; netlifyDatabaseUrlConfigured: boolean; }; /** Optional metadata-only schema compatibility check. */ schema?: DatabaseSchemaHealthResult; } /** * Run a trivial `SELECT 1` to confirm the database is reachable and, as a side * effect, keep a scale-to-zero serverless database (e.g. Neon) warm. Touching * the DB on a schedule prevents the multi-second cold-start that otherwise * stalls the next real user request. * * Always resolves: an app with no database (or a momentarily unreachable one) * is still live, so the probe reports `db: false` rather than throwing. The * `exec` parameter is injectable purely for tests. */ export async function runDbHealthProbe( exec: () => { execute: (sql: string) => Promise } = getDbExec, options: { schema?: boolean } = {}, ): Promise { const startedAt = Date.now(); let db = false; let schema: DatabaseSchemaHealthResult | undefined; const dbExec = exec(); try { await dbExec.execute("SELECT 1"); db = true; } catch { // Live even when the DB is unreachable or the app has no database. } if (db && options.schema) { schema = await runDatabaseSchemaHealthCheck({ exec: dbExec as ReturnType, }); } const database = getDatabaseRuntimeFingerprint(); return { ok: true, ready: db && (!schema || schema.ok), db, ms: Date.now() - startedAt, database: { configured: database.configured, source: database.source, dialect: database.dialect, urlHash: database.urlHash, appName: database.appName, authTokenConfigured: database.authTokenConfigured, netlifyDatabaseUrlConfigured: database.netlifyDatabaseUrlConfigured, }, ...(schema ? { schema } : {}), }; } const DEFAULT_BUILDER_WAITLIST_FORM_ID = "DYTHuM0jlV"; const DEFAULT_BUILDER_WAITLIST_FORMS_ORIGIN = "https://forms.agent-native.com"; const BUILDER_WAITLIST_FORM_SOURCE = "connect_builder_card"; const BUILDER_WAITLIST_DEFAULT_USE_CASE = "builder_agent_background_coding"; const BUILDER_WAITLIST_USE_CASES = new Set([ BUILDER_WAITLIST_DEFAULT_USE_CASE, "design_publish_app", "docs_build_online_waitlist", "docs_edit_online_waitlist", ]); const BUILDER_WAITLIST_FORM_TIMEOUT_MS = 8000; const BUILDER_WAITLIST_TEXT_LIMIT = 4000; const BUILDER_WAITLIST_RATE_LIMIT_WINDOW_MS = 60_000; const BUILDER_WAITLIST_RATE_LIMIT_MAX = 5; const builderWaitlistRateLimitHits = new Map< string, { count: number; resetAt: number } >(); interface BuilderWaitlistFormTarget { formId: string; formsOrigin: string; } export interface BuilderWaitlistBody { email?: unknown; prompt?: unknown; orgName?: unknown; appUrl?: unknown; pageUrl?: unknown; source?: unknown; template?: unknown; useCase?: unknown; } export function resolveFrameworkSseRoutes(sseRoute?: string): string[] { return Array.from( new Set([ sseRoute ?? FRAMEWORK_EVENTS_ROUTE, FRAMEWORK_EVENTS_ROUTE, LEGACY_FRAMEWORK_EVENTS_ROUTE, ]), ); } export const BUILDER_STATUS_ROUTE_SUFFIXES = [ "/builder/status", "/connection-status/builder", ] as const; export function mountBuilderStatusRouteAliases( mount: (path: string, handler: T) => void, prefix: string, handler: T, ): void { for (const routeSuffix of BUILDER_STATUS_ROUTE_SUFFIXES) { mount(`${prefix}${routeSuffix}`, handler); } } registerBuiltinEngines(); function cleanBuilderWaitlistText( value: unknown, maxLength = BUILDER_WAITLIST_TEXT_LIMIT, ): string | undefined { if (typeof value !== "string") return undefined; const trimmed = value.trim(); if (!trimmed) return undefined; return trimmed.slice(0, maxLength); } function normalizeBuilderWaitlistUseCase(value: unknown): string { const useCase = cleanBuilderWaitlistText(value, 100); return useCase && BUILDER_WAITLIST_USE_CASES.has(useCase) ? useCase : BUILDER_WAITLIST_DEFAULT_USE_CASE; } function normalizeBuilderWaitlistTemplate(value: unknown): string | undefined { const template = cleanBuilderWaitlistText(value, 100); return template && /^[a-z0-9][a-z0-9-]{0,99}$/.test(template) ? template : undefined; } function isValidWaitlistEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } export function isAnonymousWaitlistSessionEmail(email: string): boolean { return email.startsWith("anon-") && email.endsWith("@agent-native.com"); } export function resolveWaitlistEmail( sessionEmail: string | undefined, bodyEmail: unknown, ): string | null { const provided = cleanBuilderWaitlistText(bodyEmail, 320); if (provided && isValidWaitlistEmail(provided)) return provided; if (sessionEmail && !isAnonymousWaitlistSessionEmail(sessionEmail)) { return sessionEmail; } return null; } function normalizeWaitlistRateLimitPart(value: string): string { return value.trim().toLowerCase(); } function getBuilderWaitlistClientIp(event: H3Event): string | undefined { const trusted = getHeader(event, "x-nf-client-connection-ip") ?? getHeader(event, "cf-connecting-ip") ?? getHeader(event, "true-client-ip") ?? getHeader(event, "x-real-ip"); if (trusted && trusted.trim()) return trusted.trim(); const forwardedFor = getHeader(event, "x-forwarded-for"); const forwardedClientIp = forwardedFor?.split(",")[0]?.trim(); if (forwardedClientIp) return forwardedClientIp; try { return getRequestIP(event) ?? undefined; } catch { return undefined; } } function getBuilderWaitlistRateLimitKeys( event: H3Event, email: string, ): string[] { const clientIp = getBuilderWaitlistClientIp(event); return [ `email:${normalizeWaitlistRateLimitPart(email)}`, `ip:${normalizeWaitlistRateLimitPart(clientIp ?? "unknown")}`, ]; } export function checkBuilderWaitlistRateLimit( event: H3Event, email: string, now = Date.now(), ): { ok: true } | { ok: false; retryAfterSeconds: number } { const keys = getBuilderWaitlistRateLimitKeys(event, email); let retryAfterMs = 0; for (const key of keys) { const entry = builderWaitlistRateLimitHits.get(key); if (!entry) continue; if (entry.resetAt <= now) { builderWaitlistRateLimitHits.delete(key); continue; } if (entry.count >= BUILDER_WAITLIST_RATE_LIMIT_MAX) { retryAfterMs = Math.max(retryAfterMs, entry.resetAt - now); } } if (retryAfterMs > 0) { return { ok: false, retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1000)), }; } for (const key of keys) { const entry = builderWaitlistRateLimitHits.get(key); if (!entry || entry.resetAt <= now) { builderWaitlistRateLimitHits.set(key, { count: 1, resetAt: now + BUILDER_WAITLIST_RATE_LIMIT_WINDOW_MS, }); } else { entry.count += 1; } } return { ok: true }; } export function resetBuilderWaitlistRateLimitForTests() { builderWaitlistRateLimitHits.clear(); } function normalizeHttpOrigin(value: string): string | null { try { const url = new URL(value); if (url.protocol !== "http:" && url.protocol !== "https:") return null; return url.origin; } catch { return null; } } function isAgentNativeHostedRequest(event: H3Event): boolean { const hostname = getRequestURL(event).hostname.toLowerCase(); return ( hostname === "agent-native.com" || hostname.endsWith(".agent-native.com") ); } export function resolveBuilderWaitlistFormTargetForRequest( event: H3Event, ): BuilderWaitlistFormTarget | null { if (process.env.AGENT_NATIVE_DISABLE_BUILDER_WAITLIST_FORM === "1") { return null; } const envFormId = process.env.AGENT_NATIVE_BUILDER_WAITLIST_FORM_ID?.trim(); const envFormsOrigin = process.env.AGENT_NATIVE_BUILDER_WAITLIST_FORMS_ORIGIN?.trim(); const hasExplicitTarget = Boolean(envFormId || envFormsOrigin); if (!hasExplicitTarget && !isAgentNativeHostedRequest(event)) { return null; } const formId = envFormId || DEFAULT_BUILDER_WAITLIST_FORM_ID; const formsOrigin = normalizeHttpOrigin( envFormsOrigin || DEFAULT_BUILDER_WAITLIST_FORMS_ORIGIN, ); if (!formsOrigin) { throw new Error("Invalid Builder waitlist Forms origin"); } return { formId, formsOrigin }; } export function buildBuilderWaitlistFormPayload( event: H3Event, sessionEmail: string, body: BuilderWaitlistBody, ) { const appUrl = cleanBuilderWaitlistText(body.pageUrl ?? body.appUrl, 2000) ?? cleanBuilderWaitlistText(getHeader(event, "referer"), 2000) ?? getOrigin(event); const source = cleanBuilderWaitlistText(body.source, 100) ?? BUILDER_WAITLIST_FORM_SOURCE; const template = normalizeBuilderWaitlistTemplate(body.template); const useCase = normalizeBuilderWaitlistUseCase(body.useCase); return { data: { email: sessionEmail, orgName: cleanBuilderWaitlistText(body.orgName, 500), appUrl, prompt: cleanBuilderWaitlistText(body.prompt), source, template, useCase, }, _hp: "", _meta: { submitterEmail: sessionEmail, pageUrl: appUrl, source, template, useCase, }, }; } async function submitBuilderWaitlistForm( event: H3Event, sessionEmail: string, body: BuilderWaitlistBody, ): Promise<{ submitted: boolean; formId?: string }> { const target = resolveBuilderWaitlistFormTargetForRequest(event); if (!target) return { submitted: false }; const controller = new AbortController(); const timeout = setTimeout( () => controller.abort(), BUILDER_WAITLIST_FORM_TIMEOUT_MS, ); try { const res = await fetch( `${target.formsOrigin}/api/submit/${encodeURIComponent(target.formId)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify( buildBuilderWaitlistFormPayload(event, sessionEmail, body), ), signal: controller.signal, }, ); if (!res.ok) { throw new Error(`Forms waitlist submission failed (${res.status})`); } return { submitted: true, formId: target.formId }; } finally { clearTimeout(timeout); } } function parseBuilderCallbackBoolean( value: string | null | undefined, ): boolean | null { if (value == null || value === "") return null; return /^(1|true)$/i.test(value); } // Raster-only data-URI allowlist for avatar writes. SVG is deliberately absent: // data:image/svg+xml payloads can carry inline