From bdd580e2f5619d6d2d9463a7cd6d377a71400971 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 26 Feb 2026 16:18:39 -0800 Subject: [PATCH 1/7] Revert "fix(api): prevent Vercel CDN from caching Electric responses (#1864)" This reverts commit ae64aa46c51de258a9c8e56f5d08848fc3f2166a. --- apps/api/vercel.json | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 apps/api/vercel.json diff --git a/apps/api/vercel.json b/apps/api/vercel.json deleted file mode 100644 index 5870d3a8598..00000000000 --- a/apps/api/vercel.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "headers": [ - { - "source": "/api/electric/(.*)", - "headers": [ - { "key": "CDN-Cache-Control", "value": "no-store" }, - { "key": "Vercel-CDN-Cache-Control", "value": "no-store" } - ] - } - ] -} From bd517d51422906fa5dcf2ce53f55d8aff132940b Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 26 Feb 2026 22:14:03 -0800 Subject: [PATCH 2/7] feat(electric-proxy): add Cloudflare Worker proxy for Electric with JWT auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the API server's Electric proxy with a standalone Cloudflare Worker that authenticates via JWT instead of session tokens. This decouples Electric from the API server and enables edge caching via Cloudflare's Cache API. The worker supports both local Docker Electric (ELECTRIC_URL + ELECTRIC_SECRET) and Electric Cloud (ELECTRIC_SOURCE_ID + ELECTRIC_SOURCE_SECRET), matching the dual-mode pattern from the old API proxy. Desktop changes: - Rename electricJwt → jwt, add jwtClient() plugin to auth client - Fetch JWT from authClient.token() response body during hydration - Periodic JWT refresh every 50 minutes - Collections use JWT-only auth headers for the worker --- .superset/lib/setup/steps.sh | 16 ++- apps/desktop/src/renderer/lib/auth-client.ts | 18 +++ .../providers/AuthProvider/AuthProvider.tsx | 27 +++- .../CollectionsProvider/collections.ts | 5 +- apps/electric-proxy/.dev.vars.example | 11 ++ apps/electric-proxy/package.json | 22 +++ apps/electric-proxy/src/auth.ts | 35 +++++ apps/electric-proxy/src/electric.ts | 86 ++++++++++++ apps/electric-proxy/src/index.ts | 116 ++++++++++++++++ apps/electric-proxy/src/types.ts | 24 ++++ apps/electric-proxy/src/where.ts | 114 ++++++++++++++++ apps/electric-proxy/tsconfig.json | 12 ++ apps/electric-proxy/wrangler.jsonc | 10 ++ bun.lock | 127 ++++++++++++++++++ package.json | 2 +- 15 files changed, 618 insertions(+), 7 deletions(-) create mode 100644 apps/electric-proxy/.dev.vars.example create mode 100644 apps/electric-proxy/package.json create mode 100644 apps/electric-proxy/src/auth.ts create mode 100644 apps/electric-proxy/src/electric.ts create mode 100644 apps/electric-proxy/src/index.ts create mode 100644 apps/electric-proxy/src/types.ts create mode 100644 apps/electric-proxy/src/where.ts create mode 100644 apps/electric-proxy/tsconfig.json create mode 100644 apps/electric-proxy/wrangler.jsonc diff --git a/.superset/lib/setup/steps.sh b/.superset/lib/setup/steps.sh index 97ab7868ccc..95633961ab3 100644 --- a/.superset/lib/setup/steps.sh +++ b/.superset/lib/setup/steps.sh @@ -422,7 +422,7 @@ step_write_env() { # Offsets: +0 web, +1 api, +2 marketing, +3 admin, +4 docs, # +5 desktop vite, +6 notifications, +7 streams, +8 streams internal, +9 electric, # +10 caddy (HTTP/2 reverse proxy for API electric endpoint), +11 code inspector, - # +12 desktop automation (CDP) + # +12 desktop automation (CDP), +13 wrangler (electric-proxy worker) local BASE=$SUPERSET_PORT_BASE # App ports (fixed offsets from base) @@ -439,6 +439,7 @@ step_write_env() { local CADDY_ELECTRIC_PORT=$((BASE + 10)) local CODE_INSPECTOR_PORT=$((BASE + 11)) local DESKTOP_AUTOMATION_PORT=$((BASE + 12)) + local WRANGLER_PORT=$((BASE + 13)) echo "" echo "# Workspace Ports (allocated from SUPERSET_PORT_BASE=$BASE, range=20)" @@ -456,6 +457,7 @@ step_write_env() { write_env_var "CADDY_ELECTRIC_PORT" "$CADDY_ELECTRIC_PORT" write_env_var "CODE_INSPECTOR_PORT" "$CODE_INSPECTOR_PORT" write_env_var "DESKTOP_AUTOMATION_PORT" "$DESKTOP_AUTOMATION_PORT" + write_env_var "WRANGLER_PORT" "$WRANGLER_PORT" echo "" echo "# Cross-app URLs (overrides from root .env)" write_env_var "NEXT_PUBLIC_API_URL" "http://localhost:$API_PORT" @@ -510,12 +512,22 @@ step_write_env() { { "port": $STREAMS_INTERNAL_PORT, "label": "Streams Internal" }, { "port": $ELECTRIC_PORT, "label": "Electric" }, { "port": $CADDY_ELECTRIC_PORT, "label": "Caddy Electric" }, - { "port": $CODE_INSPECTOR_PORT, "label": "Code Inspector" } + { "port": $CODE_INSPECTOR_PORT, "label": "Code Inspector" }, + { "port": $WRANGLER_PORT, "label": "Electric Proxy (Wrangler)" } ] } PORTSJSON success "Port name mapping written to .superset/ports.json" + # Generate apps/electric-proxy/.dev.vars for local wrangler dev + cat > apps/electric-proxy/.dev.vars <(), stripeClient({ subscription: true }), apiKeyClient(), + jwtClient(), ], fetchOptions: { credentials: "include", @@ -40,5 +52,11 @@ export const authClient = createAuthClient({ context.headers.set("Authorization", `Bearer ${token}`); } }, + onResponse: async (context) => { + const token = context.response.headers.get("set-auth-jwt"); + if (token) { + setJwt(token); + } + }, }, }); diff --git a/apps/desktop/src/renderer/providers/AuthProvider/AuthProvider.tsx b/apps/desktop/src/renderer/providers/AuthProvider/AuthProvider.tsx index ae2230017f5..437a88fd4f6 100644 --- a/apps/desktop/src/renderer/providers/AuthProvider/AuthProvider.tsx +++ b/apps/desktop/src/renderer/providers/AuthProvider/AuthProvider.tsx @@ -1,5 +1,5 @@ import { type ReactNode, useEffect, useState } from "react"; -import { authClient, setAuthToken } from "renderer/lib/auth-client"; +import { authClient, setAuthToken, setJwt } from "renderer/lib/auth-client"; import { SupersetLogo } from "renderer/routes/sign-in/components/SupersetLogo/SupersetLogo"; import { electronTrpc } from "../../lib/electron-trpc"; @@ -31,6 +31,10 @@ export function AuthProvider({ children }: { children: ReactNode }) { err, ); } + const res = await authClient.token(); + if (res.data?.token) { + setJwt(res.data.token); + } } } if (!cancelled) { @@ -61,6 +65,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setIsHydrated(true); } else if (data === null) { setAuthToken(null); + setJwt(null); try { await refetchSession(); } catch (err) { @@ -73,6 +78,26 @@ export function AuthProvider({ children }: { children: ReactNode }) { }, }); + useEffect(() => { + if (!isHydrated) return; + + const refreshJwt = () => + authClient + .token() + .then((res) => { + if (res.data?.token) { + setJwt(res.data.token); + } + }) + .catch((err: unknown) => { + console.warn("[AuthProvider] JWT refresh failed", err); + }); + + refreshJwt(); + const interval = setInterval(refreshJwt, 50 * 60 * 1000); + return () => clearInterval(interval); + }, [isHydrated]); + if (!isHydrated) { return (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts index 348fbe0cf85..554f1066298 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts @@ -21,7 +21,7 @@ import type { Collection } from "@tanstack/react-db"; import { createCollection } from "@tanstack/react-db"; import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; import { env } from "renderer/env.renderer"; -import { getAuthToken } from "renderer/lib/auth-client"; +import { getAuthToken, getJwt } from "renderer/lib/auth-client"; import superjson from "superjson"; import { z } from "zod"; @@ -79,10 +79,9 @@ const apiClient = createTRPCProxyClient({ const electricHeaders = { Authorization: () => { - const token = getAuthToken(); + const token = getJwt(); return token ? `Bearer ${token}` : ""; }, - "X-Electric-Backend": "cloud", }; const organizationsCollection = createCollection( diff --git a/apps/electric-proxy/.dev.vars.example b/apps/electric-proxy/.dev.vars.example new file mode 100644 index 00000000000..02917682de0 --- /dev/null +++ b/apps/electric-proxy/.dev.vars.example @@ -0,0 +1,11 @@ +# Copy to .dev.vars and fill in values for local wrangler dev +AUTH_URL=http://localhost:3141 + +# Local Docker Electric (default for local dev) +ELECTRIC_URL=http://localhost:3149/v1/shape +ELECTRIC_SECRET=local_electric_dev_secret + +# Electric Cloud (set these to use cloud instead of local Docker) +# ELECTRIC_CLOUD_URL=https://api.electric-sql.cloud +# ELECTRIC_SOURCE_ID= +# ELECTRIC_SOURCE_SECRET= diff --git a/apps/electric-proxy/package.json b/apps/electric-proxy/package.json new file mode 100644 index 00000000000..9fa85989d94 --- /dev/null +++ b/apps/electric-proxy/package.json @@ -0,0 +1,22 @@ +{ + "name": "electric-proxy", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "dotenv -e ../../.env -- sh -c 'wrangler dev --port ${WRANGLER_PORT:-8787}'", + "deploy": "wrangler deploy", + "typecheck": "tsc --noEmit --emitDeclarationOnly false" + }, + "dependencies": { + "@superset/db": "workspace:*", + "drizzle-orm": "0.45.1", + "jose": "^6.1.3" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250214.0", + "@superset/typescript": "workspace:*", + "typescript": "^5.9.3", + "wrangler": "^4.14.4" + } +} diff --git a/apps/electric-proxy/src/auth.ts b/apps/electric-proxy/src/auth.ts new file mode 100644 index 00000000000..7c13dede68d --- /dev/null +++ b/apps/electric-proxy/src/auth.ts @@ -0,0 +1,35 @@ +import { createRemoteJWKSet, jwtVerify } from "jose"; +import type { AuthContext, Env } from "./types"; + +let jwks: ReturnType | null = null; + +function getJWKS(authUrl: string): ReturnType { + if (!jwks) { + jwks = createRemoteJWKSet(new URL("/api/auth/jwks", authUrl)); + } + return jwks; +} + +export async function verifyJWT( + token: string, + env: Env, +): Promise { + try { + const { payload } = await jwtVerify(token, getJWKS(env.AUTH_URL), { + issuer: env.AUTH_URL, + audience: env.AUTH_URL, + }); + + const sub = payload.sub; + const email = payload.email as string | undefined; + const organizationIds = payload.organizationIds as string[] | undefined; + + if (!sub || !organizationIds) { + return null; + } + + return { sub, email: email ?? "", organizationIds }; + } catch { + return null; + } +} diff --git a/apps/electric-proxy/src/electric.ts b/apps/electric-proxy/src/electric.ts new file mode 100644 index 00000000000..ae62f0efe2f --- /dev/null +++ b/apps/electric-proxy/src/electric.ts @@ -0,0 +1,86 @@ +import type { AuthContext, Env, WhereClause } from "./types"; + +/** + * Electric protocol query params that should be forwarded from the client request + * to the upstream Electric Cloud service. + * Source: @electric-sql/client/src/constants.ts + */ +const PROTOCOL_PARAMS = new Set([ + "live", + "live_sse", + "handle", + "offset", + "cursor", + "expired_handle", + "log", + "subset__where", + "subset__limit", + "subset__offset", + "subset__order_by", + "subset__params", + "subset__where_expr", + "subset__order_by_expr", + "cache-buster", +]); + +/** Column allowlists for sensitive tables */ +const COLUMN_RESTRICTIONS: Record = { + "auth.apikeys": "id,name,start,created_at,last_request", + integration_connections: + "id,organization_id,connected_by_user_id,provider,token_expires_at,external_org_id,external_org_name,config,created_at,updated_at", +}; + +export function buildUpstreamUrl( + clientUrl: URL, + tableName: string, + whereClause: WhereClause, + env: Env, +): URL { + const useCloud = env.ELECTRIC_SOURCE_ID && env.ELECTRIC_SOURCE_SECRET; + + const upstream = useCloud + ? new URL("/v1/shape", env.ELECTRIC_CLOUD_URL) + : new URL(env.ELECTRIC_URL ?? "http://localhost:3149/v1/shape"); + + if (useCloud) { + // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check + upstream.searchParams.set("source_id", env.ELECTRIC_SOURCE_ID!); + // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check + upstream.searchParams.set("secret", env.ELECTRIC_SOURCE_SECRET!); + } else if (env.ELECTRIC_SECRET) { + upstream.searchParams.set("secret", env.ELECTRIC_SECRET); + } + + // Forward Electric protocol params from the client request + for (const [key, value] of clientUrl.searchParams) { + if (PROTOCOL_PARAMS.has(key)) { + upstream.searchParams.set(key, value); + } + } + + upstream.searchParams.set("table", tableName); + upstream.searchParams.set("where", whereClause.fragment); + for (let i = 0; i < whereClause.params.length; i++) { + upstream.searchParams.set( + `params[${i + 1}]`, + String(whereClause.params[i]), + ); + } + + const columns = COLUMN_RESTRICTIONS[tableName]; + if (columns) { + upstream.searchParams.set("columns", columns); + } + + return upstream; +} + +/** + * Build a cache key URL scoped to the auth context. For most tables, the where + * clause already contains the organizationId so the cache key is naturally + * org-scoped. For `auth.organizations`, the where clause contains user-specific + * org IDs — less cache sharing but no cross-user leakage. + */ +export function buildCacheKey(upstreamUrl: URL, _auth: AuthContext): string { + return upstreamUrl.toString(); +} diff --git a/apps/electric-proxy/src/index.ts b/apps/electric-proxy/src/index.ts new file mode 100644 index 00000000000..b7200e70f8c --- /dev/null +++ b/apps/electric-proxy/src/index.ts @@ -0,0 +1,116 @@ +import { verifyJWT } from "./auth"; +import { buildCacheKey, buildUpstreamUrl } from "./electric"; +import type { Env } from "./types"; +import { buildWhereClause } from "./where"; + +const CORS_HEADERS: Record = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Authorization, Content-Type", + "Access-Control-Expose-Headers": + "electric-handle, electric-offset, electric-schema, electric-up-to-date, electric-cursor", +}; + +function corsResponse(status: number, body: string): Response { + return new Response(body, { status, headers: CORS_HEADERS }); +} + +export default { + async fetch(request: Request, env: Env): Promise { + // CORS preflight + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: CORS_HEADERS }); + } + + if (request.method !== "GET") { + return corsResponse(405, "Method not allowed"); + } + + // Extract and verify JWT + const authHeader = request.headers.get("Authorization"); + if (!authHeader?.startsWith("Bearer ")) { + return corsResponse(401, "Missing or invalid Authorization header"); + } + + const token = authHeader.slice(7); + const auth = await verifyJWT(token, env); + if (!auth) { + return corsResponse(401, "Invalid or expired token"); + } + + const url = new URL(request.url); + + // Validate required params + const tableName = url.searchParams.get("table"); + if (!tableName) { + return corsResponse(400, "Missing table parameter"); + } + + const organizationId = url.searchParams.get("organizationId"); + + // auth.organizations doesn't need organizationId — it uses the JWT's org list + if (tableName !== "auth.organizations") { + if (!organizationId) { + return corsResponse(400, "Missing organizationId parameter"); + } + if (!auth.organizationIds.includes(organizationId)) { + return corsResponse(403, "Not a member of this organization"); + } + } + + // Build where clause (pure function, no DB) + const whereClause = buildWhereClause( + tableName, + organizationId ?? "", + auth.organizationIds, + ); + if (!whereClause) { + return corsResponse(400, `Unknown table: ${tableName}`); + } + + // Build upstream Electric Cloud URL + const upstreamUrl = buildUpstreamUrl(url, tableName, whereClause, env); + const cacheKey = buildCacheKey(upstreamUrl, auth); + + // Check Cloudflare Cache API + const cache = caches.default; + const cacheRequest = new Request(cacheKey); + let response = await cache.match(cacheRequest); + + if (!response) { + // Cache miss — fetch from upstream Electric + response = await fetch(upstreamUrl.toString()); + + // Strip content-encoding to avoid decompression issues + const headers = new Headers(response.headers); + if (headers.get("content-encoding")) { + headers.delete("content-encoding"); + headers.delete("content-length"); + } + + response = new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); + + // Cache if Electric sent Cache-Control headers and the response is successful + if (response.ok && response.headers.has("cache-control")) { + // Use waitUntil-free put — Cloudflare handles it in the background + await cache.put(cacheRequest, response.clone()); + } + } + + // Add CORS headers to the response + const finalHeaders = new Headers(response.headers); + for (const [key, value] of Object.entries(CORS_HEADERS)) { + finalHeaders.set(key, value); + } + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: finalHeaders, + }); + }, +} satisfies ExportedHandler; diff --git a/apps/electric-proxy/src/types.ts b/apps/electric-proxy/src/types.ts new file mode 100644 index 00000000000..8aba34f7b50 --- /dev/null +++ b/apps/electric-proxy/src/types.ts @@ -0,0 +1,24 @@ +export interface Env { + AUTH_URL: string; + /** Local Docker Electric URL (e.g. http://localhost:3149/v1/shape) */ + ELECTRIC_URL?: string; + /** Local Electric secret */ + ELECTRIC_SECRET?: string; + /** Electric Cloud API URL (e.g. https://api.electric-sql.cloud) */ + ELECTRIC_CLOUD_URL?: string; + /** Electric Cloud source ID */ + ELECTRIC_SOURCE_ID?: string; + /** Electric Cloud source secret */ + ELECTRIC_SOURCE_SECRET?: string; +} + +export interface AuthContext { + sub: string; + email: string; + organizationIds: string[]; +} + +export interface WhereClause { + fragment: string; + params: unknown[]; +} diff --git a/apps/electric-proxy/src/where.ts b/apps/electric-proxy/src/where.ts new file mode 100644 index 00000000000..46abf924d51 --- /dev/null +++ b/apps/electric-proxy/src/where.ts @@ -0,0 +1,114 @@ +import { + agentCommands, + chatSessions, + devicePresence, + integrationConnections, + invitations, + members, + organizations, + projects, + sessionHosts, + subscriptions, + taskStatuses, + tasks, + workspaces, +} from "@superset/db/schema"; +import { eq, inArray, sql } from "drizzle-orm"; +import type { PgColumn, PgTable } from "drizzle-orm/pg-core"; +import { QueryBuilder } from "drizzle-orm/pg-core"; +import type { WhereClause } from "./types"; + +function build(table: PgTable, column: PgColumn, id: string): WhereClause { + const whereExpr = eq(sql`${sql.identifier(column.name)}`, id); + const qb = new QueryBuilder(); + const { sql: query, params } = qb + .select() + .from(table) + .where(whereExpr) + .toSQL(); + const fragment = query.replace(/^select .* from .* where\s+/i, ""); + return { fragment, params }; +} + +export function buildWhereClause( + tableName: string, + organizationId: string, + organizationIds: string[], +): WhereClause | null { + switch (tableName) { + case "tasks": + return build(tasks, tasks.organizationId, organizationId); + + case "task_statuses": + return build(taskStatuses, taskStatuses.organizationId, organizationId); + + case "projects": + return build(projects, projects.organizationId, organizationId); + + case "auth.members": + return build(members, members.organizationId, organizationId); + + case "auth.invitations": + return build(invitations, invitations.organizationId, organizationId); + + case "auth.organizations": { + if (organizationIds.length === 0) { + return { fragment: "1 = 0", params: [] }; + } + const whereExpr = inArray( + sql`${sql.identifier(organizations.id.name)}`, + organizationIds, + ); + const qb = new QueryBuilder(); + const { sql: query, params } = qb + .select() + .from(organizations) + .where(whereExpr) + .toSQL(); + const fragment = query.replace(/^select .* from .* where\s+/i, ""); + return { fragment, params }; + } + + case "auth.users": { + const fragment = `$1 = ANY("organization_ids")`; + return { fragment, params: [organizationId] }; + } + + case "device_presence": + return build( + devicePresence, + devicePresence.organizationId, + organizationId, + ); + + case "agent_commands": + return build(agentCommands, agentCommands.organizationId, organizationId); + + case "auth.apikeys": { + const fragment = `"metadata" LIKE '%"organizationId":"' || $1 || '"%'`; + return { fragment, params: [organizationId] }; + } + + case "integration_connections": + return build( + integrationConnections, + integrationConnections.organizationId, + organizationId, + ); + + case "subscriptions": + return build(subscriptions, subscriptions.referenceId, organizationId); + + case "workspaces": + return build(workspaces, workspaces.organizationId, organizationId); + + case "chat_sessions": + return build(chatSessions, chatSessions.organizationId, organizationId); + + case "session_hosts": + return build(sessionHosts, sessionHosts.organizationId, organizationId); + + default: + return null; + } +} diff --git a/apps/electric-proxy/tsconfig.json b/apps/electric-proxy/tsconfig.json new file mode 100644 index 00000000000..8a1f2d3087c --- /dev/null +++ b/apps/electric-proxy/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@superset/typescript/base.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types"], + "lib": ["ES2022", "ES2021.String"], + "moduleResolution": "Bundler", + "module": "ESNext", + "noEmit": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/apps/electric-proxy/wrangler.jsonc b/apps/electric-proxy/wrangler.jsonc new file mode 100644 index 00000000000..45ddfb9ce34 --- /dev/null +++ b/apps/electric-proxy/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "name": "electric-proxy", + "main": "src/index.ts", + "compatibility_date": "2025-01-01" + // All config via secrets (changeable without redeploy): + // AUTH_URL — e.g. https://api.superset.sh + // ELECTRIC_CLOUD_URL — e.g. https://api.electric-sql.cloud + // ELECTRIC_SOURCE_ID + // ELECTRIC_SOURCE_SECRET +} diff --git a/bun.lock b/bun.lock index da4b455e52e..8c1c4f68dba 100644 --- a/bun.lock +++ b/bun.lock @@ -329,6 +329,21 @@ "typescript": "^5.9.3", }, }, + "apps/electric-proxy": { + "name": "electric-proxy", + "version": "0.0.0", + "dependencies": { + "@superset/db": "workspace:*", + "drizzle-orm": "0.45.1", + "jose": "^6.1.3", + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250214.0", + "@superset/typescript": "workspace:*", + "typescript": "^5.9.3", + "wrangler": "^4.14.4", + }, + }, "apps/marketing": { "name": "@superset/marketing", "version": "0.1.0", @@ -1116,6 +1131,20 @@ "@chevrotain/utils": ["@chevrotain/utils@11.1.1", "", {}, "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.14.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260218.0" }, "optionalPeers": ["workerd"] }, "sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260305.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-chhKOpymo0Eh9J3nymrauMqKGboCc4uz/j0gA1G4gioMnKsN2ZDKJ+qjRZDnCoVGy8u2C4pxlmyIfsXCAfIzhQ=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260305.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K9aG2OQk5bBfOP+fyGPqLcqZ9OR3ra6uwnxJ8f2mveq2A2LsCI7ZeGxQiAj75Ti80ytH/gJffZIx4Np2JtU3aQ=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260305.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tt7XUoIw/cYFeGbkPkcZ6XX1aZm26Aju/4ih+DXxOosbBeGshFSrNJDBfAKKOvkjsAZymJ+WWVDBU+hmNaGfwA=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260305.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-72QTkY5EzylmvCZ8ZTrnJ9DctmQsfSof1OKyOWqu/pv/B2yACfuPMikq8RpPxvVu7hhS0ztGP6ZvXz72Htq4Zg=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260305.0", "", { "os": "win32", "cpu": "x64" }, "sha512-BA0uaQPOaI2F6mJtBDqplGnQQhpXCzwEMI33p/TnDxtSk9u8CGIfBFuI6uqo8mJ6ijIaPjeBLGOn2CiRMET4qg=="], + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260212.0", "", {}, "sha512-ZK+e8T/2tWBCrE8PoAi9oqTxcBen9Apq+dxbsy1R5LFVdB6M4pY+oP49OFuHTTezrvNXbyvmzbf/vjtrCPGdNg=="], "@code-inspector/core": ["@code-inspector/core@1.4.0", "", { "dependencies": { "@vue/compiler-dom": "^3.5.13", "chalk": "^4.1.1", "dotenv": "^16.1.4", "launch-ide": "1.4.0", "portfinder": "^1.0.28" } }, "sha512-LLBRdu0+Y+Ai5FXmRJtn72qTx//tfooxEeI9LFSL9G0H2T0k+cyFvg91njlkPQIDM2IA59N5pz5Jkx1zbTgC4A=="], @@ -1130,6 +1159,8 @@ "@code-inspector/webpack": ["@code-inspector/webpack@1.4.0", "", { "dependencies": { "@code-inspector/core": "1.4.0" } }, "sha512-ClUGCuowdLx36cTVPrz9E3TTP0WlTn3mSw4XPEQ9XWcEQpixFtImBxIRpC4xyztkYENTfEWZDy7mKxE4lthmDQ=="], + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], @@ -1640,6 +1671,12 @@ "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + "@posthog/core": ["@posthog/core@1.9.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-j7KSWxJTUtNyKynLt/p0hfip/3I46dWU2dk+pt7dKRoz2l5CYueHuHK4EO7Wlgno5yo1HO4sc4s30MXMTICHJw=="], "@prisma/instrumentation": ["@prisma/instrumentation@7.2.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-Rh9Z4x5kEj1OdARd7U18AtVrnL6rmLSI0qYShaB4W7Wx5BKbgzndWF+QnuzMb7GLfVdlT5aYCXoPQVYuYtVu0g=="], @@ -2054,6 +2091,8 @@ "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], + "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="], @@ -2790,6 +2829,8 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + "blueimp-md5": ["blueimp-md5@2.19.0", "", {}, "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="], "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], @@ -3216,6 +3257,8 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + "electric-proxy": ["electric-proxy@workspace:apps/electric-proxy"], + "electron": ["electron@40.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-0zOeyN8LB1KHIjVV5jyMmQmkqx3J8OkkVlab3p7vOM28jI46blxW7M52Tcdi6X2m5o2jj8ejOlAh5+boL3w8aQ=="], "electron-builder": ["electron-builder@26.4.0", "", { "dependencies": { "app-builder-lib": "26.4.0", "builder-util": "26.3.4", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.4.0", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-FCUqvdq2AULL+Db2SUGgjOYTbrgkPxZtCjqIZGnjH9p29pTWyesQqBIfvQBKa6ewqde87aWl49n/WyI/NyUBog=="], @@ -3266,6 +3309,8 @@ "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -4152,6 +4197,8 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "miniflare": ["miniflare@4.20260305.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260305.0", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-jVhtKJtiwaZa3rI+WgoLvSJmEazDsoUmAPYRUmEe2VO6VSbvkhbnDRm+dsPbYRatgNIExwrpqG1rv96jHiSb0w=="], + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -5156,6 +5203,8 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], @@ -5322,6 +5371,10 @@ "wonka": ["wonka@6.3.5", "", {}, "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw=="], + "workerd": ["workerd@1.20260305.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260305.0", "@cloudflare/workerd-darwin-arm64": "1.20260305.0", "@cloudflare/workerd-linux-64": "1.20260305.0", "@cloudflare/workerd-linux-arm64": "1.20260305.0", "@cloudflare/workerd-windows-64": "1.20260305.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-JkhfCLU+w+KbQmZ9k49IcDYc78GBo7eG8Mir8E2+KVjR7otQAmpcLlsous09YLh8WQ3Bt3Mi6/WMStvMAPukeA=="], + + "wrangler": ["wrangler@4.69.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.14.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260305.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260305.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260305.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-EmVfIM65I5b4ITHe3Y9R7zQyf4NUBQ1leStakMlWiVR9n6VlDwuEltyQI2l3i0JciDnWyR3uqe+T6C08ivniTQ=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -5358,6 +5411,10 @@ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod-from-json-schema": ["zod-from-json-schema@0.5.2", "", { "dependencies": { "zod": "^4.0.17" } }, "sha512-/dNaicfdhJTOuUd4RImbLUE2g5yrSzzDjI/S6C2vO2ecAGZzn9UcRVgtyLSnENSmAOBRiSpUdzDS6fDWX3Z35g=="], @@ -5410,6 +5467,8 @@ "@code-inspector/vite/chalk": ["chalk@4.1.1", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg=="], + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@develar/schema-utils/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "@develar/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], @@ -5568,6 +5627,12 @@ "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], + "@poppinss/colors/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], "@puppeteer/browsers/tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="], @@ -6022,6 +6087,10 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "miniflare/undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], + + "miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "minipass-fetch/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -6242,12 +6311,18 @@ "whatwg-url-without-unicode/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "wrangler/path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "xcode/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], + "youch/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "zod-from-json-schema-v3/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@a2a-js/sdk/@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="], @@ -7606,6 +7681,58 @@ "webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/package.json b/package.json index eb28c9090bf..149b3155d63 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "packageManager": "bun@1.3.6", "private": true, "scripts": { - "dev": "turbo run dev dev:caddy --filter=@superset/api --filter=@superset/web --filter=@superset/desktop --filter=//", + "dev": "turbo run dev dev:caddy --filter=@superset/api --filter=@superset/web --filter=@superset/desktop --filter=electric-proxy --filter=//", "dev:all": "turbo dev", "dev:caddy": "dotenv -- caddy run --config Caddyfile", "dev:docs": "turbo dev --filter=@superset/docs", From 70ba4591d799338ddf52ebbca8f39fb074515218 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 26 Feb 2026 22:36:49 -0800 Subject: [PATCH 3/7] refactor(electric-proxy): move AuthContext/WhereClause to auth.ts, simplify types.ts to Env only --- apps/electric-proxy/src/auth.ts | 20 +++++++++++++++----- apps/electric-proxy/src/electric.ts | 3 ++- apps/electric-proxy/src/index.ts | 2 +- apps/electric-proxy/src/types.ts | 16 ---------------- apps/electric-proxy/src/where.ts | 2 +- apps/electric-proxy/wrangler.jsonc | 5 ----- 6 files changed, 19 insertions(+), 29 deletions(-) diff --git a/apps/electric-proxy/src/auth.ts b/apps/electric-proxy/src/auth.ts index 7c13dede68d..51e21d78cbb 100644 --- a/apps/electric-proxy/src/auth.ts +++ b/apps/electric-proxy/src/auth.ts @@ -1,5 +1,15 @@ import { createRemoteJWKSet, jwtVerify } from "jose"; -import type { AuthContext, Env } from "./types"; + +export interface AuthContext { + sub: string; + email: string; + organizationIds: string[]; +} + +export interface WhereClause { + fragment: string; + params: unknown[]; +} let jwks: ReturnType | null = null; @@ -12,12 +22,12 @@ function getJWKS(authUrl: string): ReturnType { export async function verifyJWT( token: string, - env: Env, + authUrl: string, ): Promise { try { - const { payload } = await jwtVerify(token, getJWKS(env.AUTH_URL), { - issuer: env.AUTH_URL, - audience: env.AUTH_URL, + const { payload } = await jwtVerify(token, getJWKS(authUrl), { + issuer: authUrl, + audience: authUrl, }); const sub = payload.sub; diff --git a/apps/electric-proxy/src/electric.ts b/apps/electric-proxy/src/electric.ts index ae62f0efe2f..29300b57924 100644 --- a/apps/electric-proxy/src/electric.ts +++ b/apps/electric-proxy/src/electric.ts @@ -1,4 +1,5 @@ -import type { AuthContext, Env, WhereClause } from "./types"; +import type { AuthContext, WhereClause } from "./auth"; +import type { Env } from "./types"; /** * Electric protocol query params that should be forwarded from the client request diff --git a/apps/electric-proxy/src/index.ts b/apps/electric-proxy/src/index.ts index b7200e70f8c..c0e438d79f5 100644 --- a/apps/electric-proxy/src/index.ts +++ b/apps/electric-proxy/src/index.ts @@ -33,7 +33,7 @@ export default { } const token = authHeader.slice(7); - const auth = await verifyJWT(token, env); + const auth = await verifyJWT(token, env.AUTH_URL); if (!auth) { return corsResponse(401, "Invalid or expired token"); } diff --git a/apps/electric-proxy/src/types.ts b/apps/electric-proxy/src/types.ts index 8aba34f7b50..4c67ef36386 100644 --- a/apps/electric-proxy/src/types.ts +++ b/apps/electric-proxy/src/types.ts @@ -1,24 +1,8 @@ export interface Env { AUTH_URL: string; - /** Local Docker Electric URL (e.g. http://localhost:3149/v1/shape) */ ELECTRIC_URL?: string; - /** Local Electric secret */ ELECTRIC_SECRET?: string; - /** Electric Cloud API URL (e.g. https://api.electric-sql.cloud) */ ELECTRIC_CLOUD_URL?: string; - /** Electric Cloud source ID */ ELECTRIC_SOURCE_ID?: string; - /** Electric Cloud source secret */ ELECTRIC_SOURCE_SECRET?: string; } - -export interface AuthContext { - sub: string; - email: string; - organizationIds: string[]; -} - -export interface WhereClause { - fragment: string; - params: unknown[]; -} diff --git a/apps/electric-proxy/src/where.ts b/apps/electric-proxy/src/where.ts index 46abf924d51..ff3bad74173 100644 --- a/apps/electric-proxy/src/where.ts +++ b/apps/electric-proxy/src/where.ts @@ -16,7 +16,7 @@ import { import { eq, inArray, sql } from "drizzle-orm"; import type { PgColumn, PgTable } from "drizzle-orm/pg-core"; import { QueryBuilder } from "drizzle-orm/pg-core"; -import type { WhereClause } from "./types"; +import type { WhereClause } from "./auth"; function build(table: PgTable, column: PgColumn, id: string): WhereClause { const whereExpr = eq(sql`${sql.identifier(column.name)}`, id); diff --git a/apps/electric-proxy/wrangler.jsonc b/apps/electric-proxy/wrangler.jsonc index 45ddfb9ce34..107d21dd1c6 100644 --- a/apps/electric-proxy/wrangler.jsonc +++ b/apps/electric-proxy/wrangler.jsonc @@ -2,9 +2,4 @@ "name": "electric-proxy", "main": "src/index.ts", "compatibility_date": "2025-01-01" - // All config via secrets (changeable without redeploy): - // AUTH_URL — e.g. https://api.superset.sh - // ELECTRIC_CLOUD_URL — e.g. https://api.electric-sql.cloud - // ELECTRIC_SOURCE_ID - // ELECTRIC_SOURCE_SECRET } From 3c2b71530713ec805e7170747732919a2f5da973 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 26 Feb 2026 23:19:12 -0800 Subject: [PATCH 4/7] feat: point desktop Electric URL to Cloudflare Worker and add CI deploy job --- .github/workflows/deploy-production.yml | 30 +++++++++++++++++++++++ apps/desktop/electron.vite.config.ts | 2 +- apps/desktop/src/main/env.main.ts | 2 +- apps/desktop/src/renderer/env.renderer.ts | 2 +- apps/desktop/vite/helpers.ts | 2 +- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index ba1115973a5..af5c3c7d13c 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -448,6 +448,36 @@ jobs: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} run: flyctl deploy . --config fly.toml --remote-only + deploy-electric-proxy: + name: Deploy Electric Proxy to Cloudflare + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.3 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + + - name: Install dependencies + run: bun install --frozen + + - name: Deploy Worker + working-directory: apps/electric-proxy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: bunx wrangler deploy + deploy-docs: name: Deploy Docs to Vercel runs-on: ubuntu-latest diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index e49a2df401b..e09f1f07e5b 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -167,7 +167,7 @@ export default defineConfig({ ), "process.env.NEXT_PUBLIC_ELECTRIC_URL": defineEnv( process.env.NEXT_PUBLIC_ELECTRIC_URL, - "https://api.superset.sh/api/electric", + "https://electric-proxy.avi-6ac.workers.dev", ), "process.env.NEXT_PUBLIC_DOCS_URL": defineEnv( process.env.NEXT_PUBLIC_DOCS_URL, diff --git a/apps/desktop/src/main/env.main.ts b/apps/desktop/src/main/env.main.ts index 4e832791a7f..851535704fc 100644 --- a/apps/desktop/src/main/env.main.ts +++ b/apps/desktop/src/main/env.main.ts @@ -18,7 +18,7 @@ export const env = createEnv({ NEXT_PUBLIC_STREAMS_URL: z.url().default("https://streams.superset.sh"), NEXT_PUBLIC_ELECTRIC_URL: z .url() - .default("https://api.superset.sh/api/electric"), + .default("https://electric-proxy.avi-6ac.workers.dev"), NEXT_PUBLIC_WEB_URL: z.url().default("https://app.superset.sh"), NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), diff --git a/apps/desktop/src/renderer/env.renderer.ts b/apps/desktop/src/renderer/env.renderer.ts index 404ce33ec36..4c41ac232e1 100644 --- a/apps/desktop/src/renderer/env.renderer.ts +++ b/apps/desktop/src/renderer/env.renderer.ts @@ -19,7 +19,7 @@ const envSchema = z.object({ NEXT_PUBLIC_WEB_URL: z.url().default("https://app.superset.sh"), NEXT_PUBLIC_ELECTRIC_URL: z .url() - .default("https://api.superset.sh/api/electric"), + .default("https://electric-proxy.avi-6ac.workers.dev"), NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), SENTRY_DSN_DESKTOP: z.string().optional(), diff --git a/apps/desktop/vite/helpers.ts b/apps/desktop/vite/helpers.ts index 1d175727585..4b6292822ad 100644 --- a/apps/desktop/vite/helpers.ts +++ b/apps/desktop/vite/helpers.ts @@ -78,7 +78,7 @@ export function htmlEnvTransformPlugin(): Plugin { /%NEXT_PUBLIC_ELECTRIC_URL%/g, new URL( process.env.NEXT_PUBLIC_ELECTRIC_URL || - "https://api.superset.sh/api/electric", + "https://electric-proxy.avi-6ac.workers.dev", ).origin, ) .replace( From 10444fa74053b162cae5b8f3b242b4daaa32de87 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 2 Mar 2026 17:26:36 -0800 Subject: [PATCH 5/7] fix: add local Electric vars to .dev.vars, guard JWT hydration, accept JWTs in old proxy --- .superset/lib/setup/steps.sh | 3 +- .../src/app/api/electric/[...path]/route.ts | 50 ++++++++++++++----- .../providers/AuthProvider/AuthProvider.tsx | 13 +++-- apps/electric-proxy/.dev.vars.example | 11 ++-- apps/electric-proxy/src/electric.ts | 17 +------ apps/electric-proxy/src/index.ts | 12 ----- 6 files changed, 54 insertions(+), 52 deletions(-) diff --git a/.superset/lib/setup/steps.sh b/.superset/lib/setup/steps.sh index 95633961ab3..4e1d2fca2c2 100644 --- a/.superset/lib/setup/steps.sh +++ b/.superset/lib/setup/steps.sh @@ -519,9 +519,10 @@ step_write_env() { PORTSJSON success "Port name mapping written to .superset/ports.json" - # Generate apps/electric-proxy/.dev.vars for local wrangler dev cat > apps/electric-proxy/.dev.vars < { + const bearer = request.headers.get("Authorization"); + if (bearer?.startsWith("Bearer ")) { + const token = bearer.slice(7); + try { + const { payload } = await auth.api.verifyJWT({ body: { token } }); + if (payload?.sub && Array.isArray(payload.organizationIds)) { + return { + userId: payload.sub, + email: (payload.email as string) ?? "", + organizationIds: payload.organizationIds as string[], + }; + } + } catch {} + } + + const sessionData = await auth.api.getSession({ headers: request.headers }); + if (!sessionData?.user) return null; + return { + userId: sessionData.user.id, + email: sessionData.user.email ?? "", + organizationIds: sessionData.session.organizationIds ?? [], + }; +} + export async function GET(request: Request): Promise { - const sessionData = await auth.api.getSession({ - headers: request.headers, - }); - if (!sessionData?.user) { + const authInfo = await authenticate(request); + if (!authInfo) { return new Response("Unauthorized", { status: 401 }); } const url = new URL(request.url); - // Use client-sent organizationId, falling back to session for older clients. - // TODO(2026-02-26): Remove activeOrganizationId fallback once all clients send organizationId param. - const organizationId = - url.searchParams.get("organizationId") ?? - sessionData.session.activeOrganizationId; - const allowedOrgIds = sessionData.session.organizationIds ?? []; + const organizationId = url.searchParams.get("organizationId"); - if (organizationId && !allowedOrgIds.includes(organizationId)) { + if (organizationId && !authInfo.organizationIds.includes(organizationId)) { return new Response("Not a member of this organization", { status: 403 }); } @@ -28,7 +52,7 @@ export async function GET(request: Request): Promise { env.ELECTRIC_SOURCE_ID && env.ELECTRIC_SOURCE_SECRET && (request.headers.get("x-electric-backend") === "cloud" || - sessionData.user.email?.endsWith("@superset.sh")); + authInfo.email?.endsWith("@superset.sh")); const originUrl = useCloud ? new URL("/v1/shape", "https://api.electric-sql.cloud") @@ -57,7 +81,7 @@ export async function GET(request: Request): Promise { const whereClause = await buildWhereClause( tableName, organizationId ?? "", - sessionData.user.id, + authInfo.userId, ); if (!whereClause) { return new Response(`Unknown table: ${tableName}`, { status: 400 }); diff --git a/apps/desktop/src/renderer/providers/AuthProvider/AuthProvider.tsx b/apps/desktop/src/renderer/providers/AuthProvider/AuthProvider.tsx index 437a88fd4f6..6a055e59228 100644 --- a/apps/desktop/src/renderer/providers/AuthProvider/AuthProvider.tsx +++ b/apps/desktop/src/renderer/providers/AuthProvider/AuthProvider.tsx @@ -31,9 +31,16 @@ export function AuthProvider({ children }: { children: ReactNode }) { err, ); } - const res = await authClient.token(); - if (res.data?.token) { - setJwt(res.data.token); + try { + const res = await authClient.token(); + if (res.data?.token) { + setJwt(res.data.token); + } + } catch (err) { + console.warn( + "[AuthProvider] JWT fetch failed during hydration", + err, + ); } } } diff --git a/apps/electric-proxy/.dev.vars.example b/apps/electric-proxy/.dev.vars.example index 02917682de0..ed052bd7d37 100644 --- a/apps/electric-proxy/.dev.vars.example +++ b/apps/electric-proxy/.dev.vars.example @@ -1,11 +1,6 @@ -# Copy to .dev.vars and fill in values for local wrangler dev AUTH_URL=http://localhost:3141 - -# Local Docker Electric (default for local dev) ELECTRIC_URL=http://localhost:3149/v1/shape ELECTRIC_SECRET=local_electric_dev_secret - -# Electric Cloud (set these to use cloud instead of local Docker) -# ELECTRIC_CLOUD_URL=https://api.electric-sql.cloud -# ELECTRIC_SOURCE_ID= -# ELECTRIC_SOURCE_SECRET= +ELECTRIC_CLOUD_URL=https://api.electric-sql.cloud +ELECTRIC_SOURCE_ID= +ELECTRIC_SOURCE_SECRET= diff --git a/apps/electric-proxy/src/electric.ts b/apps/electric-proxy/src/electric.ts index 29300b57924..f8539e5b75f 100644 --- a/apps/electric-proxy/src/electric.ts +++ b/apps/electric-proxy/src/electric.ts @@ -1,11 +1,6 @@ import type { AuthContext, WhereClause } from "./auth"; import type { Env } from "./types"; -/** - * Electric protocol query params that should be forwarded from the client request - * to the upstream Electric Cloud service. - * Source: @electric-sql/client/src/constants.ts - */ const PROTOCOL_PARAMS = new Set([ "live", "live_sse", @@ -24,7 +19,6 @@ const PROTOCOL_PARAMS = new Set([ "cache-buster", ]); -/** Column allowlists for sensitive tables */ const COLUMN_RESTRICTIONS: Record = { "auth.apikeys": "id,name,start,created_at,last_request", integration_connections: @@ -44,15 +38,14 @@ export function buildUpstreamUrl( : new URL(env.ELECTRIC_URL ?? "http://localhost:3149/v1/shape"); if (useCloud) { - // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check + // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud upstream.searchParams.set("source_id", env.ELECTRIC_SOURCE_ID!); - // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check + // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud upstream.searchParams.set("secret", env.ELECTRIC_SOURCE_SECRET!); } else if (env.ELECTRIC_SECRET) { upstream.searchParams.set("secret", env.ELECTRIC_SECRET); } - // Forward Electric protocol params from the client request for (const [key, value] of clientUrl.searchParams) { if (PROTOCOL_PARAMS.has(key)) { upstream.searchParams.set(key, value); @@ -76,12 +69,6 @@ export function buildUpstreamUrl( return upstream; } -/** - * Build a cache key URL scoped to the auth context. For most tables, the where - * clause already contains the organizationId so the cache key is naturally - * org-scoped. For `auth.organizations`, the where clause contains user-specific - * org IDs — less cache sharing but no cross-user leakage. - */ export function buildCacheKey(upstreamUrl: URL, _auth: AuthContext): string { return upstreamUrl.toString(); } diff --git a/apps/electric-proxy/src/index.ts b/apps/electric-proxy/src/index.ts index c0e438d79f5..19630a8eeb9 100644 --- a/apps/electric-proxy/src/index.ts +++ b/apps/electric-proxy/src/index.ts @@ -17,7 +17,6 @@ function corsResponse(status: number, body: string): Response { export default { async fetch(request: Request, env: Env): Promise { - // CORS preflight if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: CORS_HEADERS }); } @@ -26,7 +25,6 @@ export default { return corsResponse(405, "Method not allowed"); } - // Extract and verify JWT const authHeader = request.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { return corsResponse(401, "Missing or invalid Authorization header"); @@ -40,7 +38,6 @@ export default { const url = new URL(request.url); - // Validate required params const tableName = url.searchParams.get("table"); if (!tableName) { return corsResponse(400, "Missing table parameter"); @@ -48,7 +45,6 @@ export default { const organizationId = url.searchParams.get("organizationId"); - // auth.organizations doesn't need organizationId — it uses the JWT's org list if (tableName !== "auth.organizations") { if (!organizationId) { return corsResponse(400, "Missing organizationId parameter"); @@ -58,7 +54,6 @@ export default { } } - // Build where clause (pure function, no DB) const whereClause = buildWhereClause( tableName, organizationId ?? "", @@ -68,20 +63,16 @@ export default { return corsResponse(400, `Unknown table: ${tableName}`); } - // Build upstream Electric Cloud URL const upstreamUrl = buildUpstreamUrl(url, tableName, whereClause, env); const cacheKey = buildCacheKey(upstreamUrl, auth); - // Check Cloudflare Cache API const cache = caches.default; const cacheRequest = new Request(cacheKey); let response = await cache.match(cacheRequest); if (!response) { - // Cache miss — fetch from upstream Electric response = await fetch(upstreamUrl.toString()); - // Strip content-encoding to avoid decompression issues const headers = new Headers(response.headers); if (headers.get("content-encoding")) { headers.delete("content-encoding"); @@ -94,14 +85,11 @@ export default { headers, }); - // Cache if Electric sent Cache-Control headers and the response is successful if (response.ok && response.headers.has("cache-control")) { - // Use waitUntil-free put — Cloudflare handles it in the background await cache.put(cacheRequest, response.clone()); } } - // Add CORS headers to the response const finalHeaders = new Headers(response.headers); for (const [key, value] of Object.entries(CORS_HEADERS)) { finalHeaders.set(key, value); From 9b4e62f810288d8ed88d5d320d017c6440e97ae4 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 2 Mar 2026 17:44:35 -0800 Subject: [PATCH 6/7] feat: add electric-cloud feature flag, simplify proxy routing - Old Vercel proxy always routes to Fly.io (removed cloud/email check) - Cloudflare Worker always routes to Electric Cloud (removed dual-mode) - Desktop uses PostHog `electric-cloud` flag to pick proxy - Default: old proxy (Fly.io), flag on: Cloudflare Worker (Electric Cloud) --- .superset/lib/setup/steps.sh | 2 -- .../src/app/api/electric/[...path]/route.ts | 25 ++----------------- .../CollectionsProvider.tsx | 15 ++++++++++- .../CollectionsProvider/collections.ts | 7 +++++- apps/electric-proxy/.dev.vars.example | 2 -- apps/electric-proxy/src/electric.ts | 17 +++---------- apps/electric-proxy/src/types.ts | 8 +++--- packages/shared/src/constants.ts | 1 + 8 files changed, 29 insertions(+), 48 deletions(-) diff --git a/.superset/lib/setup/steps.sh b/.superset/lib/setup/steps.sh index 4e1d2fca2c2..598ca8fee37 100644 --- a/.superset/lib/setup/steps.sh +++ b/.superset/lib/setup/steps.sh @@ -521,8 +521,6 @@ PORTSJSON cat > apps/electric-proxy/.dev.vars < { if (payload?.sub && Array.isArray(payload.organizationIds)) { return { userId: payload.sub, - email: (payload.email as string) ?? "", organizationIds: payload.organizationIds as string[], }; } @@ -29,7 +27,6 @@ async function authenticate(request: Request): Promise { if (!sessionData?.user) return null; return { userId: sessionData.user.id, - email: sessionData.user.email ?? "", organizationIds: sessionData.session.organizationIds ?? [], }; } @@ -48,24 +45,8 @@ export async function GET(request: Request): Promise { return new Response("Not a member of this organization", { status: 403 }); } - const useCloud = - env.ELECTRIC_SOURCE_ID && - env.ELECTRIC_SOURCE_SECRET && - (request.headers.get("x-electric-backend") === "cloud" || - authInfo.email?.endsWith("@superset.sh")); - - const originUrl = useCloud - ? new URL("/v1/shape", "https://api.electric-sql.cloud") - : new URL(env.ELECTRIC_URL); - - if (useCloud) { - // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check above - originUrl.searchParams.set("source_id", env.ELECTRIC_SOURCE_ID!); - // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud check above - originUrl.searchParams.set("secret", env.ELECTRIC_SOURCE_SECRET!); - } else { - originUrl.searchParams.set("secret", env.ELECTRIC_SECRET); - } + const originUrl = new URL(env.ELECTRIC_URL); + originUrl.searchParams.set("secret", env.ELECTRIC_SECRET); url.searchParams.forEach((value, key) => { if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) { @@ -110,8 +91,6 @@ export async function GET(request: Request): Promise { const response = await fetch(originUrl.toString()); const headers = new Headers(response.headers); - headers.append("Vary", "Authorization, X-Electric-Backend"); - if (headers.get("content-encoding")) { headers.delete("content-encoding"); headers.delete("content-length"); diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx index 432ea4417b8..9ea4da122a4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx @@ -1,3 +1,5 @@ +import { FEATURE_FLAGS } from "@superset/shared/constants"; +import { useFeatureFlagEnabled } from "posthog-js/react"; import { createContext, type ReactNode, @@ -10,7 +12,11 @@ import { import { env } from "renderer/env.renderer"; import { authClient } from "renderer/lib/auth-client"; import { MOCK_ORG_ID } from "shared/constants"; -import { getCollections, preloadCollections } from "./collections"; +import { + getCollections, + preloadCollections, + setElectricUrl, +} from "./collections"; type CollectionsContextType = ReturnType & { switchOrganization: (organizationId: string) => Promise; @@ -20,9 +26,16 @@ const CollectionsContext = createContext(null); export function CollectionsProvider({ children }: { children: ReactNode }) { const { data: session, refetch: refetchSession } = authClient.useSession(); + const useElectricCloud = useFeatureFlagEnabled(FEATURE_FLAGS.ELECTRIC_CLOUD); const activeOrganizationId = env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : session?.session?.activeOrganizationId; + + useEffect(() => { + if (useElectricCloud) { + setElectricUrl(env.NEXT_PUBLIC_ELECTRIC_URL); + } + }, [useElectricCloud]); const [isSwitching, setIsSwitching] = useState(false); const switchOrganization = useCallback( diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts index 554f1066298..8c8ca9d90df 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts @@ -26,7 +26,12 @@ import superjson from "superjson"; import { z } from "zod"; const columnMapper = snakeCamelMapper(); -const electricUrl = `${env.NEXT_PUBLIC_ELECTRIC_URL}/v1/shape`; + +let electricUrl = `${env.NEXT_PUBLIC_API_URL}/api/electric/v1/shape`; + +export function setElectricUrl(url: string) { + electricUrl = `${url}/v1/shape`; +} const apiKeyDisplaySchema = z.object({ id: z.string(), diff --git a/apps/electric-proxy/.dev.vars.example b/apps/electric-proxy/.dev.vars.example index ed052bd7d37..b94a97c7188 100644 --- a/apps/electric-proxy/.dev.vars.example +++ b/apps/electric-proxy/.dev.vars.example @@ -1,6 +1,4 @@ AUTH_URL=http://localhost:3141 -ELECTRIC_URL=http://localhost:3149/v1/shape -ELECTRIC_SECRET=local_electric_dev_secret ELECTRIC_CLOUD_URL=https://api.electric-sql.cloud ELECTRIC_SOURCE_ID= ELECTRIC_SOURCE_SECRET= diff --git a/apps/electric-proxy/src/electric.ts b/apps/electric-proxy/src/electric.ts index f8539e5b75f..38736ddfa28 100644 --- a/apps/electric-proxy/src/electric.ts +++ b/apps/electric-proxy/src/electric.ts @@ -31,20 +31,9 @@ export function buildUpstreamUrl( whereClause: WhereClause, env: Env, ): URL { - const useCloud = env.ELECTRIC_SOURCE_ID && env.ELECTRIC_SOURCE_SECRET; - - const upstream = useCloud - ? new URL("/v1/shape", env.ELECTRIC_CLOUD_URL) - : new URL(env.ELECTRIC_URL ?? "http://localhost:3149/v1/shape"); - - if (useCloud) { - // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud - upstream.searchParams.set("source_id", env.ELECTRIC_SOURCE_ID!); - // biome-ignore lint/style/noNonNullAssertion: guarded by useCloud - upstream.searchParams.set("secret", env.ELECTRIC_SOURCE_SECRET!); - } else if (env.ELECTRIC_SECRET) { - upstream.searchParams.set("secret", env.ELECTRIC_SECRET); - } + const upstream = new URL("/v1/shape", env.ELECTRIC_CLOUD_URL); + upstream.searchParams.set("source_id", env.ELECTRIC_SOURCE_ID); + upstream.searchParams.set("secret", env.ELECTRIC_SOURCE_SECRET); for (const [key, value] of clientUrl.searchParams) { if (PROTOCOL_PARAMS.has(key)) { diff --git a/apps/electric-proxy/src/types.ts b/apps/electric-proxy/src/types.ts index 4c67ef36386..10d98bb900e 100644 --- a/apps/electric-proxy/src/types.ts +++ b/apps/electric-proxy/src/types.ts @@ -1,8 +1,6 @@ export interface Env { AUTH_URL: string; - ELECTRIC_URL?: string; - ELECTRIC_SECRET?: string; - ELECTRIC_CLOUD_URL?: string; - ELECTRIC_SOURCE_ID?: string; - ELECTRIC_SOURCE_SECRET?: string; + ELECTRIC_CLOUD_URL: string; + ELECTRIC_SOURCE_ID: string; + ELECTRIC_SOURCE_SECRET: string; } diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 4012eafaf6c..03002a53ced 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -59,4 +59,5 @@ export const FEATURE_FLAGS = { SLACK_INTEGRATION_ACCESS: "slack-integration-access", /** Gates access to Cloud features (environment variables, sandboxes). */ CLOUD_ACCESS: "cloud-access", + ELECTRIC_CLOUD: "electric-cloud", } as const; From 31f711d1bd09a697e67c83dcb53f2af9c635b751 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 2 Mar 2026 17:57:37 -0800 Subject: [PATCH 7/7] fix: mock posthog-js/react in CollectionsProvider test --- .../CollectionsProvider/CollectionsProvider.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.test.tsx b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.test.tsx index 83bceeed9f1..dc3e721469f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.test.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.test.tsx @@ -2,9 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; const preloadCollectionsMock = mock(() => Promise.resolve()); +mock.module("posthog-js/react", () => ({ + useFeatureFlagEnabled: mock(() => false), +})); + mock.module("./collections", () => ({ getCollections: mock(() => ({})), preloadCollections: preloadCollectionsMock, + setElectricUrl: mock(), })); const { preloadActiveOrganizationCollections } = await import(