@@ -737,7 +743,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
{summary.price && summary.price.amount !== null
- ? `${formatMoneyMinor(summary.price.amount, summary.price.currency)} ${formatRecurringInterval(summary.price.recurringInterval, summary.price.recurringIntervalCount)}`
+ ? `${formatDenMoneyMinor(summary.price.amount, summary.price.currency)} ${formatDenRecurringInterval(summary.price.recurringInterval, summary.price.recurringIntervalCount)}`
: "Current plan amount is unavailable."}
@@ -752,14 +758,14 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
const subscription = subscriptionAccessor();
return (
<>
-
{formatSubscriptionStatus(subscription.status)}
+
{formatDenSubscriptionStatus(subscription.status)}
- {formatMoneyMinor(subscription.amount, subscription.currency)} {formatRecurringInterval(subscription.recurringInterval, subscription.recurringIntervalCount)}
+ {formatDenMoneyMinor(subscription.amount, subscription.currency)} {formatDenRecurringInterval(subscription.recurringInterval, subscription.recurringIntervalCount)}
{subscription.cancelAtPeriodEnd
- ? `Cancels on ${formatIsoDate(subscription.currentPeriodEnd)}`
- : `Renews on ${formatIsoDate(subscription.currentPeriodEnd)}`}
+ ? `Cancels on ${formatDenIsoDate(subscription.currentPeriodEnd)}`
+ : `Renews on ${formatDenIsoDate(subscription.currentPeriodEnd)}`}
>
);
@@ -814,9 +820,9 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
{(invoice) => (
-
{invoice.invoiceNumber ?? formatSubscriptionStatus(invoice.status)}
+
{invoice.invoiceNumber ?? formatDenSubscriptionStatus(invoice.status)}
- {formatIsoDate(invoice.createdAt)} · {formatMoneyMinor(invoice.totalAmount, invoice.currency)} · {formatSubscriptionStatus(invoice.status)}
+ {formatDenIsoDate(invoice.createdAt)} · {formatDenMoneyMinor(invoice.totalAmount, invoice.currency)} · {formatDenSubscriptionStatus(invoice.status)}
{(worker) => {
- const status = createMemo(() => workerStatusMeta(worker.status));
+ const status = createMemo(() => denWorkerStatusMeta(worker.status));
return (
{worker.workerName}
-
+
{status().label}
diff --git a/apps/app/src/app/features/den/browser-auth.ts b/apps/app/src/app/features/den/browser-auth.ts
new file mode 100644
index 000000000..1cf0ca422
--- /dev/null
+++ b/apps/app/src/app/features/den/browser-auth.ts
@@ -0,0 +1,58 @@
+import { normalizeDenBaseUrl, resolveDenBaseUrls } from "../../lib/den";
+
+type DenAuthMode = "sign-in" | "sign-up";
+
+function appHostedCloudBaseUrl(): URL | null {
+ if (typeof window === "undefined") {
+ return null;
+ }
+
+ try {
+ const current = new URL(window.location.href);
+ if (current.protocol !== "http:" && current.protocol !== "https:") {
+ return null;
+ }
+ return new URL("/cloud", current.origin);
+ } catch {
+ return null;
+ }
+}
+
+export function buildDenBrowserAuthUrl(input: {
+ baseUrl: string;
+ mode: DenAuthMode;
+ desktopAuth?: boolean;
+ desktopScheme?: string | null;
+}): string {
+ const hosted = appHostedCloudBaseUrl();
+ const normalizedBaseUrl = normalizeDenBaseUrl(input.baseUrl);
+
+ if (hosted) {
+ hosted.searchParams.set("mode", input.mode);
+ if (input.desktopAuth) {
+ hosted.searchParams.set("desktopAuth", "1");
+ hosted.searchParams.set("desktopScheme", input.desktopScheme?.trim() || "openwork");
+ }
+ if (normalizedBaseUrl) {
+ hosted.searchParams.set("denBaseUrl", normalizedBaseUrl);
+ }
+ return hosted.toString();
+ }
+
+ const fallback = new URL(resolveDenBaseUrls(input.baseUrl).baseUrl);
+ fallback.searchParams.set("mode", input.mode);
+ if (input.desktopAuth) {
+ fallback.searchParams.set("desktopAuth", "1");
+ fallback.searchParams.set("desktopScheme", input.desktopScheme?.trim() || "openwork");
+ }
+ return fallback.toString();
+}
+
+export function buildDenSocialCallbackUrl(mode: DenAuthMode): string | null {
+ const hosted = appHostedCloudBaseUrl();
+ if (!hosted) {
+ return null;
+ }
+ hosted.searchParams.set("mode", mode);
+ return hosted.toString();
+}
diff --git a/apps/app/src/app/features/den/formatters.ts b/apps/app/src/app/features/den/formatters.ts
new file mode 100644
index 000000000..4e7b4638a
--- /dev/null
+++ b/apps/app/src/app/features/den/formatters.ts
@@ -0,0 +1,111 @@
+export function denStatusBadgeClass(
+ kind: "ready" | "warning" | "neutral" | "error",
+) {
+ switch (kind) {
+ case "ready":
+ return "border-green-7/30 bg-green-3/20 text-green-11";
+ case "warning":
+ return "border-amber-7/30 bg-amber-3/20 text-amber-11";
+ case "error":
+ return "border-red-7/30 bg-red-3/20 text-red-11";
+ default:
+ return "border-gray-6/60 bg-gray-3/20 text-gray-11";
+ }
+}
+
+export function denWorkerStatusMeta(status: string) {
+ const normalized = status.trim().toLowerCase();
+ switch (normalized) {
+ case "healthy":
+ case "ready":
+ return { label: "Ready", tone: "ready" as const, canOpen: true };
+ case "provisioning":
+ case "starting":
+ return {
+ label: "Provisioning",
+ tone: "warning" as const,
+ canOpen: false,
+ };
+ case "failed":
+ return { label: "Failed", tone: "error" as const, canOpen: false };
+ case "stopped":
+ case "suspended":
+ return { label: "Stopped", tone: "neutral" as const, canOpen: false };
+ default:
+ return {
+ label: normalized
+ ? `${normalized.slice(0, 1).toUpperCase()}${normalized.slice(1)}`
+ : "Unknown",
+ tone: "neutral" as const,
+ canOpen: normalized === "ready",
+ };
+ }
+}
+
+export function formatDenMoneyMinor(
+ amount: number | null,
+ currency: string | null,
+): string {
+ if (typeof amount !== "number" || !Number.isFinite(amount)) {
+ return "Not available";
+ }
+
+ const normalizedCurrency = (currency ?? "USD").toUpperCase();
+ const majorValue = amount / 100;
+
+ try {
+ return new Intl.NumberFormat(undefined, {
+ style: "currency",
+ currency: normalizedCurrency,
+ }).format(majorValue);
+ } catch {
+ return `${majorValue.toFixed(2)} ${normalizedCurrency}`;
+ }
+}
+
+export function formatDenIsoDate(value: string | null): string {
+ if (!value) {
+ return "Not available";
+ }
+
+ try {
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ return "Not available";
+ }
+ return date.toLocaleDateString();
+ } catch {
+ return "Not available";
+ }
+}
+
+export function formatDenRecurringInterval(
+ interval: string | null,
+ count: number | null,
+): string {
+ if (!interval) {
+ return "billing cycle";
+ }
+
+ const normalizedInterval = interval.replace(/_/g, " ");
+ const normalizedCount =
+ typeof count === "number" && Number.isFinite(count) ? count : 1;
+ if (normalizedCount <= 1) {
+ return `per ${normalizedInterval}`;
+ }
+
+ const pluralSuffix = normalizedInterval.endsWith("s") ? "" : "s";
+ return `every ${normalizedCount} ${normalizedInterval}${pluralSuffix}`;
+}
+
+export function formatDenSubscriptionStatus(status: string): string {
+ const normalized = status.trim().toLowerCase();
+ if (!normalized) {
+ return "Unknown";
+ }
+
+ return normalized
+ .split("_")
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
+ .join(" ");
+}
diff --git a/apps/app/src/app/features/den/state.ts b/apps/app/src/app/features/den/state.ts
new file mode 100644
index 000000000..b4fb5a9a3
--- /dev/null
+++ b/apps/app/src/app/features/den/state.ts
@@ -0,0 +1,1288 @@
+import {
+ createEffect,
+ createMemo,
+ createSignal,
+ onCleanup,
+ type Accessor,
+} from "solid-js";
+import {
+ clearDenSession,
+ createDenClient,
+ type DenAdminOverview,
+ DenApiError,
+ type DenBillingSummary,
+ type DenSocialProvider,
+ type DenUser,
+ type DenWorkerLaunch,
+ type DenWorkerRuntimeSnapshot,
+ type DenWorkerSummary,
+ normalizeDenBaseUrl,
+ readDenSettings,
+ resolveDenBaseUrls,
+ writeDenSettings,
+} from "../../lib/den";
+import {
+ canConfigureDenBaseUrlOverride,
+ DEN_CONFIG_UPDATED_EVENT,
+ dispatchDenConfigUpdated,
+ readDenFeatureGate,
+} from "../../lib/den-gate";
+import { isDesktopDeployment } from "../../lib/openwork-deployment";
+import {
+ buildDenBrowserAuthUrl,
+ buildDenSocialCallbackUrl,
+} from "./browser-auth";
+
+type DenAuthMode = "sign-in" | "sign-up";
+
+type DenFeatureStateOptions = {
+ developerMode: Accessor;
+ openLink: (url: string) => void;
+ connectRemoteWorkspace?: (input: {
+ openworkHostUrl?: string | null;
+ openworkToken?: string | null;
+ directory?: string | null;
+ displayName?: string | null;
+ }) => Promise;
+};
+
+const DEFAULT_WORKER_NAME = "My Worker";
+const ONBOARDING_INTENT_STORAGE_KEY = "openwork:web:onboarding-intent";
+
+type OnboardingIntent = {
+ version: 1;
+ workerName: string;
+ shouldLaunch: boolean;
+ completed: boolean;
+ authMethod: "email" | "github" | "google";
+};
+
+function deriveOnboardingWorkerName(user: DenUser): string {
+ const rawIdentity = (user.name?.trim() || user.email.split("@")[0] || DEFAULT_WORKER_NAME)
+ .replace(/[._-]+/g, " ")
+ .trim();
+ const base = rawIdentity
+ .split(/\s+/)
+ .filter(Boolean)
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
+ .join(" ");
+
+ const owner = base || DEFAULT_WORKER_NAME;
+ const suffix = owner.endsWith("s") ? "' Worker" : "'s Worker";
+ return `${owner}${suffix}`;
+}
+
+function readOnboardingIntent(): OnboardingIntent | null {
+ if (typeof window === "undefined") return null;
+ try {
+ const raw = window.localStorage.getItem(ONBOARDING_INTENT_STORAGE_KEY);
+ if (!raw) return null;
+ const parsed = JSON.parse(raw) as Partial | null;
+ if (!parsed || parsed.version !== 1 || typeof parsed.workerName !== "string") {
+ return null;
+ }
+ return {
+ version: 1,
+ workerName: parsed.workerName,
+ shouldLaunch: parsed.shouldLaunch === true,
+ completed: parsed.completed === true,
+ authMethod:
+ parsed.authMethod === "github" || parsed.authMethod === "google"
+ ? parsed.authMethod
+ : "email",
+ };
+ } catch {
+ return null;
+ }
+}
+
+function writeOnboardingIntent(next: OnboardingIntent | null) {
+ if (typeof window === "undefined") return;
+ if (!next) {
+ window.localStorage.removeItem(ONBOARDING_INTENT_STORAGE_KEY);
+ return;
+ }
+ window.localStorage.setItem(ONBOARDING_INTENT_STORAGE_KEY, JSON.stringify(next));
+}
+
+function workerSummaryToLaunch(
+ worker: DenWorkerSummary,
+ current?: DenWorkerLaunch | null,
+): DenWorkerLaunch {
+ return {
+ workerId: worker.workerId,
+ workerName: worker.workerName,
+ status: worker.status,
+ provider: worker.provider,
+ instanceUrl: worker.instanceUrl,
+ openworkUrl:
+ current?.workerId === worker.workerId
+ ? current.openworkUrl
+ : worker.instanceUrl,
+ workspaceId: current?.workerId === worker.workerId ? current.workspaceId : null,
+ clientToken: current?.workerId === worker.workerId ? current.clientToken : null,
+ ownerToken: current?.workerId === worker.workerId ? current.ownerToken : null,
+ hostToken: current?.workerId === worker.workerId ? current.hostToken : null,
+ };
+}
+
+export function createDenFeatureState(options: DenFeatureStateOptions) {
+ const initialGate = readDenFeatureGate(options.developerMode());
+ const initialSettings = readDenSettings();
+ const initialBaseUrl = initialGate.baseUrl ?? "";
+
+ const [authMode, setAuthMode] = createSignal("sign-in");
+ const [email, setEmail] = createSignal("");
+ const [password, setPassword] = createSignal("");
+ const [workerName, setWorkerName] = createSignal(DEFAULT_WORKER_NAME);
+ const [baseUrl, setBaseUrl] = createSignal(initialBaseUrl);
+ const [baseUrlDraft, setBaseUrlDraft] = createSignal(initialBaseUrl);
+ const [baseUrlError, setBaseUrlError] = createSignal(null);
+ const [authToken, setAuthToken] = createSignal(
+ initialGate.enabled ? initialSettings.authToken?.trim() || "" : "",
+ );
+ const [activeOrgId, setActiveOrgId] = createSignal(
+ initialGate.enabled ? initialSettings.activeOrgId?.trim() || "" : "",
+ );
+ const [selectedWorkerId, setSelectedWorkerId] = createSignal(null);
+ const [selectedWorkerLaunch, setSelectedWorkerLaunch] =
+ createSignal(null);
+ const [onboardingIntent, setOnboardingIntent] =
+ createSignal(readOnboardingIntent());
+ const [desktopAuthRequested, setDesktopAuthRequested] = createSignal(false);
+ const [desktopAuthScheme, setDesktopAuthScheme] = createSignal("openwork");
+ const [desktopRedirectBusy, setDesktopRedirectBusy] = createSignal(false);
+ const [desktopRedirectUrl, setDesktopRedirectUrl] = createSignal(null);
+
+ const [authBusy, setAuthBusy] = createSignal(false);
+ const [sessionBusy, setSessionBusy] = createSignal(false);
+ const [orgsBusy, setOrgsBusy] = createSignal(false);
+ const [workersBusy, setWorkersBusy] = createSignal(false);
+ const [billingBusy, setBillingBusy] = createSignal(false);
+ const [billingCheckoutBusy, setBillingCheckoutBusy] = createSignal(false);
+ const [billingSubscriptionBusy, setBillingSubscriptionBusy] =
+ createSignal(false);
+ const [workerActionBusy, setWorkerActionBusy] = createSignal(false);
+ const [runtimeBusy, setRuntimeBusy] = createSignal(false);
+ const [adminBusy, setAdminBusy] = createSignal(false);
+ const [openingWorkerId, setOpeningWorkerId] = createSignal(null);
+
+ const [user, setUser] = createSignal(null);
+ const [orgs, setOrgs] = createSignal<
+ Array<{ id: string; name: string; slug: string; role: "owner" | "member" }>
+ >([]);
+ const [workers, setWorkers] = createSignal([]);
+ const [runtimeSnapshot, setRuntimeSnapshot] =
+ createSignal(null);
+ const [billingSummary, setBillingSummary] =
+ createSignal(null);
+ const [adminOverview, setAdminOverview] =
+ createSignal(null);
+
+ const [statusMessage, setStatusMessage] = createSignal(null);
+ const [authError, setAuthError] = createSignal(null);
+ const [orgsError, setOrgsError] = createSignal(null);
+ const [workersError, setWorkersError] = createSignal(null);
+ const [runtimeError, setRuntimeError] = createSignal(null);
+ const [billingError, setBillingError] = createSignal(null);
+ const [adminError, setAdminError] = createSignal(null);
+
+ const canEditBaseUrl = createMemo(() =>
+ canConfigureDenBaseUrlOverride(options.developerMode()),
+ );
+ const isConfigured = createMemo(() => Boolean(baseUrl().trim()));
+ const client = createMemo(() =>
+ isConfigured()
+ ? createDenClient({ baseUrl: baseUrl(), token: authToken() })
+ : null,
+ );
+ const activeOrg = createMemo(
+ () => orgs().find((org) => org.id === activeOrgId()) ?? null,
+ );
+ const isSignedIn = createMemo(
+ () => isConfigured() && Boolean(user() && authToken().trim()),
+ );
+ const billingSubscription = createMemo(
+ () => billingSummary()?.subscription ?? null,
+ );
+ const billingCheckoutUrl = createMemo(
+ () => billingSummary()?.checkoutUrl ?? null,
+ );
+ const selectedWorkerSummary = createMemo(
+ () => workers().find((worker) => worker.workerId === selectedWorkerId()) ?? null,
+ );
+ const selectedWorker = createMemo(() => {
+ const summary = selectedWorkerSummary();
+ const current = selectedWorkerLaunch();
+ if (summary) {
+ return workerSummaryToLaunch(summary, current);
+ }
+ return current && current.workerId === selectedWorkerId() ? current : null;
+ });
+ const selectedWorkerStatus = createMemo(
+ () => selectedWorker()?.status ?? selectedWorkerSummary()?.status ?? "",
+ );
+ const onboardingPending = createMemo(
+ () => Boolean(onboardingIntent()?.shouldLaunch && !onboardingIntent()?.completed),
+ );
+
+ const clearRuntimeState = () => {
+ setRuntimeSnapshot(null);
+ setRuntimeError(null);
+ setRuntimeBusy(false);
+ };
+
+ const clearSessionState = () => {
+ setUser(null);
+ setOrgs([]);
+ setWorkers([]);
+ setSelectedWorkerId(null);
+ setSelectedWorkerLaunch(null);
+ setBillingSummary(null);
+ setAdminOverview(null);
+ setActiveOrgId("");
+ setOrgsError(null);
+ setWorkersError(null);
+ setBillingError(null);
+ setAdminError(null);
+ setDesktopRedirectUrl(null);
+ setDesktopRedirectBusy(false);
+ clearRuntimeState();
+ };
+
+ const persistOnboardingIntent = (next: OnboardingIntent | null) => {
+ setOnboardingIntent(next);
+ writeOnboardingIntent(next);
+ };
+
+ const markOnboardingComplete = () => {
+ const current = onboardingIntent();
+ if (!current) return;
+ persistOnboardingIntent({
+ ...current,
+ shouldLaunch: false,
+ completed: true,
+ });
+ };
+
+ const clearSignedInState = (message?: string | null) => {
+ clearDenSession();
+ setAuthToken("");
+ clearSessionState();
+ setAuthError(null);
+ setStatusMessage(message ?? null);
+ };
+
+ const syncGateState = () => {
+ const nextGate = readDenFeatureGate(options.developerMode());
+ const nextSettings = readDenSettings();
+ const nextBaseUrl = nextGate.baseUrl ?? "";
+ setBaseUrl(nextBaseUrl);
+ setBaseUrlDraft(nextBaseUrl);
+ setBaseUrlError(null);
+ if (!nextGate.enabled) {
+ setAuthToken("");
+ setActiveOrgId("");
+ clearSessionState();
+ return;
+ }
+ setAuthToken(nextSettings.authToken?.trim() || "");
+ setActiveOrgId(nextSettings.activeOrgId?.trim() || "");
+ };
+
+ createEffect(() => {
+ const nextBaseUrl = baseUrl().trim();
+ if (!nextBaseUrl) {
+ clearDenSession({ includeBaseUrls: true });
+ return;
+ }
+
+ const nextSettings = readDenSettings();
+ writeDenSettings({
+ baseUrl: nextBaseUrl,
+ authToken: authToken() || null,
+ activeOrgId: activeOrgId() || null,
+ apiBaseUrl: nextSettings.apiBaseUrl,
+ });
+ });
+
+ createEffect(() => {
+ options.developerMode();
+ if (!canEditBaseUrl()) {
+ syncGateState();
+ }
+ });
+
+ if (typeof window !== "undefined") {
+ try {
+ const params = new URLSearchParams(window.location.search);
+ const requestedMode = params.get("mode")?.trim().toLowerCase();
+ if (requestedMode === "sign-up" || requestedMode === "sign-in") {
+ setAuthMode(requestedMode);
+ }
+
+ if (params.get("desktopAuth") === "1") {
+ setDesktopAuthRequested(true);
+ }
+
+ const requestedScheme = params.get("desktopScheme")?.trim() ?? "";
+ if (/^[a-z][a-z0-9+.-]*$/i.test(requestedScheme)) {
+ setDesktopAuthScheme(requestedScheme);
+ }
+
+ if (!readDenFeatureGate(options.developerMode()).enabled) {
+ const queryBaseUrl = normalizeDenBaseUrl(params.get("denBaseUrl")?.trim() ?? "");
+ if (queryBaseUrl) {
+ setBaseUrl(queryBaseUrl);
+ setBaseUrlDraft(queryBaseUrl);
+ }
+ }
+ } catch {
+ // ignore search param parsing failures
+ }
+ }
+
+ createEffect(() => {
+ const currentBaseUrl = baseUrl().trim();
+ const token = authToken().trim();
+ let cancelled = false;
+
+ if (!currentBaseUrl) {
+ setSessionBusy(false);
+ clearSessionState();
+ setAuthError(null);
+ return;
+ }
+
+ if (!token) {
+ setSessionBusy(false);
+ clearSessionState();
+ setAuthError(null);
+ return;
+ }
+
+ setSessionBusy(true);
+ setAuthError(null);
+
+ void createDenClient({ baseUrl: currentBaseUrl, token })
+ .getSession()
+ .then((nextUser) => {
+ if (cancelled) return;
+ setUser(nextUser);
+ })
+ .catch((error) => {
+ if (cancelled) return;
+ if (error instanceof DenApiError && error.status === 401) {
+ clearSignedInState();
+ } else {
+ clearSessionState();
+ }
+ setAuthError(
+ error instanceof Error ? error.message : "No active Cloud session found.",
+ );
+ })
+ .finally(() => {
+ if (!cancelled) {
+ setSessionBusy(false);
+ }
+ });
+
+ onCleanup(() => {
+ cancelled = true;
+ });
+ });
+
+ createEffect(() => {
+ if (!user()) return;
+ void refreshOrgs(true);
+ });
+
+ createEffect(() => {
+ if (!user() || !activeOrgId().trim()) return;
+ void refreshWorkers(true);
+ });
+
+ createEffect(() => {
+ if (!user()) return;
+ void refreshBilling({ quiet: true });
+ });
+
+ createEffect(() => {
+ const currentUser = user();
+ if (!currentUser) return;
+ if (onboardingPending()) return;
+ if (workerName().trim() && workerName().trim() !== DEFAULT_WORKER_NAME) return;
+ setWorkerName(deriveOnboardingWorkerName(currentUser));
+ });
+
+ createEffect(() => {
+ if (!user() || !onboardingPending()) {
+ return;
+ }
+
+ const summary = billingSummary();
+ if (!summary) return;
+ if (summary.featureGateEnabled && !summary.hasActivePlan) return;
+ if (workers().length > 0) {
+ markOnboardingComplete();
+ return;
+ }
+ if (workerActionBusy()) return;
+
+ void launchWorker({
+ workerNameOverride: onboardingIntent()?.workerName ?? DEFAULT_WORKER_NAME,
+ source: "signup_auto",
+ });
+ });
+
+ createEffect(() => {
+ const worker = selectedWorker();
+ if (!worker) {
+ clearRuntimeState();
+ return;
+ }
+
+ if (!["provisioning", "starting"].includes(worker.status.trim().toLowerCase())) {
+ return;
+ }
+
+ const interval = window.setInterval(() => {
+ void refreshSelectedWorker(true);
+ }, 5_000);
+
+ onCleanup(() => window.clearInterval(interval));
+ });
+
+ if (typeof window !== "undefined") {
+ const handleSessionUpdated = (event: Event) => {
+ const customEvent = event as CustomEvent<{
+ status?: string;
+ email?: string | null;
+ message?: string | null;
+ }>;
+ syncGateState();
+ if (customEvent.detail?.status === "success") {
+ setAuthError(null);
+ setStatusMessage(
+ customEvent.detail.email?.trim()
+ ? `Connected OpenWork Den as ${customEvent.detail.email.trim()}.`
+ : "Connected OpenWork Den.",
+ );
+ } else if (customEvent.detail?.status === "error") {
+ setAuthError(
+ customEvent.detail.message?.trim() ||
+ "Failed to finish OpenWork Den sign-in.",
+ );
+ }
+ };
+
+ const handleConfigUpdated = () => {
+ syncGateState();
+ };
+
+ window.addEventListener(
+ "openwork-den-session-updated",
+ handleSessionUpdated as EventListener,
+ );
+ window.addEventListener(DEN_CONFIG_UPDATED_EVENT, handleConfigUpdated);
+ onCleanup(() => {
+ window.removeEventListener(
+ "openwork-den-session-updated",
+ handleSessionUpdated as EventListener,
+ );
+ window.removeEventListener(DEN_CONFIG_UPDATED_EVENT, handleConfigUpdated);
+ });
+ }
+
+ createEffect(() => {
+ if (!desktopAuthRequested() || !isSignedIn() || desktopRedirectUrl() || desktopRedirectBusy()) {
+ return;
+ }
+ void createDesktopRedirect();
+ });
+
+ async function refreshOrgs(quiet = false) {
+ const activeClient = client();
+ if (!activeClient || !authToken().trim()) {
+ setOrgs([]);
+ setActiveOrgId("");
+ return;
+ }
+
+ setOrgsBusy(true);
+ if (!quiet) setOrgsError(null);
+
+ try {
+ const response = await activeClient.listOrgs();
+ setOrgs(response.orgs);
+ const current = activeOrgId().trim();
+ const fallback = response.defaultOrgId ?? response.orgs[0]?.id ?? "";
+ const next = response.orgs.some((org) => org.id === current)
+ ? current
+ : fallback;
+ setActiveOrgId(next);
+ if (!quiet && response.orgs.length > 0) {
+ setStatusMessage(
+ `Loaded ${response.orgs.length} org${response.orgs.length === 1 ? "" : "s"}.`,
+ );
+ }
+ } catch (error) {
+ setOrgsError(error instanceof Error ? error.message : "Failed to load orgs.");
+ } finally {
+ setOrgsBusy(false);
+ }
+ }
+
+ async function refreshWorkers(quiet = false) {
+ const activeClient = client();
+ const orgId = activeOrgId().trim();
+ if (!activeClient || !authToken().trim() || !orgId) {
+ setWorkers([]);
+ setSelectedWorkerId(null);
+ setSelectedWorkerLaunch(null);
+ clearRuntimeState();
+ return;
+ }
+
+ setWorkersBusy(true);
+ if (!quiet) setWorkersError(null);
+
+ try {
+ const nextWorkers = await activeClient.listWorkers(orgId, 20);
+ setWorkers(nextWorkers);
+ const currentId = selectedWorkerId();
+ const fallbackId = nextWorkers[0]?.workerId ?? null;
+ const nextSelectedId =
+ currentId && nextWorkers.some((worker) => worker.workerId === currentId)
+ ? currentId
+ : fallbackId;
+ setSelectedWorkerId(nextSelectedId);
+ if (!nextSelectedId) {
+ setSelectedWorkerLaunch(null);
+ clearRuntimeState();
+ }
+ if (!quiet) {
+ setStatusMessage(
+ nextWorkers.length > 0
+ ? `Loaded ${nextWorkers.length} worker${nextWorkers.length === 1 ? "" : "s"}.`
+ : `No workers found for ${activeOrg()?.name ?? "this org"}.`,
+ );
+ }
+ } catch (error) {
+ setWorkersError(
+ error instanceof Error ? error.message : "Failed to load workers.",
+ );
+ } finally {
+ setWorkersBusy(false);
+ }
+ }
+
+ async function refreshBilling(options: {
+ quiet?: boolean;
+ includeCheckout?: boolean;
+ } = {}) {
+ const activeClient = client();
+ if (!activeClient || !authToken().trim()) {
+ setBillingSummary(null);
+ return null;
+ }
+
+ const quiet = options.quiet === true;
+ if (options.includeCheckout) {
+ setBillingCheckoutBusy(true);
+ } else {
+ setBillingBusy(true);
+ }
+ if (!quiet) setBillingError(null);
+
+ try {
+ const summary = await activeClient.getBillingStatus({
+ includeCheckout: options.includeCheckout,
+ });
+ setBillingSummary(summary);
+ return summary;
+ } catch (error) {
+ if (!quiet) {
+ setBillingError(
+ error instanceof Error ? error.message : "Failed to load billing.",
+ );
+ }
+ return null;
+ } finally {
+ if (options.includeCheckout) {
+ setBillingCheckoutBusy(false);
+ } else {
+ setBillingBusy(false);
+ }
+ }
+ }
+
+ function resolveLandingRoute() {
+ const summary = billingSummary();
+ if (
+ summary &&
+ summary.featureGateEnabled &&
+ summary.checkoutRequired &&
+ !summary.hasActivePlan
+ ) {
+ return "/cloud/checkout" as const;
+ }
+ return "/cloud/dashboard" as const;
+ }
+
+ async function handleCheckoutReturn(customerSessionToken: string | null) {
+ if (customerSessionToken) {
+ setStatusMessage(
+ "Checkout return detected. Billing is refreshing now.",
+ );
+ }
+ await refreshBilling({ quiet: true });
+ return resolveLandingRoute();
+ }
+
+ async function submitEmailAuth() {
+ const activeClient = client();
+ if (!activeClient) {
+ setBaseUrlError(
+ "Set a Den control plane URL before attempting Cloud auth.",
+ );
+ return null;
+ }
+
+ const nextEmail = email().trim();
+ if (!nextEmail || !password()) {
+ setAuthError("Enter your email and password.");
+ return null;
+ }
+
+ setAuthBusy(true);
+ setAuthError(null);
+
+ try {
+ const result =
+ authMode() === "sign-up"
+ ? await activeClient.signUpEmail(nextEmail, password())
+ : await activeClient.signInEmail(nextEmail, password());
+
+ if (!result.token || !result.user) {
+ throw new Error(
+ authMode() === "sign-up"
+ ? "Cloud sign-up completed, but the response was missing session details."
+ : "Cloud sign-in completed, but the response was missing session details.",
+ );
+ }
+
+ setAuthToken(result.token);
+ setUser(result.user);
+ setPassword("");
+ if (authMode() === "sign-up") {
+ const autoName = deriveOnboardingWorkerName(result.user);
+ setWorkerName(autoName);
+ persistOnboardingIntent({
+ version: 1,
+ workerName: autoName,
+ shouldLaunch: true,
+ completed: false,
+ authMethod: "email",
+ });
+ }
+ setStatusMessage(
+ authMode() === "sign-up"
+ ? `Created Cloud account for ${result.user.email}.`
+ : `Signed in as ${result.user.email}.`,
+ );
+ await refreshOrgs(true);
+ await refreshBilling({ includeCheckout: authMode() === "sign-up", quiet: true });
+ return resolveLandingRoute();
+ } catch (error) {
+ setAuthError(
+ error instanceof Error ? error.message : "Failed to complete Cloud auth.",
+ );
+ return null;
+ } finally {
+ setAuthBusy(false);
+ }
+ }
+
+ async function beginSocialAuth(provider: DenSocialProvider) {
+ if (!isConfigured()) {
+ setBaseUrlError("Set a Den control plane URL before starting Cloud auth.");
+ return false;
+ }
+
+ if (isDesktopDeployment()) {
+ openBrowserAuth(authMode());
+ return true;
+ }
+
+ const activeClient = client();
+ if (!activeClient || typeof window === "undefined") {
+ setAuthError("Cloud social auth is unavailable in this environment.");
+ return false;
+ }
+
+ try {
+ const callbackUrl = buildDenSocialCallbackUrl(authMode());
+ if (!callbackUrl) {
+ openBrowserAuth(authMode());
+ return true;
+ }
+ const result = await activeClient.beginSocialAuth({
+ provider,
+ callbackURL: callbackUrl,
+ errorCallbackURL: callbackUrl,
+ });
+ window.location.assign(result.url);
+ return true;
+ } catch (error) {
+ setAuthError(
+ error instanceof Error ? error.message : "Failed to start social auth.",
+ );
+ return false;
+ }
+ }
+
+ function openControlPlane() {
+ if (!isConfigured()) {
+ setBaseUrlError(
+ "Set a Den control plane URL before opening Cloud in your browser.",
+ );
+ return;
+ }
+ options.openLink(resolveDenBaseUrls(baseUrl()).baseUrl);
+ }
+
+ function openBrowserAuth(mode: DenAuthMode) {
+ if (!isConfigured()) {
+ setBaseUrlError(
+ "Set a Den control plane URL before starting Cloud auth.",
+ );
+ return;
+ }
+ const target = buildDenBrowserAuthUrl({
+ baseUrl: baseUrl(),
+ mode,
+ desktopAuth: isDesktopDeployment(),
+ desktopScheme: "openwork",
+ });
+ options.openLink(target);
+ setStatusMessage(
+ mode === "sign-up"
+ ? "Finish account creation in your browser to connect OpenWork."
+ : "Finish signing in in your browser to connect OpenWork.",
+ );
+ setAuthError(null);
+ }
+
+ function applyBaseUrl() {
+ if (!canEditBaseUrl()) return;
+ const normalized = normalizeDenBaseUrl(baseUrlDraft());
+ if (!normalized) {
+ setBaseUrlError(
+ "Enter a valid http:// or https:// Den control plane URL.",
+ );
+ return;
+ }
+
+ const resolved = resolveDenBaseUrls(normalized);
+ setBaseUrlError(null);
+ setBaseUrl(resolved.baseUrl);
+ setBaseUrlDraft(resolved.baseUrl);
+ clearSignedInState("Updated the Den control plane URL. Sign in again to continue.");
+ dispatchDenConfigUpdated({
+ source: "override",
+ baseUrl: resolved.baseUrl,
+ enabled: true,
+ });
+ }
+
+ function disableCloud() {
+ if (!canEditBaseUrl()) return;
+ setBaseUrl("");
+ setBaseUrlDraft("");
+ setAuthToken("");
+ clearSessionState();
+ setBaseUrlError(null);
+ setAuthError(null);
+ setStatusMessage("Cloud features disabled on this device.");
+ dispatchDenConfigUpdated({
+ source: "none",
+ baseUrl: null,
+ enabled: false,
+ });
+ }
+
+ async function signOut() {
+ const activeClient = client();
+ if (authBusy()) return;
+ setAuthBusy(true);
+ try {
+ if (activeClient && authToken().trim()) {
+ await activeClient.signOut();
+ }
+ } catch {
+ // ignore remote sign-out failures
+ } finally {
+ setAuthBusy(false);
+ }
+ clearSignedInState(
+ "Signed out and cleared your OpenWork Den session on this device.",
+ );
+ }
+
+ async function createDesktopRedirect() {
+ const activeClient = client();
+ if (!activeClient || !desktopAuthRequested() || !isSignedIn()) {
+ return null;
+ }
+
+ setDesktopRedirectBusy(true);
+ try {
+ const result = await activeClient.createDesktopHandoffGrant({
+ desktopScheme: desktopAuthScheme(),
+ });
+ if (!result.openworkUrl) {
+ throw new Error(
+ "Cloud auth completed, but OpenWork did not receive a desktop handoff link.",
+ );
+ }
+ setDesktopRedirectUrl(result.openworkUrl);
+ setStatusMessage("Desktop handoff is ready. Open OpenWork to finish sign-in.");
+ return result.openworkUrl;
+ } catch (error) {
+ setAuthError(
+ error instanceof Error
+ ? error.message
+ : "Failed to prepare desktop sign-in handoff.",
+ );
+ return null;
+ } finally {
+ setDesktopRedirectBusy(false);
+ }
+ }
+
+ function openDesktopRedirect() {
+ const target = desktopRedirectUrl();
+ if (!target) return;
+ options.openLink(target);
+ }
+
+ async function launchWorker(options: {
+ workerNameOverride?: string | null;
+ source?: "manual" | "signup_auto";
+ } = {}) {
+ const activeClient = client();
+ if (!activeClient || !isSignedIn()) {
+ setWorkersError("Sign in before launching a Cloud worker.");
+ return null;
+ }
+
+ const nextWorkerName =
+ options.workerNameOverride?.trim() || workerName().trim() || DEFAULT_WORKER_NAME;
+ setWorkerActionBusy(true);
+ setWorkersError(null);
+ try {
+ const result = await activeClient.createWorker({
+ name: nextWorkerName,
+ destination: "cloud",
+ });
+ if (result.kind === "paywall") {
+ setBillingSummary((current) =>
+ current
+ ? {
+ ...current,
+ hasActivePlan: false,
+ checkoutRequired: true,
+ checkoutUrl: result.checkoutUrl ?? current.checkoutUrl,
+ productId: result.productId ?? current.productId,
+ benefitId: result.benefitId ?? current.benefitId,
+ }
+ : current,
+ );
+ setStatusMessage(
+ "Payment is required before another Cloud worker can be created.",
+ );
+ return "/cloud/checkout" as const;
+ }
+
+ setSelectedWorkerId(result.worker.workerId);
+ setSelectedWorkerLaunch(result.worker);
+ markOnboardingComplete();
+ setStatusMessage(
+ result.launchMode === "async"
+ ? `Provisioning ${result.worker.workerName}...`
+ : `${result.worker.workerName} is ready.`,
+ );
+ await refreshWorkers(true);
+ return "/cloud/dashboard" as const;
+ } catch (error) {
+ setWorkersError(
+ error instanceof Error ? error.message : "Failed to launch a worker.",
+ );
+ return null;
+ } finally {
+ setWorkerActionBusy(false);
+ }
+ }
+
+ function selectWorker(workerId: string) {
+ setSelectedWorkerId(workerId);
+ setRuntimeSnapshot(null);
+ setRuntimeError(null);
+ }
+
+ async function refreshSelectedWorker(quiet = false) {
+ const activeClient = client();
+ const workerId = selectedWorkerId();
+ if (!activeClient || !workerId) return null;
+
+ if (!quiet) {
+ setWorkerActionBusy(true);
+ setWorkersError(null);
+ }
+
+ try {
+ const summary = await activeClient.getWorker(workerId);
+ setWorkers((current) => {
+ const next = current.slice();
+ const index = next.findIndex((worker) => worker.workerId === workerId);
+ if (index >= 0) {
+ next[index] = summary;
+ return next;
+ }
+ return [summary, ...next];
+ });
+ setSelectedWorkerLaunch((current) =>
+ current?.workerId === workerId ? workerSummaryToLaunch(summary, current) : current,
+ );
+ return summary;
+ } catch (error) {
+ if (!quiet) {
+ setWorkersError(
+ error instanceof Error ? error.message : "Failed to refresh worker.",
+ );
+ }
+ return null;
+ } finally {
+ if (!quiet) setWorkerActionBusy(false);
+ }
+ }
+
+ async function openWorker(workerId?: string) {
+ const activeClient = client();
+ const targetWorkerId = workerId ?? selectedWorkerId();
+ const orgId = activeOrgId().trim();
+ if (!activeClient || !targetWorkerId || !orgId) {
+ setWorkersError("Choose an org and worker before opening Cloud.");
+ return false;
+ }
+ if (!options.connectRemoteWorkspace) {
+ setWorkersError("Opening a Cloud worker is unavailable in this environment.");
+ return false;
+ }
+
+ const workerLabel =
+ workers().find((worker) => worker.workerId === targetWorkerId)?.workerName ??
+ "Cloud worker";
+ setOpeningWorkerId(targetWorkerId);
+ setWorkersError(null);
+ try {
+ const tokens = await activeClient.getWorkerTokens(targetWorkerId, orgId);
+ setSelectedWorkerLaunch((current) => {
+ if (current?.workerId === targetWorkerId) {
+ return {
+ ...current,
+ openworkUrl: tokens.openworkUrl ?? current.openworkUrl,
+ workspaceId: tokens.workspaceId ?? current.workspaceId,
+ clientToken: tokens.clientToken,
+ ownerToken: tokens.ownerToken,
+ hostToken: tokens.hostToken,
+ };
+ }
+ const summary = workers().find((worker) => worker.workerId === targetWorkerId);
+ return summary
+ ? {
+ ...workerSummaryToLaunch(summary, null),
+ openworkUrl: tokens.openworkUrl,
+ workspaceId: tokens.workspaceId,
+ clientToken: tokens.clientToken,
+ ownerToken: tokens.ownerToken,
+ hostToken: tokens.hostToken,
+ }
+ : current;
+ });
+ const openworkUrl = tokens.openworkUrl?.trim() ?? "";
+ const accessToken =
+ tokens.ownerToken?.trim() || tokens.clientToken?.trim() || "";
+ if (!openworkUrl || !accessToken) {
+ throw new Error(
+ "Worker is not ready to open yet. Try again after provisioning finishes.",
+ );
+ }
+
+ const ok = await options.connectRemoteWorkspace({
+ openworkHostUrl: openworkUrl,
+ openworkToken: accessToken,
+ directory: null,
+ displayName: workerLabel,
+ });
+ if (!ok) {
+ throw new Error(`Failed to open ${workerLabel} in OpenWork.`);
+ }
+ setStatusMessage(`Opened ${workerLabel} in OpenWork.`);
+ return true;
+ } catch (error) {
+ setWorkersError(
+ error instanceof Error ? error.message : `Failed to open ${workerLabel}.`,
+ );
+ return false;
+ } finally {
+ setOpeningWorkerId(null);
+ }
+ }
+
+ async function refreshRuntime() {
+ const activeClient = client();
+ const worker = selectedWorker();
+ if (!activeClient || !worker) {
+ setRuntimeSnapshot(null);
+ return null;
+ }
+ setRuntimeBusy(true);
+ setRuntimeError(null);
+ try {
+ const runtime = await activeClient.getWorkerRuntime(worker.workerId);
+ setRuntimeSnapshot(runtime);
+ return runtime;
+ } catch (error) {
+ setRuntimeError(
+ error instanceof Error ? error.message : "Failed to load runtime details.",
+ );
+ return null;
+ } finally {
+ setRuntimeBusy(false);
+ }
+ }
+
+ async function upgradeRuntime() {
+ const activeClient = client();
+ const worker = selectedWorker();
+ if (!activeClient || !worker) return null;
+ setRuntimeBusy(true);
+ setRuntimeError(null);
+ try {
+ const runtime = await activeClient.upgradeWorkerRuntime(worker.workerId);
+ setRuntimeSnapshot(runtime);
+ setStatusMessage(`Requested runtime upgrade for ${worker.workerName}.`);
+ return runtime;
+ } catch (error) {
+ setRuntimeError(
+ error instanceof Error ? error.message : "Failed to upgrade runtime.",
+ );
+ return null;
+ } finally {
+ setRuntimeBusy(false);
+ }
+ }
+
+ async function deleteWorker(workerId?: string) {
+ const activeClient = client();
+ const targetWorkerId = workerId ?? selectedWorkerId();
+ if (!activeClient || !targetWorkerId) return false;
+
+ setWorkerActionBusy(true);
+ setWorkersError(null);
+ try {
+ await activeClient.deleteWorker(targetWorkerId);
+ setWorkers((current) =>
+ current.filter((worker) => worker.workerId !== targetWorkerId),
+ );
+ if (selectedWorkerId() === targetWorkerId) {
+ setSelectedWorkerId(null);
+ setSelectedWorkerLaunch(null);
+ clearRuntimeState();
+ }
+ setStatusMessage("Deleted Cloud worker.");
+ return true;
+ } catch (error) {
+ setWorkersError(
+ error instanceof Error ? error.message : "Failed to delete worker.",
+ );
+ return false;
+ } finally {
+ setWorkerActionBusy(false);
+ }
+ }
+
+ async function redeployWorker(workerId?: string) {
+ const worker =
+ workers().find((entry) => entry.workerId === (workerId ?? selectedWorkerId())) ??
+ selectedWorkerSummary();
+ if (!worker) return null;
+ const deleted = await deleteWorker(worker.workerId);
+ if (!deleted) return null;
+ setWorkerName(worker.workerName);
+ return launchWorker();
+ }
+
+ async function updateSubscriptionCancellation(cancelAtPeriodEnd: boolean) {
+ const activeClient = client();
+ if (!activeClient || !user()) return null;
+ setBillingSubscriptionBusy(true);
+ setBillingError(null);
+ try {
+ const next = await activeClient.updateSubscriptionCancellation(
+ cancelAtPeriodEnd,
+ );
+ setBillingSummary(next.billing);
+ setStatusMessage(
+ cancelAtPeriodEnd
+ ? "Subscription will cancel at period end."
+ : "Subscription auto-renew resumed.",
+ );
+ return next;
+ } catch (error) {
+ setBillingError(
+ error instanceof Error
+ ? error.message
+ : "Failed to update subscription.",
+ );
+ return null;
+ } finally {
+ setBillingSubscriptionBusy(false);
+ }
+ }
+
+ async function refreshAdminOverview(includeBilling = true) {
+ const activeClient = client();
+ if (!activeClient || !authToken().trim()) {
+ setAdminOverview(null);
+ return null;
+ }
+ setAdminBusy(true);
+ setAdminError(null);
+ try {
+ const overview = await activeClient.getAdminOverview({ includeBilling });
+ setAdminOverview(overview);
+ return overview;
+ } catch (error) {
+ setAdminError(
+ error instanceof Error
+ ? error.message
+ : "Failed to load Cloud admin overview.",
+ );
+ return null;
+ } finally {
+ setAdminBusy(false);
+ }
+ }
+
+ return {
+ authMode,
+ setAuthMode,
+ email,
+ setEmail,
+ password,
+ setPassword,
+ workerName,
+ setWorkerName,
+ onboardingPending,
+ baseUrl,
+ baseUrlDraft,
+ setBaseUrlDraft,
+ baseUrlError,
+ authToken,
+ activeOrgId,
+ setActiveOrgId,
+ user,
+ orgs,
+ workers,
+ selectedWorkerId,
+ selectedWorker,
+ selectedWorkerSummary,
+ billingSummary,
+ billingSubscription,
+ billingCheckoutUrl,
+ runtimeSnapshot,
+ adminOverview,
+ activeOrg,
+ isConfigured,
+ isSignedIn,
+ canEditBaseUrl,
+ authBusy,
+ sessionBusy,
+ orgsBusy,
+ workersBusy,
+ billingBusy,
+ billingCheckoutBusy,
+ billingSubscriptionBusy,
+ workerActionBusy,
+ runtimeBusy,
+ adminBusy,
+ openingWorkerId,
+ desktopAuthRequested,
+ desktopRedirectBusy,
+ desktopRedirectUrl,
+ statusMessage,
+ authError,
+ orgsError,
+ workersError,
+ runtimeError,
+ billingError,
+ adminError,
+ summaryTone: createMemo(() => {
+ if (!isConfigured()) return "neutral" as const;
+ if (authError() || workersError() || orgsError() || billingError()) {
+ return "error" as const;
+ }
+ if (
+ sessionBusy() ||
+ orgsBusy() ||
+ workersBusy() ||
+ billingBusy() ||
+ billingCheckoutBusy() ||
+ billingSubscriptionBusy() ||
+ workerActionBusy()
+ ) {
+ return "warning" as const;
+ }
+ if (isSignedIn()) return "ready" as const;
+ return "neutral" as const;
+ }),
+ summaryLabel: createMemo(() => {
+ if (!isConfigured()) return canEditBaseUrl() ? "Cloud hidden" : "Unavailable";
+ if (authError()) return "Needs attention";
+ if (billingError()) return "Billing issue";
+ if (sessionBusy()) return "Checking session";
+ if (isSignedIn()) return "Connected";
+ return "Signed out";
+ }),
+ resolveLandingRoute,
+ handleCheckoutReturn,
+ openControlPlane,
+ openBrowserAuth,
+ applyBaseUrl,
+ disableCloud,
+ submitEmailAuth,
+ beginSocialAuth,
+ createDesktopRedirect,
+ openDesktopRedirect,
+ signOut,
+ refreshSession: syncGateState,
+ refreshOrgs,
+ refreshWorkers,
+ launchWorker,
+ selectWorker,
+ refreshSelectedWorker,
+ openWorker,
+ refreshRuntime,
+ upgradeRuntime,
+ deleteWorker,
+ redeployWorker,
+ refreshBilling,
+ updateSubscriptionCancellation,
+ refreshAdminOverview,
+ };
+}
+
+export type DenFeatureState = ReturnType;
diff --git a/apps/app/src/app/lib/den-gate.ts b/apps/app/src/app/lib/den-gate.ts
new file mode 100644
index 000000000..f93b3a352
--- /dev/null
+++ b/apps/app/src/app/lib/den-gate.ts
@@ -0,0 +1,88 @@
+import {
+ ENV_DEN_BASE_URL,
+ normalizeDenBaseUrl,
+ readStoredDenBaseUrls,
+ resolveDenBaseUrls,
+} from "./den";
+
+export const DEN_CONFIG_UPDATED_EVENT = "openwork-den-config-updated";
+
+export type DenFeatureConfigSource = "env" | "override" | "none";
+
+export type DenFeatureGate = {
+ enabled: boolean;
+ source: DenFeatureConfigSource;
+ baseUrl: string | null;
+ apiBaseUrl: string | null;
+ envBaseUrl: string | null;
+ overrideBaseUrl: string | null;
+ canConfigureInDeveloperMode: boolean;
+};
+
+export function canConfigureDenBaseUrlOverride(developerMode: boolean): boolean {
+ return developerMode && !ENV_DEN_BASE_URL;
+}
+
+export function readDenFeatureGate(developerMode: boolean): DenFeatureGate {
+ if (ENV_DEN_BASE_URL) {
+ const resolved = resolveDenBaseUrls(ENV_DEN_BASE_URL);
+ return {
+ enabled: true,
+ source: "env",
+ baseUrl: resolved.baseUrl,
+ apiBaseUrl: resolved.apiBaseUrl,
+ envBaseUrl: resolved.baseUrl,
+ overrideBaseUrl: readStoredDenBaseUrlOverride(),
+ canConfigureInDeveloperMode: false,
+ };
+ }
+
+ const stored = readStoredDenBaseUrls();
+ const overrideBaseUrl = stored.baseUrl ?? normalizeDenBaseUrl(stored.apiBaseUrl);
+ if (overrideBaseUrl) {
+ const resolved = resolveDenBaseUrls({
+ baseUrl: overrideBaseUrl,
+ apiBaseUrl: stored.apiBaseUrl,
+ });
+ return {
+ enabled: true,
+ source: "override",
+ baseUrl: resolved.baseUrl,
+ apiBaseUrl: resolved.apiBaseUrl,
+ envBaseUrl: null,
+ overrideBaseUrl: resolved.baseUrl,
+ canConfigureInDeveloperMode: canConfigureDenBaseUrlOverride(developerMode),
+ };
+ }
+
+ return {
+ enabled: false,
+ source: "none",
+ baseUrl: null,
+ apiBaseUrl: null,
+ envBaseUrl: null,
+ overrideBaseUrl: null,
+ canConfigureInDeveloperMode: canConfigureDenBaseUrlOverride(developerMode),
+ };
+}
+
+export function readStoredDenBaseUrlOverride(): string | null {
+ const stored = readStoredDenBaseUrls();
+ return stored.baseUrl ?? normalizeDenBaseUrl(stored.apiBaseUrl);
+}
+
+export function dispatchDenConfigUpdated(detail?: {
+ source?: DenFeatureConfigSource;
+ baseUrl?: string | null;
+ enabled?: boolean;
+}) {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ window.dispatchEvent(
+ new CustomEvent(DEN_CONFIG_UPDATED_EVENT, {
+ detail,
+ }),
+ );
+}
diff --git a/apps/app/src/app/lib/den.ts b/apps/app/src/app/lib/den.ts
index 8d3692572..058defb3f 100644
--- a/apps/app/src/app/lib/den.ts
+++ b/apps/app/src/app/lib/den.ts
@@ -7,11 +7,33 @@ const STORAGE_AUTH_TOKEN = "openwork.den.authToken";
const STORAGE_ACTIVE_ORG_ID = "openwork.den.activeOrgId";
const DEFAULT_DEN_TIMEOUT_MS = 12_000;
+export const DEN_BASE_URL_STORAGE_KEY = STORAGE_BASE_URL;
+export const DEN_API_BASE_URL_STORAGE_KEY = STORAGE_API_BASE_URL;
+export const DEN_AUTH_TOKEN_STORAGE_KEY = STORAGE_AUTH_TOKEN;
+export const DEN_ACTIVE_ORG_ID_STORAGE_KEY = STORAGE_ACTIVE_ORG_ID;
+
export const DEFAULT_DEN_AUTH_NAME = "OpenWork User";
+export const ENV_DEN_BASE_URL = (() => {
+ const rawValue =
+ typeof import.meta !== "undefined" && typeof import.meta.env?.VITE_DEN_BASE_URL === "string"
+ ? import.meta.env.VITE_DEN_BASE_URL.trim()
+ : "";
+ if (!rawValue) {
+ return null;
+ }
+
+ try {
+ const url = new URL(rawValue);
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
+ return null;
+ }
+ return url.toString().replace(/\/+$/, "");
+ } catch {
+ return null;
+ }
+})();
export const DEFAULT_DEN_BASE_URL =
- (typeof import.meta !== "undefined" && typeof import.meta.env?.VITE_DEN_BASE_URL === "string"
- ? import.meta.env.VITE_DEN_BASE_URL
- : "").trim() || "https://app.openworklabs.com";
+ ENV_DEN_BASE_URL ?? "https://app.openworklabs.com";
export type DenSettings = {
baseUrl: string;
@@ -56,6 +78,135 @@ export type DenWorkerTokens = {
workspaceId: string | null;
};
+export type DenWorkerLaunch = {
+ workerId: string;
+ workerName: string;
+ status: string;
+ provider: string | null;
+ instanceUrl: string | null;
+ openworkUrl: string | null;
+ workspaceId: string | null;
+ clientToken: string | null;
+ ownerToken: string | null;
+ hostToken: string | null;
+};
+
+export type DenRuntimeServiceName =
+ | "openwork-server"
+ | "opencode"
+ | "opencode-router";
+
+export type DenWorkerRuntimeService = {
+ name: DenRuntimeServiceName;
+ enabled: boolean;
+ running: boolean;
+ targetVersion: string | null;
+ actualVersion: string | null;
+ upgradeAvailable: boolean;
+};
+
+export type DenWorkerRuntimeSnapshot = {
+ services: DenWorkerRuntimeService[];
+ upgrade: {
+ status: "idle" | "running" | "failed";
+ startedAt: string | null;
+ finishedAt: string | null;
+ error: string | null;
+ };
+};
+
+export type DenSocialProvider = "github" | "google";
+
+export type DenDesktopHandoffGrant = {
+ grant: string;
+ expiresAt: string | null;
+ openworkUrl: string | null;
+};
+
+export type DenWorkerCreateInput = {
+ name: string;
+ description?: string;
+ destination: "local" | "cloud";
+ workspacePath?: string;
+ sandboxBackend?: string;
+ imageVersion?: string;
+};
+
+export type DenWorkerCreateResult =
+ | {
+ kind: "success";
+ worker: DenWorkerLaunch;
+ launchMode: "async" | "instant";
+ pollAfterMs: number;
+ }
+ | {
+ kind: "paywall";
+ checkoutUrl: string | null;
+ productId: string | null;
+ benefitId: string | null;
+ };
+
+export type DenAdminBillingStatus = {
+ status: "paid" | "unpaid" | "unavailable";
+ featureGateEnabled: boolean;
+ subscriptionId: string | null;
+ subscriptionStatus: string | null;
+ currentPeriodEnd: string | null;
+ source: "benefit" | "subscription" | "unavailable";
+ note: string | null;
+};
+
+export type DenAdminEntry = {
+ email: string;
+ note: string | null;
+};
+
+export type DenAdminSummary = {
+ totalUsers: number;
+ verifiedUsers: number;
+ recentUsers7d: number;
+ recentUsers30d: number;
+ totalWorkers: number;
+ cloudWorkers: number;
+ localWorkers: number;
+ usersWithWorkers: number;
+ usersWithoutWorkers: number;
+ paidUsers: number | null;
+ unpaidUsers: number | null;
+ billingUnavailableUsers: number | null;
+ adminCount: number;
+ billingLoaded: boolean;
+};
+
+export type DenAdminUser = {
+ id: string;
+ name: string | null;
+ email: string;
+ emailVerified: boolean;
+ createdAt: string | null;
+ updatedAt: string | null;
+ lastSeenAt: string | null;
+ sessionCount: number;
+ authProviders: string[];
+ workerCount: number;
+ cloudWorkerCount: number;
+ localWorkerCount: number;
+ latestWorkerCreatedAt: string | null;
+ billing: DenAdminBillingStatus | null;
+};
+
+export type DenAdminOverview = {
+ viewer: {
+ id: string;
+ email: string | null;
+ name: string | null;
+ };
+ admins: DenAdminEntry[];
+ summary: DenAdminSummary;
+ users: DenAdminUser[];
+ generatedAt: string | null;
+};
+
export type DenBillingPrice = {
amount: number | null;
currency: string | null;
@@ -114,6 +265,7 @@ type RawJsonResponse = {
ok: boolean;
status: number;
json: T | null;
+ headers: Headers;
};
export class DenApiError extends Error {
@@ -243,6 +395,23 @@ export function readDenSettings(): DenSettings {
};
}
+export function readStoredDenBaseUrls(): {
+ baseUrl: string | null;
+ apiBaseUrl: string | null;
+} {
+ if (typeof window === "undefined") {
+ return {
+ baseUrl: null,
+ apiBaseUrl: null,
+ };
+ }
+
+ return {
+ baseUrl: normalizeDenBaseUrl(window.localStorage.getItem(STORAGE_BASE_URL) ?? ""),
+ apiBaseUrl: normalizeDenBaseUrl(window.localStorage.getItem(STORAGE_API_BASE_URL) ?? ""),
+ };
+}
+
export function writeDenSettings(next: DenSettings) {
if (typeof window === "undefined") {
return;
@@ -325,6 +494,13 @@ function getToken(payload: unknown): string | null {
return payload.token.trim() || null;
}
+function getCheckoutUrl(payload: unknown): string | null {
+ if (!isRecord(payload) || !isRecord(payload.polar)) {
+ return null;
+ }
+ return typeof payload.polar.checkoutUrl === "string" ? payload.polar.checkoutUrl : null;
+}
+
function getOrgList(payload: unknown): DenOrgSummary[] {
if (!isRecord(payload) || !Array.isArray(payload.orgs)) {
return [];
@@ -393,6 +569,146 @@ function getWorkerTokens(payload: unknown): DenWorkerTokens | null {
};
}
+function getEffectiveWorkerStatus(
+ workerStatus: unknown,
+ instance: Record | null,
+): string {
+ const normalizedWorkerStatus = typeof workerStatus === "string" ? workerStatus : "unknown";
+ const normalized = normalizedWorkerStatus.trim().toLowerCase();
+ const instanceStatus =
+ instance && typeof instance.status === "string"
+ ? instance.status.trim().toLowerCase()
+ : null;
+
+ if (!instanceStatus) {
+ return normalizedWorkerStatus;
+ }
+
+ if (normalized === "provisioning" || normalized === "starting") {
+ return instanceStatus;
+ }
+
+ return normalizedWorkerStatus;
+}
+
+function getWorker(payload: unknown): DenWorkerLaunch | null {
+ if (!isRecord(payload) || !isRecord(payload.worker)) {
+ return null;
+ }
+
+ const worker = payload.worker;
+ if (typeof worker.id !== "string" || typeof worker.name !== "string") {
+ return null;
+ }
+
+ const instance = isRecord(payload.instance) ? payload.instance : null;
+ const tokens = isRecord(payload.tokens) ? payload.tokens : null;
+
+ return {
+ workerId: worker.id,
+ workerName: worker.name,
+ status: getEffectiveWorkerStatus(worker.status, instance),
+ provider: instance && typeof instance.provider === "string" ? instance.provider : null,
+ instanceUrl: instance && typeof instance.url === "string" ? instance.url : null,
+ openworkUrl: instance && typeof instance.url === "string" ? instance.url : null,
+ workspaceId: null,
+ clientToken: tokens && typeof tokens.client === "string" ? tokens.client : null,
+ ownerToken: tokens && typeof tokens.owner === "string" ? tokens.owner : null,
+ hostToken: tokens && typeof tokens.host === "string" ? tokens.host : null,
+ };
+}
+
+function getWorkerSummary(payload: unknown): DenWorkerSummary | null {
+ if (!isRecord(payload) || !isRecord(payload.worker)) {
+ return null;
+ }
+
+ const worker = payload.worker;
+ if (typeof worker.id !== "string" || typeof worker.name !== "string") {
+ return null;
+ }
+
+ const instance = isRecord(payload.instance) ? payload.instance : null;
+
+ return {
+ workerId: worker.id,
+ workerName: worker.name,
+ status: getEffectiveWorkerStatus(worker.status, instance),
+ instanceUrl: instance && typeof instance.url === "string" ? instance.url : null,
+ provider: instance && typeof instance.provider === "string" ? instance.provider : null,
+ isMine: worker.isMine === true,
+ createdAt: typeof worker.createdAt === "string" ? worker.createdAt : null,
+ };
+}
+
+function getWorkerRuntimeSnapshot(payload: unknown): DenWorkerRuntimeSnapshot | null {
+ if (!isRecord(payload) || !Array.isArray(payload.services)) {
+ return null;
+ }
+
+ const services = payload.services
+ .map((value) => {
+ if (!isRecord(value) || typeof value.name !== "string") {
+ return null;
+ }
+
+ const name = value.name;
+ if (
+ name !== "openwork-server" &&
+ name !== "opencode" &&
+ name !== "opencode-router"
+ ) {
+ return null;
+ }
+
+ return {
+ name,
+ enabled: value.enabled === true,
+ running: value.running === true,
+ targetVersion:
+ typeof value.targetVersion === "string" ? value.targetVersion : null,
+ actualVersion:
+ typeof value.actualVersion === "string" ? value.actualVersion : null,
+ upgradeAvailable: value.upgradeAvailable === true,
+ } satisfies DenWorkerRuntimeService;
+ })
+ .filter((item): item is DenWorkerRuntimeService => item !== null);
+
+ const upgrade = isRecord(payload.upgrade) ? payload.upgrade : null;
+
+ return {
+ services,
+ upgrade: {
+ status:
+ upgrade?.status === "running" ||
+ upgrade?.status === "failed" ||
+ upgrade?.status === "idle"
+ ? upgrade.status
+ : "idle",
+ startedAt:
+ typeof upgrade?.startedAt === "number"
+ ? new Date(upgrade.startedAt).toISOString()
+ : null,
+ finishedAt:
+ typeof upgrade?.finishedAt === "number"
+ ? new Date(upgrade.finishedAt).toISOString()
+ : null,
+ error: typeof upgrade?.error === "string" ? upgrade.error : null,
+ },
+ };
+}
+
+export function getRuntimeServiceLabel(name: DenRuntimeServiceName): string {
+ switch (name) {
+ case "openwork-server":
+ return "OpenWork server";
+ case "opencode":
+ return "OpenCode";
+ case "opencode-router":
+ return "OpenCode Router";
+ }
+}
+
function getBillingPrice(value: unknown): DenBillingPrice | null {
if (!isRecord(value)) {
return null;
@@ -472,6 +788,136 @@ function getBillingSummary(payload: unknown): DenBillingSummary | null {
};
}
+function toNumberValue(value: unknown): number {
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
+}
+
+function toNullableNumberValue(value: unknown): number | null {
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
+}
+
+function parseAdminBillingStatus(value: unknown): DenAdminBillingStatus | null {
+ if (!isRecord(value)) {
+ return null;
+ }
+
+ const status =
+ value.status === "paid" ||
+ value.status === "unpaid" ||
+ value.status === "unavailable"
+ ? value.status
+ : "unavailable";
+ const source =
+ value.source === "benefit" ||
+ value.source === "subscription" ||
+ value.source === "unavailable"
+ ? value.source
+ : "unavailable";
+
+ return {
+ status,
+ featureGateEnabled: value.featureGateEnabled === true,
+ subscriptionId:
+ typeof value.subscriptionId === "string" ? value.subscriptionId : null,
+ subscriptionStatus:
+ typeof value.subscriptionStatus === "string"
+ ? value.subscriptionStatus
+ : null,
+ currentPeriodEnd:
+ typeof value.currentPeriodEnd === "string" ? value.currentPeriodEnd : null,
+ source,
+ note: typeof value.note === "string" ? value.note : null,
+ };
+}
+
+function getAdminOverview(payload: unknown): DenAdminOverview | null {
+ if (
+ !isRecord(payload) ||
+ !isRecord(payload.summary) ||
+ !Array.isArray(payload.users) ||
+ !Array.isArray(payload.admins)
+ ) {
+ return null;
+ }
+
+ const viewer = isRecord(payload.viewer) ? payload.viewer : {};
+ const summary = payload.summary;
+
+ const users: DenAdminUser[] = payload.users
+ .map((value) => {
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.email !== "string") {
+ return null;
+ }
+
+ const authProviders = Array.isArray(value.authProviders)
+ ? value.authProviders.filter(
+ (provider): provider is string => typeof provider === "string",
+ )
+ : [];
+
+ return {
+ id: value.id,
+ name: typeof value.name === "string" ? value.name : null,
+ email: value.email,
+ emailVerified: value.emailVerified === true,
+ createdAt: typeof value.createdAt === "string" ? value.createdAt : null,
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : null,
+ lastSeenAt: typeof value.lastSeenAt === "string" ? value.lastSeenAt : null,
+ sessionCount: toNumberValue(value.sessionCount),
+ authProviders,
+ workerCount: toNumberValue(value.workerCount),
+ cloudWorkerCount: toNumberValue(value.cloudWorkerCount),
+ localWorkerCount: toNumberValue(value.localWorkerCount),
+ latestWorkerCreatedAt:
+ typeof value.latestWorkerCreatedAt === "string"
+ ? value.latestWorkerCreatedAt
+ : null,
+ billing: parseAdminBillingStatus(value.billing),
+ } satisfies DenAdminUser;
+ })
+ .filter((value): value is DenAdminUser => value !== null);
+
+ const admins: DenAdminEntry[] = payload.admins
+ .map((value) => {
+ if (!isRecord(value) || typeof value.email !== "string") {
+ return null;
+ }
+
+ return {
+ email: value.email,
+ note: typeof value.note === "string" ? value.note : null,
+ } satisfies DenAdminEntry;
+ })
+ .filter((value): value is DenAdminEntry => value !== null);
+
+ return {
+ viewer: {
+ id: typeof viewer.id === "string" ? viewer.id : "unknown",
+ email: typeof viewer.email === "string" ? viewer.email : null,
+ name: typeof viewer.name === "string" ? viewer.name : null,
+ },
+ admins,
+ summary: {
+ totalUsers: toNumberValue(summary.totalUsers),
+ verifiedUsers: toNumberValue(summary.verifiedUsers),
+ recentUsers7d: toNumberValue(summary.recentUsers7d),
+ recentUsers30d: toNumberValue(summary.recentUsers30d),
+ totalWorkers: toNumberValue(summary.totalWorkers),
+ cloudWorkers: toNumberValue(summary.cloudWorkers),
+ localWorkers: toNumberValue(summary.localWorkers),
+ usersWithWorkers: toNumberValue(summary.usersWithWorkers),
+ usersWithoutWorkers: toNumberValue(summary.usersWithoutWorkers),
+ paidUsers: toNullableNumberValue(summary.paidUsers),
+ unpaidUsers: toNullableNumberValue(summary.unpaidUsers),
+ billingUnavailableUsers: toNullableNumberValue(summary.billingUnavailableUsers),
+ adminCount: toNumberValue(summary.adminCount),
+ billingLoaded: summary.billingLoaded === true,
+ },
+ users,
+ generatedAt: typeof payload.generatedAt === "string" ? payload.generatedAt : null,
+ };
+}
+
const resolveFetch = () => (isTauriRuntime() ? tauriFetch : globalThis.fetch);
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise;
@@ -539,7 +985,7 @@ async function requestJsonRaw(
} catch {
json = null;
}
- return { ok: response.ok, status: response.status, json };
+ return { ok: response.ok, status: response.status, json, headers: response.headers };
}
async function requestJson(
@@ -585,6 +1031,36 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
return { user: getUser(payload), token: getToken(payload) };
},
+ async beginSocialAuth(input: {
+ provider: DenSocialProvider;
+ callbackURL: string;
+ errorCallbackURL?: string | null;
+ }): Promise<{ url: string }> {
+ const raw = await requestJsonRaw(baseUrls, "/api/auth/sign-in/social", {
+ method: "POST",
+ body: {
+ provider: input.provider,
+ callbackURL: input.callbackURL,
+ errorCallbackURL: input.errorCallbackURL ?? input.callbackURL,
+ },
+ });
+
+ if (!raw.ok) {
+ const payload = raw.json;
+ const code = isRecord(payload) && typeof payload.error === "string" ? payload.error : "request_failed";
+ const message = getErrorMessage(payload, `Request failed with ${raw.status}.`);
+ throw new DenApiError(raw.status, code, message, isRecord(payload) ? payload.details : undefined);
+ }
+
+ const payloadUrl = isRecord(raw.json) && typeof raw.json.url === "string" ? raw.json.url.trim() : "";
+ const headerUrl = raw.headers.get("location")?.trim() ?? "";
+ const url = payloadUrl || headerUrl;
+ if (!url) {
+ throw new DenApiError(500, "missing_redirect_url", "Social auth did not return a redirect URL.");
+ }
+ return { url };
+ },
+
async signOut() {
await requestJsonRaw(baseUrls, "/api/auth/sign-out", {
method: "POST",
@@ -613,6 +1089,26 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
return { user: getUser(payload), token: getToken(payload) };
},
+ async createDesktopHandoffGrant(input: {
+ next?: string | null;
+ desktopScheme?: string | null;
+ } = {}): Promise {
+ const payload = await requestJson(baseUrls, "/v1/auth/desktop-handoff", {
+ method: "POST",
+ token,
+ body: {
+ next: input.next?.trim() || undefined,
+ desktopScheme: input.desktopScheme?.trim() || undefined,
+ },
+ });
+
+ return {
+ grant: isRecord(payload) && typeof payload.grant === "string" ? payload.grant : "",
+ expiresAt: isRecord(payload) && typeof payload.expiresAt === "string" ? payload.expiresAt : null,
+ openworkUrl: isRecord(payload) && typeof payload.openworkUrl === "string" ? payload.openworkUrl : null,
+ };
+ },
+
async listOrgs(): Promise<{ orgs: DenOrgSummary[]; defaultOrgId: string | null }> {
const payload = await requestJson(baseUrls, "/v1/me/orgs", {
method: "GET",
@@ -635,6 +1131,62 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
return getWorkers(payload);
},
+ async createWorker(input: DenWorkerCreateInput): Promise {
+ const raw = await requestJsonRaw(baseUrls, "/v1/workers", {
+ method: "POST",
+ token,
+ body: {
+ name: input.name.trim(),
+ description: input.description?.trim() || undefined,
+ destination: input.destination,
+ workspacePath: input.workspacePath?.trim() || undefined,
+ sandboxBackend: input.sandboxBackend?.trim() || undefined,
+ imageVersion: input.imageVersion?.trim() || undefined,
+ },
+ });
+
+ if (raw.status === 402) {
+ return {
+ kind: "paywall",
+ checkoutUrl: getCheckoutUrl(raw.json),
+ productId: isRecord(raw.json) && isRecord(raw.json.polar) && typeof raw.json.polar.productId === "string" ? raw.json.polar.productId : null,
+ benefitId: isRecord(raw.json) && isRecord(raw.json.polar) && typeof raw.json.polar.benefitId === "string" ? raw.json.polar.benefitId : null,
+ };
+ }
+
+ if (!raw.ok) {
+ const payload = raw.json;
+ const code = isRecord(payload) && typeof payload.error === "string" ? payload.error : "request_failed";
+ const message = getErrorMessage(payload, `Request failed with ${raw.status}.`);
+ throw new DenApiError(raw.status, code, message, isRecord(payload) ? payload.details : undefined);
+ }
+
+ const worker = getWorker(raw.json);
+ if (!worker) {
+ throw new DenApiError(500, "invalid_worker_payload", "Worker create response was missing worker details.");
+ }
+
+ const launch = isRecord(raw.json) && isRecord(raw.json.launch) ? raw.json.launch : null;
+ return {
+ kind: "success",
+ worker,
+ launchMode: launch?.mode === "instant" ? "instant" : "async",
+ pollAfterMs: typeof launch?.pollAfterMs === "number" ? launch.pollAfterMs : 0,
+ };
+ },
+
+ async getWorker(workerId: string): Promise {
+ const payload = await requestJson(baseUrls, `/v1/workers/${encodeURIComponent(workerId)}`, {
+ method: "GET",
+ token,
+ });
+ const worker = getWorkerSummary(payload);
+ if (!worker) {
+ throw new DenApiError(500, "invalid_worker_payload", "Worker response was missing summary details.");
+ }
+ return worker;
+ },
+
async getWorkerTokens(workerId: string, orgId: string): Promise {
const params = new URLSearchParams();
params.set("orgId", orgId);
@@ -650,6 +1202,46 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
return tokens;
},
+ async getWorkerRuntime(workerId: string): Promise {
+ const payload = await requestJson(baseUrls, `/v1/workers/${encodeURIComponent(workerId)}/runtime`, {
+ method: "GET",
+ token,
+ });
+ const runtime = getWorkerRuntimeSnapshot(payload);
+ if (!runtime) {
+ throw new DenApiError(500, "invalid_runtime_payload", "Runtime response was missing service details.");
+ }
+ return runtime;
+ },
+
+ async upgradeWorkerRuntime(workerId: string, input: Record = {}): Promise {
+ const payload = await requestJson(baseUrls, `/v1/workers/${encodeURIComponent(workerId)}/runtime/upgrade`, {
+ method: "POST",
+ token,
+ body: input,
+ });
+ const runtime = getWorkerRuntimeSnapshot(payload);
+ if (!runtime) {
+ throw new DenApiError(500, "invalid_runtime_payload", "Runtime upgrade response was missing service details.");
+ }
+ return runtime;
+ },
+
+ async deleteWorker(workerId: string): Promise {
+ const raw = await requestJsonRaw(baseUrls, `/v1/workers/${encodeURIComponent(workerId)}`, {
+ method: "DELETE",
+ token,
+ });
+ if (raw.status === 204 || raw.ok) {
+ return;
+ }
+
+ const payload = raw.json;
+ const code = isRecord(payload) && typeof payload.error === "string" ? payload.error : "request_failed";
+ const message = getErrorMessage(payload, `Request failed with ${raw.status}.`);
+ throw new DenApiError(raw.status, code, message, isRecord(payload) ? payload.details : undefined);
+ },
+
async getBillingStatus(options: { includeCheckout?: boolean; includePortal?: boolean; includeInvoices?: boolean } = {}): Promise {
const params = new URLSearchParams();
if (options.includeCheckout) {
@@ -690,5 +1282,23 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
billing,
};
},
+
+ async getAdminOverview(options: { includeBilling?: boolean } = {}): Promise {
+ const params = new URLSearchParams();
+ if (options.includeBilling) {
+ params.set("includeBilling", "1");
+ }
+
+ const path = params.size > 0 ? `/v1/admin/overview?${params.toString()}` : "/v1/admin/overview";
+ const payload = await requestJson(baseUrls, path, {
+ method: "GET",
+ token,
+ });
+ const overview = getAdminOverview(payload);
+ if (!overview) {
+ throw new DenApiError(500, "invalid_admin_payload", "Admin overview response was missing details.");
+ }
+ return overview;
+ },
};
}
diff --git a/apps/app/src/app/pages/cloud.tsx b/apps/app/src/app/pages/cloud.tsx
new file mode 100644
index 000000000..7a641580c
--- /dev/null
+++ b/apps/app/src/app/pages/cloud.tsx
@@ -0,0 +1,945 @@
+import {
+ For,
+ Match,
+ Show,
+ Switch,
+ createEffect,
+ createMemo,
+ createSignal,
+} from "solid-js";
+import { useLocation, useNavigate } from "@solidjs/router";
+import {
+ ArrowUpRight,
+ Cloud,
+ CreditCard,
+ LogOut,
+ RefreshCcw,
+ Server,
+ Settings,
+ Shield,
+ Sparkles,
+ UserCircle2,
+} from "lucide-solid";
+import Button from "../components/button";
+import TextInput from "../components/text-input";
+import { usePlatform } from "../context/platform";
+import {
+ formatDenIsoDate,
+ formatDenMoneyMinor,
+ formatDenRecurringInterval,
+ formatDenSubscriptionStatus,
+ denStatusBadgeClass,
+ denWorkerStatusMeta,
+} from "../features/den/formatters";
+import { createDenFeatureState } from "../features/den/state";
+import { getRuntimeServiceLabel } from "../lib/den";
+
+export type CloudViewProps = {
+ developerMode: boolean;
+ openCloudSettings: () => void;
+ connectRemoteWorkspace: (input: {
+ openworkHostUrl?: string | null;
+ openworkToken?: string | null;
+ directory?: string | null;
+ displayName?: string | null;
+ }) => Promise;
+};
+
+type CloudRoute = "auth" | "dashboard" | "checkout" | "admin";
+
+function routeButtonClass(active: boolean) {
+ return active
+ ? "bg-gray-12/10 text-white border-gray-6/30"
+ : "text-gray-10 border-gray-6/50 hover:text-gray-12 hover:bg-gray-2/40";
+}
+
+export default function CloudView(props: CloudViewProps) {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const platform = usePlatform();
+ const state = createDenFeatureState({
+ developerMode: () => props.developerMode,
+ openLink: platform.openLink,
+ connectRemoteWorkspace: props.connectRemoteWorkspace,
+ });
+ const [checkoutTokenHandled, setCheckoutTokenHandled] = createSignal(
+ null,
+ );
+ const [includeBillingDetails, setIncludeBillingDetails] = createSignal(true);
+
+ const route = createMemo(() => {
+ const path = location.pathname.toLowerCase();
+ if (path === "/cloud/admin") return "admin";
+ if (path === "/cloud/checkout") return "checkout";
+ if (path === "/cloud/dashboard") return "dashboard";
+ return "auth";
+ });
+
+ const customerSessionToken = createMemo(() => {
+ const params = new URLSearchParams(location.search);
+ return params.get("customer_session_token")?.trim() ?? null;
+ });
+
+ createEffect(() => {
+ const path = location.pathname.toLowerCase();
+ if (!path.startsWith("/cloud")) return;
+ if (
+ path !== "/cloud" &&
+ path !== "/cloud/dashboard" &&
+ path !== "/cloud/checkout" &&
+ path !== "/cloud/admin"
+ ) {
+ navigate("/cloud", { replace: true });
+ }
+ });
+
+ createEffect(() => {
+ if (location.pathname.toLowerCase() !== "/cloud") return;
+ if (!state.isConfigured()) return;
+ if (!state.isSignedIn()) return;
+ if (state.desktopAuthRequested()) return;
+ navigate(state.resolveLandingRoute(), { replace: true });
+ });
+
+ createEffect(() => {
+ const token = customerSessionToken();
+ if (!token) return;
+ if (checkoutTokenHandled() === token) return;
+ setCheckoutTokenHandled(token);
+ void state.handleCheckoutReturn(token).then((target) => {
+ navigate(target, { replace: true });
+ });
+ });
+
+ const cloudTabs: Array<{ key: Exclude; label: string }> = [
+ { key: "dashboard", label: "Dashboard" },
+ { key: "checkout", label: "Billing" },
+ { key: "admin", label: "Admin" },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+ OpenWork Cloud
+
+
+
+ Launch, manage, and reconnect your Den workers from the main app.
+
+
+ Cloud routes live inside OpenWork now. Configure a Den control plane, sign in, then launch workers and reconnect them with the same remote flow the desktop app already understands.
+
+
+
+
+
+
+ {state.summaryLabel()}
+
+
+
+ Cloud settings
+
+
+
+
+
+ {(value) => (
+
+ {value()}
+
+ )}
+
+
+
+
+
+
+ {(tab) => (
+ navigate(`/cloud/${tab.key}`)}
+ >
+ {tab.label}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {state.canEditBaseUrl() ? "Unlock Cloud locally" : "Cloud is unavailable in this build"}
+
+
+ {state.canEditBaseUrl()
+ ? "Open Cloud settings, save a Den control plane URL, and this route will unlock immediately on this device."
+ : "This build has no Den API URL configured. Enable developer mode or provide VITE_DEN_BASE_URL to surface Cloud features."}
+
+
+
+ Open Cloud settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cloud access
+
+
+ {state.authMode() === "sign-up"
+ ? "Create your OpenWork Cloud account."
+ : "Sign in to OpenWork Cloud."}
+
+
+ Direct email auth works inside the app. Social auth and browser handoff are also available when you want the full external flow.
+
+
+
+
+
+
+
+ state.setAuthMode("sign-in")}
+ >
+ Sign in
+
+ state.setAuthMode("sign-up")}
+ >
+ Create account
+
+
+
+
+ state.setEmail(event.currentTarget.value)}
+ placeholder="you@example.com"
+ disabled={state.authBusy()}
+ />
+ state.setPassword(event.currentTarget.value)}
+ placeholder="••••••••"
+ disabled={state.authBusy()}
+ />
+
+
+
+
{
+ void state.submitEmailAuth().then((target) => {
+ if (target) navigate(target);
+ });
+ }}
+ >
+ {state.authBusy()
+ ? "Working..."
+ : state.authMode() === "sign-up"
+ ? "Create account"
+ : "Sign in"}
+
+
state.openBrowserAuth(state.authMode())}
+ >
+ Continue in browser
+
+
+
+
+
+
+ Social auth
+
+
+ void state.beginSocialAuth("github")}
+ >
+ Continue with GitHub
+
+ void state.beginSocialAuth("google")}
+ >
+ Continue with Google
+
+
+
+
+
+
+
Finish in OpenWork
+
+ Sign in here, then bounce back into the desktop app with a one-time handoff link.
+
+
+ {(redirectAccessor) => (
+
+
state.openDesktopRedirect()}>
+ Open OpenWork
+
+
+
{redirectAccessor()}
+
+ )}
+
+
+ Preparing desktop handoff...
+
+
+
+
+
+ {(value) => (
+
+ {value()}
+
+ )}
+
+
+
+
+
+
+
+
+
+
Sign in to continue
+
+ Cloud dashboard routes need an active Den session.
+
+
navigate("/cloud")}>Go to Cloud auth
+
+
+ }
+ >
+
+
+
+
+
+
Account
+
+ {state.user()?.name || state.user()?.email}
+
+
{state.user()?.email}
+
+
void state.signOut()}>
+
+ Sign out
+
+
+
+
+
+
+
+
Active org
+
+ Workers are listed from the selected org.
+
+
+
void state.refreshOrgs()}
+ disabled={state.orgsBusy()}
+ >
+
+ Refresh orgs
+
+
+
+
+ Org
+ state.setActiveOrgId(event.currentTarget.value)}
+ disabled={state.orgsBusy() || state.orgs().length === 0}
+ >
+
+ {(org) => (
+
+ {org.name} {org.role === "owner" ? "(Owner)" : "(Member)"}
+
+ )}
+
+
+
+
+
+ {(value) => (
+
+ {value()}
+
+ )}
+
+
+
+
+
+
+
Create Cloud worker
+
+ Launch a new cloud worker from the unified app experience.
+
+
+
{
+ void state.launchWorker().then((target) => {
+ if (target) navigate(target);
+ });
+ }}
+ >
+
+ {state.workerActionBusy() ? "Launching..." : "Launch worker"}
+
+
+
+ state.setWorkerName(event.currentTarget.value)}
+ placeholder="My Worker"
+ disabled={state.workerActionBusy()}
+ />
+
+
+
+
+
+
+
+
+ Billing snapshot
+
+
+ Quick access to checkout, invoices, and renewal state.
+
+
+
+ navigate("/cloud/checkout")}>Open billing
+ void state.refreshBilling()} disabled={state.billingBusy() || state.billingCheckoutBusy()}>
+
+ Refresh
+
+
+
+
Billing details will appear after your first refresh. }>
+ {(summaryAccessor) => {
+ const summary = summaryAccessor();
+ return (
+
+
+ {!summary.featureGateEnabled
+ ? "Billing disabled"
+ : summary.hasActivePlan
+ ? "Active plan"
+ : "Payment required"}
+
+
+ {summary.price && summary.price.amount !== null
+ ? `${formatDenMoneyMinor(summary.price.amount, summary.price.currency)} ${formatDenRecurringInterval(summary.price.recurringInterval, summary.price.recurringIntervalCount)}`
+ : "Current plan amount unavailable."}
+
+
+ );
+ }}
+
+
+
+
+
+
+
+
+
+
+ Workers
+
+
+ Select a worker to inspect runtime, open it in OpenWork, or manage lifecycle actions.
+
+
+
void state.refreshWorkers()} disabled={state.workersBusy() || !state.activeOrgId().trim()}>
+
+ Refresh workers
+
+
+
+
+ {(value) => (
+
+ {value()}
+
+ )}
+
+
+
+
0} fallback={No Cloud workers are visible for this org yet.
}>
+
+ {(worker) => {
+ const meta = createMemo(() => denWorkerStatusMeta(worker.status));
+ return (
+ state.selectWorker(worker.workerId)}
+ >
+
+
+
+
{worker.workerName}
+
+ {meta().label}
+
+
+
+ {worker.provider ? `${worker.provider} worker` : "Cloud worker"}
+
+
+
+ {
+ event.stopPropagation();
+ void state.openWorker(worker.workerId);
+ }}
+ >
+ {state.openingWorkerId() === worker.workerId ? "Opening..." : "Open"}
+
+ {
+ event.stopPropagation();
+ void state.redeployWorker(worker.workerId).then((target) => {
+ if (target) navigate(target);
+ });
+ }}
+ disabled={state.workerActionBusy()}
+ >
+ Redeploy
+
+ {
+ event.stopPropagation();
+ void state.deleteWorker(worker.workerId);
+ }}
+ disabled={state.workerActionBusy()}
+ >
+ Delete
+
+
+
+
+ );
+ }}
+
+
+
+
+
+
+ {(workerAccessor) => {
+ const worker = workerAccessor();
+ const meta = createMemo(() => denWorkerStatusMeta(worker.status));
+ return (
+
+
+
+
{worker.workerName}
+
+ {meta().label}
+ {worker.provider || "Cloud worker"}
+ {worker.instanceUrl}
+
+
+
+ void state.refreshSelectedWorker()} disabled={state.workerActionBusy()}>
+ Refresh status
+
+ void state.refreshRuntime()} disabled={state.runtimeBusy()}>
+ Runtime
+
+
+
+
+
+ {(value) => {value()}
}
+
+
+
Load runtime details to inspect service versions and upgrades. }>
+ {(runtimeAccessor) => {
+ const runtime = runtimeAccessor();
+ return (
+
+
+
+ {(service) => (
+
+
{getRuntimeServiceLabel(service.name)}
+
+ {service.running ? "Running" : service.enabled ? "Installed" : "Unavailable"}
+
+
+ {service.actualVersion || service.targetVersion || "Version unavailable"}
+
+
+ )}
+
+
+
+ void state.upgradeRuntime()} disabled={state.runtimeBusy()}>
+ {state.runtimeBusy() ? "Updating..." : "Upgrade runtime"}
+
+
+
+ );
+ }}
+
+
+ );
+ }}
+
+
+
+
+
+
+
+
+
+
Sign in to view billing
+
+ Cloud checkout and subscription management need an active Den session.
+
+
navigate("/cloud")}>Go to Cloud auth
+
+
+ }
+ >
+
+
+
+
Cloud billing
+
+ Review plan state, open checkout or billing portal links, and manage your subscription from inside the app.
+
+
+
+ navigate("/cloud/dashboard")}>Back to dashboard
+ void state.refreshBilling({ includeCheckout: true })} disabled={state.billingBusy() || state.billingCheckoutBusy()}>
+
+ Refresh billing
+
+
+
+
+
+ {(value) => {value()}
}
+
+
+
Load billing to inspect Cloud plan state. }>
+ {(summaryAccessor) => {
+ const summary = summaryAccessor();
+ return (
+
+
+
+
Plan status
+
+ {!summary.featureGateEnabled
+ ? "Billing disabled"
+ : summary.hasActivePlan
+ ? "Active plan"
+ : "Payment required"}
+
+
+ {!summary.featureGateEnabled
+ ? "Cloud billing gates are disabled in this environment."
+ : summary.hasActivePlan
+ ? "This account can launch additional cloud workers right now."
+ : "Complete checkout to unlock additional Cloud worker launches."}
+
+
+ {summary.price && summary.price.amount !== null
+ ? `${formatDenMoneyMinor(summary.price.amount, summary.price.currency)} ${formatDenRecurringInterval(summary.price.recurringInterval, summary.price.recurringIntervalCount)}`
+ : "Current plan amount is unavailable."}
+
+
+
+
+
Subscription
+
No active subscription found yet. }>
+ {(subscriptionAccessor) => {
+ const subscription = subscriptionAccessor();
+ return (
+ <>
+
{formatDenSubscriptionStatus(subscription.status)}
+
+ {formatDenMoneyMinor(subscription.amount, subscription.currency)} {formatDenRecurringInterval(subscription.recurringInterval, subscription.recurringIntervalCount)}
+
+
+ {subscription.cancelAtPeriodEnd
+ ? `Cancels on ${formatDenIsoDate(subscription.currentPeriodEnd)}`
+ : `Renews on ${formatDenIsoDate(subscription.currentPeriodEnd)}`}
+
+ >
+ );
+ }}
+
+
+
+
+
+
+ {(checkoutUrl) => (
+ platform.openLink(checkoutUrl())}>
+ Continue checkout
+
+
+ )}
+
+
+ {(portalUrl) => (
+ platform.openLink(portalUrl())}>
+ Open billing portal
+
+
+ )}
+
+
+ {(subscriptionAccessor) => {
+ const subscription = subscriptionAccessor();
+ return (
+ void state.updateSubscriptionCancellation(!subscription.cancelAtPeriodEnd)}
+ disabled={state.billingSubscriptionBusy()}
+ >
+ {state.billingSubscriptionBusy()
+ ? "Updating..."
+ : subscription.cancelAtPeriodEnd
+ ? "Resume auto-renew"
+ : "Cancel at period end"}
+
+ );
+ }}
+
+
+
+
0}>
+
+
Invoices
+
+ {(invoice) => (
+
+
+
+ {invoice.invoiceNumber || formatDenSubscriptionStatus(invoice.status)}
+
+
+ {formatDenIsoDate(invoice.createdAt)} · {formatDenMoneyMinor(invoice.totalAmount, invoice.currency)} · {formatDenSubscriptionStatus(invoice.status)}
+
+
+
+ {(invoiceUrl) => (
+ platform.openLink(invoiceUrl())}>
+ Open invoice
+
+
+ )}
+
+
+ )}
+
+
+
+
+ );
+ }}
+
+
+
+
+
+
+
+
+
Sign in to view admin data
+
+ Cloud admin screens need an authenticated Den session before they can check your access level.
+
+
navigate("/cloud")}>Go to Cloud auth
+
+
+ }
+ >
+
+
+
+
+
+ Cloud admin
+
+
+ Inspect Cloud users, worker counts, provider usage, and billing state when your account is on the admin allowlist.
+
+
+
+ setIncludeBillingDetails((value) => !value)}>
+ {includeBillingDetails() ? "Hide billing lookups" : "Include billing lookups"}
+
+ void state.refreshAdminOverview(includeBillingDetails())} disabled={state.adminBusy()}>
+
+ Load admin overview
+
+
+
+
+
+ {(value) => {value()}
}
+
+
+
Load the admin overview to inspect Cloud user and worker activity. }>
+ {(overviewAccessor) => {
+ const overview = overviewAccessor();
+ return (
+
+
+
+
Users
+
{overview.summary.totalUsers}
+
{overview.summary.verifiedUsers} verified
+
+
+
Workers
+
{overview.summary.totalWorkers}
+
{overview.summary.cloudWorkers} cloud / {overview.summary.localWorkers} local
+
+
+
Paid users
+
{overview.summary.paidUsers ?? "-"}
+
Billing loaded: {overview.summary.billingLoaded ? "yes" : "no"}
+
+
+
Recent users
+
{overview.summary.recentUsers7d}
+
{overview.summary.recentUsers30d} in the last 30 days
+
+
+
+
+
+
+
+
+ User
+ Providers
+ Workers
+ Billing
+ Last seen
+
+
+
+
+ {(entry) => (
+
+
+ {entry.name || entry.email}
+ {entry.email}
+
+
+ {entry.authProviders.join(", ") || "-"}
+
+
+ {entry.workerCount}
+ {entry.cloudWorkerCount} cloud / {entry.localWorkerCount} local
+
+
+ {entry.billing ? formatDenSubscriptionStatus(entry.billing.status) : "-"}
+ {entry.billing?.note || "No billing note"}
+
+
+ {formatDenIsoDate(entry.lastSeenAt)}
+
+
+ )}
+
+
+
+
+
+
+ );
+ }}
+
+
+