mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
feat(app): gate desktop updates by org config (#1512)
* feat(app): gate desktop updates by org config Return allowed desktop versions through the signed-in desktop config endpoint and only surface Tauri updates when both the server and active org explicitly allow them, logging failures silently for debugging. * fix(app): trace den-gated update checks Wait for Den auth hydration before the startup update check and add debug logging around auth restoration and update gating so local-vs-cloud version decisions are visible while testing. --------- Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
@@ -1709,6 +1709,18 @@ export default function App() {
|
||||
const [appVersion, setAppVersion] = createSignal<string | null>(null);
|
||||
const [launchUpdateCheckTriggered, setLaunchUpdateCheckTriggered] = createSignal(false);
|
||||
|
||||
const logAppUpdateLifecycle = (label: string, payload?: unknown) => {
|
||||
try {
|
||||
if (payload === undefined) {
|
||||
console.log(`[APP-UPDATES] ${label}`);
|
||||
} else {
|
||||
console.log(`[APP-UPDATES] ${label}`, payload);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const busySeconds = createMemo(() => {
|
||||
const start = busyStartedAt();
|
||||
@@ -1893,7 +1905,10 @@ export default function App() {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (!launchUpdateCheckTriggered()) {
|
||||
if (!launchUpdateCheckTriggered() && denAuth.status() !== "checking") {
|
||||
logAppUpdateLifecycle("mount-triggering-launch-update-check", {
|
||||
denAuthStatus: denAuth.status(),
|
||||
});
|
||||
setLaunchUpdateCheckTriggered(true);
|
||||
checkForUpdates({ quiet: true }).catch(() => undefined);
|
||||
}
|
||||
@@ -2052,14 +2067,34 @@ export default function App() {
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
logAppUpdateLifecycle("den-auth-status", {
|
||||
status: denAuth.status(),
|
||||
isSignedIn: denAuth.isSignedIn(),
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (booting()) return;
|
||||
if (!isTauriRuntime()) return;
|
||||
if (launchUpdateCheckTriggered()) return;
|
||||
if (launchUpdateCheckTriggered()) {
|
||||
logAppUpdateLifecycle("launch-update-check-skipped-already-triggered");
|
||||
return;
|
||||
}
|
||||
if (denAuth.status() === "checking") {
|
||||
logAppUpdateLifecycle("launch-update-check-waiting-for-den-auth", {
|
||||
denAuthStatus: denAuth.status(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const state = updateStatus();
|
||||
if (state.state === "checking" || state.state === "downloading") return;
|
||||
|
||||
logAppUpdateLifecycle("effect-triggering-launch-update-check", {
|
||||
denAuthStatus: denAuth.status(),
|
||||
updateState: state.state,
|
||||
});
|
||||
setLaunchUpdateCheckTriggered(true);
|
||||
checkForUpdates({ quiet: true }).catch(() => undefined);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createContext, createMemo, createSignal, onCleanup, onMount, useContext, type Accessor, type ParentProps } from "solid-js";
|
||||
import { clearDenSession, createDenClient, DenApiError, ensureDenActiveOrganization, readDenSettings, type DenUser } from "../lib/den";
|
||||
import { denSessionUpdatedEvent } from "../lib/den-session-events";
|
||||
import { recordDevLog } from "../lib/dev-log";
|
||||
|
||||
type DenAuthStatus = "checking" | "signed_in" | "signed_out";
|
||||
|
||||
@@ -14,6 +15,19 @@ type DenAuthStore = {
|
||||
|
||||
const DenAuthContext = createContext<DenAuthStore>();
|
||||
|
||||
function logDenAuth(label: string, payload?: unknown) {
|
||||
try {
|
||||
recordDevLog(true, { level: "debug", source: "den-auth", label, payload });
|
||||
if (payload === undefined) {
|
||||
console.log(`[DEN-AUTH] ${label}`);
|
||||
} else {
|
||||
console.log(`[DEN-AUTH] ${label}`, payload);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function DenAuthProvider(props: ParentProps) {
|
||||
const [status, setStatus] = createSignal<DenAuthStatus>("checking");
|
||||
const [user, setUser] = createSignal<DenUser | null>(null);
|
||||
@@ -25,14 +39,24 @@ export function DenAuthProvider(props: ParentProps) {
|
||||
const settings = readDenSettings();
|
||||
const token = settings.authToken?.trim() ?? "";
|
||||
|
||||
logDenAuth("refresh-start", {
|
||||
currentRun,
|
||||
hasToken: Boolean(token),
|
||||
activeOrgId: settings.activeOrgId ?? null,
|
||||
activeOrgSlug: settings.activeOrgSlug ?? null,
|
||||
baseUrl: settings.baseUrl,
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
setUser(null);
|
||||
setError(null);
|
||||
setStatus("signed_out");
|
||||
logDenAuth("refresh-signed-out-no-token", { currentRun });
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("checking");
|
||||
logDenAuth("refresh-status-checking", { currentRun });
|
||||
|
||||
try {
|
||||
const nextUser = await createDenClient({
|
||||
@@ -42,6 +66,7 @@ export function DenAuthProvider(props: ParentProps) {
|
||||
}).getSession();
|
||||
|
||||
if (currentRun !== refreshToken) {
|
||||
logDenAuth("refresh-stale-after-session", { currentRun, refreshToken });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,14 +77,22 @@ export function DenAuthProvider(props: ParentProps) {
|
||||
}).catch(() => null);
|
||||
|
||||
if (currentRun !== refreshToken) {
|
||||
logDenAuth("refresh-stale-after-org-sync", { currentRun, refreshToken });
|
||||
return;
|
||||
}
|
||||
|
||||
setUser(nextUser);
|
||||
setError(null);
|
||||
setStatus("signed_in");
|
||||
logDenAuth("refresh-signed-in", {
|
||||
currentRun,
|
||||
userId: nextUser.id,
|
||||
activeOrgId: readDenSettings().activeOrgId ?? null,
|
||||
activeOrgSlug: readDenSettings().activeOrgSlug ?? null,
|
||||
});
|
||||
} catch (nextError) {
|
||||
if (currentRun !== refreshToken) {
|
||||
logDenAuth("refresh-stale-after-error", { currentRun, refreshToken });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,6 +103,10 @@ export function DenAuthProvider(props: ParentProps) {
|
||||
setUser(null);
|
||||
setError(nextError instanceof Error ? nextError.message : "Failed to restore OpenWork Cloud session.");
|
||||
setStatus("signed_out");
|
||||
logDenAuth("refresh-error", {
|
||||
currentRun,
|
||||
error: nextError instanceof Error ? nextError.message : String(nextError),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -81,6 +118,7 @@ export function DenAuthProvider(props: ParentProps) {
|
||||
}
|
||||
|
||||
const handleSessionUpdated = () => {
|
||||
logDenAuth("session-updated-event");
|
||||
void refresh();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DesktopAppRestrictions as DenDesktopConfig } from "@openwork/types/den/desktop-app-restrictions";
|
||||
import type { DesktopAppRestrictions } from "@openwork/types/den/desktop-app-restrictions";
|
||||
import type { DenDesktopConfig } from "../lib/den";
|
||||
import type { ModelRef } from "../types";
|
||||
|
||||
export type DesktopAppRestrictionKey = keyof DenDesktopConfig;
|
||||
export type DesktopAppRestrictionKey = keyof DesktopAppRestrictions;
|
||||
|
||||
export type DesktopAppRestrictionChecker = (input: {
|
||||
restriction: DesktopAppRestrictionKey;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
|
||||
import { normalizeDesktopAppRestrictions, type DesktopAppRestrictions } from "@openwork/types/den/desktop-app-restrictions";
|
||||
import { normalizeDesktopConfig, type DesktopConfig as SharedDesktopConfig } from "@openwork/types/den/desktop-app-restrictions";
|
||||
import { isDesktopDeployment } from "./openwork-deployment";
|
||||
import {
|
||||
dispatchDenSettingsChanged,
|
||||
@@ -54,7 +54,7 @@ export type DenBootstrapConfig = DenBaseUrls & {
|
||||
requireSignin: boolean;
|
||||
};
|
||||
|
||||
export type DenDesktopConfig = DesktopAppRestrictions;
|
||||
export type DenDesktopConfig = SharedDesktopConfig;
|
||||
|
||||
export type DenUser = {
|
||||
id: string;
|
||||
@@ -237,7 +237,7 @@ function getDenAppVersionMetadata(payload: unknown): DenAppVersionMetadata | nul
|
||||
}
|
||||
|
||||
export function normalizeDenDesktopConfig(payload: unknown): DenDesktopConfig {
|
||||
return normalizeDesktopAppRestrictions(payload);
|
||||
return normalizeDesktopConfig(payload);
|
||||
}
|
||||
|
||||
export function normalizeDenBaseUrl(input: string | null | undefined): string | null {
|
||||
|
||||
@@ -18,7 +18,8 @@ import type {
|
||||
import { addOpencodeCacheHint, isTauriRuntime, safeStringify } from "./utils";
|
||||
import { filterProviderList, mapConfigProvidersToList } from "./utils/providers";
|
||||
import { createUpdaterState, type UpdateStatus } from "./context/updater";
|
||||
import { createDenClient, readDenSettings } from "./lib/den";
|
||||
import { createDenClient, readDenSettings, type DenDesktopConfig } from "./lib/den";
|
||||
import { recordDevLog } from "./lib/dev-log";
|
||||
import {
|
||||
resetOpenworkState,
|
||||
resetOpencodeCache,
|
||||
@@ -132,14 +133,122 @@ function compareVersions(left: string, right: string): number | null {
|
||||
return comparePrereleaseIdentifiers(parsedLeft.prerelease, parsedRight.prerelease);
|
||||
}
|
||||
|
||||
function logUpdateGateFailure(label: string, payload?: unknown) {
|
||||
try {
|
||||
recordDevLog(true, {
|
||||
level: "warn",
|
||||
source: "updates",
|
||||
label,
|
||||
payload,
|
||||
});
|
||||
if (payload === undefined) {
|
||||
console.warn(`[UPDATES] ${label}`);
|
||||
} else {
|
||||
console.warn(`[UPDATES] ${label}`, payload);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function logUpdateGateDebug(label: string, payload?: unknown) {
|
||||
try {
|
||||
recordDevLog(true, {
|
||||
level: "debug",
|
||||
source: "updates",
|
||||
label,
|
||||
payload,
|
||||
});
|
||||
if (payload === undefined) {
|
||||
console.log(`[UPDATES] ${label}`);
|
||||
} else {
|
||||
console.log(`[UPDATES] ${label}`, payload);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function isUpdateAllowedByDesktopConfig(
|
||||
updateVersion: string,
|
||||
desktopConfig: DenDesktopConfig | null | undefined,
|
||||
) {
|
||||
if (!Array.isArray(desktopConfig?.allowedDesktopVersions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return desktopConfig.allowedDesktopVersions.some(
|
||||
(allowedVersion) => compareVersions(updateVersion, allowedVersion) === 0,
|
||||
);
|
||||
}
|
||||
|
||||
async function isUpdateSupportedByDen(updateVersion: string) {
|
||||
try {
|
||||
const settings = readDenSettings();
|
||||
const client = createDenClient({ baseUrl: settings.baseUrl, apiBaseUrl: settings.apiBaseUrl });
|
||||
const token = settings.authToken?.trim() ?? "";
|
||||
logUpdateGateDebug("den-update-check-start", {
|
||||
updateVersion,
|
||||
hasToken: Boolean(token),
|
||||
activeOrgId: settings.activeOrgId ?? null,
|
||||
activeOrgSlug: settings.activeOrgSlug ?? null,
|
||||
baseUrl: settings.baseUrl,
|
||||
apiBaseUrl: settings.apiBaseUrl ?? null,
|
||||
});
|
||||
const client = createDenClient({
|
||||
baseUrl: settings.baseUrl,
|
||||
apiBaseUrl: settings.apiBaseUrl,
|
||||
...(token ? { token } : {}),
|
||||
});
|
||||
const metadata = await client.getAppVersionMetadata();
|
||||
const comparison = compareVersions(updateVersion, metadata.latestAppVersion);
|
||||
return comparison !== null && comparison <= 0;
|
||||
} catch {
|
||||
logUpdateGateDebug("den-update-check-app-version-response", {
|
||||
updateVersion,
|
||||
minAppVersion: metadata.minAppVersion,
|
||||
latestAppVersion: metadata.latestAppVersion,
|
||||
comparison,
|
||||
});
|
||||
if (comparison === null) {
|
||||
logUpdateGateFailure("den-update-check-invalid-version-comparison", {
|
||||
updateVersion,
|
||||
latestAppVersion: metadata.latestAppVersion,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (comparison > 0) {
|
||||
logUpdateGateDebug("den-update-check-blocked-by-server-max", {
|
||||
updateVersion,
|
||||
latestAppVersion: metadata.latestAppVersion,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
logUpdateGateDebug("den-update-check-allowed-no-token", { updateVersion });
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const desktopConfig = await client.getDesktopConfig();
|
||||
const allowed = isUpdateAllowedByDesktopConfig(updateVersion, desktopConfig);
|
||||
logUpdateGateDebug("den-update-check-desktop-config-response", {
|
||||
updateVersion,
|
||||
allowedDesktopVersions: desktopConfig.allowedDesktopVersions ?? null,
|
||||
allowed,
|
||||
});
|
||||
return allowed;
|
||||
} catch (error) {
|
||||
logUpdateGateFailure("den-update-check-desktop-config-fetch-failed", {
|
||||
updateVersion,
|
||||
error: error instanceof Error ? error.message : safeStringify(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logUpdateGateFailure("den-update-check-app-version-fetch-failed", {
|
||||
updateVersion,
|
||||
error: error instanceof Error ? error.message : safeStringify(error),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -516,6 +625,15 @@ export function createSystemState(options: {
|
||||
const update = (await check({ timeout: 8_000 })) as unknown as UpdateHandle | null;
|
||||
const checkedAt = Date.now();
|
||||
|
||||
logUpdateGateDebug("tauri-update-check-result", update
|
||||
? {
|
||||
available: update.available,
|
||||
currentVersion: update.currentVersion,
|
||||
version: update.version,
|
||||
date: update.date ?? null,
|
||||
}
|
||||
: { available: false });
|
||||
|
||||
if (!update) {
|
||||
setPendingUpdate(null);
|
||||
setUpdateStatus({ state: "idle", lastCheckedAt: checkedAt });
|
||||
@@ -525,11 +643,17 @@ export function createSystemState(options: {
|
||||
const notes = typeof update.body === "string" ? update.body : undefined;
|
||||
|
||||
if (!(await isUpdateSupportedByDen(update.version))) {
|
||||
logUpdateGateDebug("tauri-update-check-suppressed-by-den", {
|
||||
version: update.version,
|
||||
});
|
||||
setPendingUpdate(null);
|
||||
setUpdateStatus({ state: "idle", lastCheckedAt: checkedAt });
|
||||
return;
|
||||
}
|
||||
|
||||
logUpdateGateDebug("tauri-update-check-allowed-by-den", {
|
||||
version: update.version,
|
||||
});
|
||||
setPendingUpdate({ update, version: update.version, notes });
|
||||
setUpdateStatus({
|
||||
state: "available",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const BUILD_LATEST_APP_VERSION = "0.11.210" as const
|
||||
export const BUILD_LATEST_APP_VERSION = "0.11.212" as const;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Hono } from "hono"
|
||||
import { describeRoute } from "hono-openapi"
|
||||
import { desktopAppRestrictionsSchema } from "@openwork/types/den/desktop-app-restrictions"
|
||||
import { desktopConfigSchema } from "@openwork/types/den/desktop-app-restrictions"
|
||||
import { z } from "zod"
|
||||
import { requireUserMiddleware, resolveOrganizationContextMiddleware, resolveUserOrganizationsMiddleware, type OrganizationContextVariables, type UserOrganizationsContext } from "../../middleware/index.js"
|
||||
import { denTypeIdSchema, jsonResponse, unauthorizedSchema } from "../../openapi.js"
|
||||
import { normalizeOrganizationMetadata } from "../../organization-limits.js"
|
||||
import type { AuthContextVariables } from "../../session.js"
|
||||
|
||||
const meResponseSchema = z.object({
|
||||
@@ -20,7 +21,7 @@ const meOrganizationsResponseSchema = z.object({
|
||||
activeOrgSlug: z.string().nullable(),
|
||||
}).meta({ ref: "CurrentUserOrganizationsResponse" })
|
||||
|
||||
const meDesktopConfigResponseSchema = desktopAppRestrictionsSchema.meta({
|
||||
const meDesktopConfigResponseSchema = desktopConfigSchema.meta({
|
||||
ref: "CurrentUserDesktopConfigResponse",
|
||||
})
|
||||
|
||||
@@ -84,7 +85,15 @@ export function registerMeRoutes<T extends { Variables: AuthContextVariables & P
|
||||
requireUserMiddleware,
|
||||
resolveOrganizationContextMiddleware,
|
||||
(c) => {
|
||||
return c.json(c.get("organizationContext").organization.desktopAppRestrictions)
|
||||
const organization = c.get("organizationContext").organization
|
||||
const metadata = normalizeOrganizationMetadata(organization.metadata).metadata
|
||||
|
||||
return c.json({
|
||||
...organization.desktopAppRestrictions,
|
||||
...(Array.isArray(metadata.allowedDesktopVersions)
|
||||
? { allowedDesktopVersions: metadata.allowedDesktopVersions }
|
||||
: {}),
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,39 @@ export const desktopAppRestrictionsSchema = z.object({
|
||||
|
||||
export type DesktopAppRestrictions = z.infer<typeof desktopAppRestrictionsSchema>
|
||||
|
||||
export const desktopConfigSchema = desktopAppRestrictionsSchema.extend({
|
||||
allowedDesktopVersions: z.array(z.string().trim().min(1).max(32)).optional(),
|
||||
}).meta({ ref: "DenDesktopConfig" })
|
||||
|
||||
export type DesktopConfig = z.infer<typeof desktopConfigSchema>
|
||||
|
||||
function normalizeDesktopVersionString(value: unknown) {
|
||||
if (typeof value !== "string") {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalized = value.trim().replace(/^v/i, "")
|
||||
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(normalized)
|
||||
? normalized
|
||||
: null
|
||||
}
|
||||
|
||||
function normalizeAllowedDesktopVersions(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const versions = [
|
||||
...new Set(
|
||||
value
|
||||
.map((entry) => normalizeDesktopVersionString(entry))
|
||||
.filter((entry): entry is string => Boolean(entry)),
|
||||
),
|
||||
]
|
||||
|
||||
return versions
|
||||
}
|
||||
|
||||
export function normalizeDesktopAppRestrictions(value: unknown): DesktopAppRestrictions {
|
||||
const parsed = desktopAppRestrictionsSchema.safeParse(value)
|
||||
if (parsed.success) {
|
||||
@@ -28,3 +61,15 @@ export function normalizeDesktopAppRestrictions(value: unknown): DesktopAppRestr
|
||||
...(legacy?.models?.removeZen === true ? { blockZenModel: true } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeDesktopConfig(value: unknown): DesktopConfig {
|
||||
const restrictions = normalizeDesktopAppRestrictions(value)
|
||||
const allowedDesktopVersions = normalizeAllowedDesktopVersions(
|
||||
(value as { allowedDesktopVersions?: unknown } | null)?.allowedDesktopVersions,
|
||||
)
|
||||
|
||||
return {
|
||||
...restrictions,
|
||||
...(allowedDesktopVersions !== undefined ? { allowedDesktopVersions } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user