Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 59 additions & 25 deletions apps/app/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,19 @@ import {
readStoredFontZoom,
} from "./lib/font-zoom";
import {
createOpenworkServerClient,
parseOpenworkWorkspaceIdFromUrl,
readOpenworkConnectInviteFromSearch,
stripOpenworkConnectInviteFromUrl,
hydrateOpenworkServerSettingsFromEnv,
normalizeOpenworkServerHostUrl,
normalizeOpenworkServerUrl,
readOpenworkServerSettings,
writeOpenworkServerSettings,
type OpenworkServerDiagnostics,
type OpenworkServerSettings,
} from "./lib/openwork-server";
import { createDenClient } from "./lib/den";
import {
parseBundleDeepLink,
stripBundleQuery,
Expand Down Expand Up @@ -251,13 +254,54 @@ export default function App() {
const invite = readOpenworkConnectInviteFromSearch(window.location.search);
const bundleInvite = parseBundleDeepLink(window.location.href);

if (!invite) {
setOpenworkServerSettings(stored);
} else {
if (bundleInvite?.bundleUrl) {
bundlesStore.queueBundleLink(window.location.href);
}

const cleanedConnect = stripOpenworkConnectInviteFromUrl(window.location.href);
const cleaned = stripBundleQuery(cleanedConnect) ?? cleanedConnect;
if (cleaned !== window.location.href) {
window.history.replaceState(window.history.state ?? null, "", cleaned);
}

void (async () => {
if (!invite) {
setOpenworkServerSettings(stored);
return;
}

let inviteUrl = invite.url;
let inviteToken = stored.token;

if (!invite.grant && /(?:^|[?&])ow_token=/.test(window.location.search)) {
setError("Legacy OpenWork invite links with embedded tokens are no longer supported. Create a new one-time connect link.");
setOpenworkServerSettings(stored);
return;
}

if (invite.grant) {
try {
const exchange = invite.denBaseUrl
? await createDenClient({ baseUrl: invite.denBaseUrl }).exchangeWorkerConnectGrant(invite.grant)
: await createOpenworkServerClient({
baseUrl: normalizeOpenworkServerHostUrl(invite.url) ?? invite.url,
}).exchangeConnectGrant(invite.grant);
inviteUrl = exchange.openworkUrl?.trim() || inviteUrl;
inviteToken = exchange.token?.trim() || inviteToken;
if (!inviteToken) {
throw new Error("This OpenWork connect link did not return a usable token.");
}
} catch (error) {
setError(error instanceof Error ? error.message : "Failed to redeem the OpenWork connect link.");
setOpenworkServerSettings(stored);
return;
}
}

const merged: OpenworkServerSettings = {
...stored,
urlOverride: invite.url,
token: invite.token ?? stored.token,
urlOverride: inviteUrl,
token: inviteToken,
};

const next = writeOpenworkServerSettings(merged);
Expand All @@ -267,27 +311,17 @@ export default function App() {
setStartupPreference("server");
setOnboardingStep("server");
}
}

if (bundleInvite?.bundleUrl) {
bundlesStore.queueBundleLink(window.location.href);
}

if (invite?.autoConnect) {
deepLinks.queueRemoteConnectDefaults({
openworkHostUrl: invite.url,
openworkToken: invite.token ?? null,
directory: null,
displayName: null,
autoConnect: true,
});
}

const cleanedConnect = stripOpenworkConnectInviteFromUrl(window.location.href);
const cleaned = stripBundleQuery(cleanedConnect) ?? cleanedConnect;
if (cleaned !== window.location.href) {
window.history.replaceState(window.history.state ?? null, "", cleaned);
}
if (invite.autoConnect) {
deepLinks.queueRemoteConnectDefaults({
openworkHostUrl: inviteUrl,
openworkToken: inviteToken ?? null,
directory: null,
displayName: null,
autoConnect: true,
});
}
})();
});

createEffect(() => {
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/app/connections/openwork-server-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type OpenworkServerStore = ReturnType<typeof createOpenworkServerStore>;
type RemoteWorkspaceInput = {
openworkHostUrl: string;
openworkToken?: string | null;
openworkConnectGrant?: string | null;
denBaseUrl?: string | null;
directory?: string | null;
displayName?: string | null;
};
Expand Down
31 changes: 29 additions & 2 deletions apps/app/src/app/context/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ import { blueprintMaterializedSessions, blueprintSessions, defaultBlueprintSessi
import {
buildOpenworkWorkspaceBaseUrl,
createOpenworkServerClient,
normalizeOpenworkServerHostUrl,
normalizeOpenworkServerUrl,
parseOpenworkWorkspaceIdFromUrl,
OpenworkServerError,
type OpenworkServerClient,
type OpenworkWorkspaceInfo,
} from "../lib/openwork-server";
import { createDenClient } from "../lib/den";
import { downloadDir, homeDir } from "@tauri-apps/api/path";
import {
engineDoctor,
Expand Down Expand Up @@ -2468,6 +2470,8 @@ export function createWorkspaceStore(options: {
async function createRemoteWorkspaceFlow(input: {
openworkHostUrl?: string | null;
openworkToken?: string | null;
openworkConnectGrant?: string | null;
denBaseUrl?: string | null;
openworkClientToken?: string | null;
openworkHostToken?: string | null;
directory?: string | null;
Expand All @@ -2489,8 +2493,10 @@ export function createWorkspaceStore(options: {
}

const run = (async () => {
const hostUrl = normalizeOpenworkServerUrl(input.openworkHostUrl ?? "") ?? "";
const token = input.openworkToken?.trim() ?? "";
let hostUrl = normalizeOpenworkServerUrl(input.openworkHostUrl ?? "") ?? "";
let token = input.openworkToken?.trim() ?? "";
const connectGrant = input.openworkConnectGrant?.trim() ?? "";
const denBaseUrl = input.denBaseUrl?.trim() ?? "";
const directory = input.directory?.trim() ?? "";
const displayName = input.displayName?.trim() || null;

Expand All @@ -2499,6 +2505,27 @@ export function createWorkspaceStore(options: {
return false;
}

if (connectGrant) {
try {
const exchange = denBaseUrl
? await createDenClient({ baseUrl: denBaseUrl }).exchangeWorkerConnectGrant(connectGrant)
: await createOpenworkServerClient({
baseUrl: normalizeOpenworkServerHostUrl(hostUrl) ?? hostUrl,
}).exchangeConnectGrant(connectGrant);
hostUrl = normalizeOpenworkServerUrl(exchange.openworkUrl ?? hostUrl) ?? hostUrl;
token = exchange.token?.trim() ?? "";
} catch (error) {
const message = error instanceof Error ? error.message : safeStringify(error);
options.setError(addOpencodeCacheHint(message));
return false;
}

if (!token) {
options.setError("Remote connect link did not return a usable token.");
return false;
}
}

options.setError(null);
console.log("[workspace] create remote request", {
hostUrl: hostUrl || null,
Expand Down
29 changes: 29 additions & 0 deletions apps/app/src/app/lib/den.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export type DenWorkerTokens = {
hostToken: string | null;
openworkUrl: string | null;
workspaceId: string | null;
connectGrant: string | null;
connectGrantExpiresAt: string | null;
connectGrantDenBaseUrl: string | null;
};

export type DenTemplateCreator = {
Expand Down Expand Up @@ -134,6 +137,14 @@ export type DenDesktopHandoffExchange = {
token: string | null;
};

export type DenWorkerConnectGrantExchange = {
openworkUrl: string | null;
workspaceId: string | null;
token: string | null;
workerId: string | null;
workerName: string | null;
};

type RawJsonResponse<T> = {
ok: boolean;
status: number;
Expand Down Expand Up @@ -442,6 +453,9 @@ function getWorkerTokens(payload: unknown): DenWorkerTokens | null {
hostToken: typeof tokens.host === "string" ? tokens.host : null,
openworkUrl: connect && typeof connect.openworkUrl === "string" ? connect.openworkUrl : null,
workspaceId: connect && typeof connect.workspaceId === "string" ? connect.workspaceId : null,
connectGrant: connect && typeof connect.grant === "string" ? connect.grant : null,
connectGrantExpiresAt: connect && typeof connect.expiresAt === "string" ? connect.expiresAt : null,
connectGrantDenBaseUrl: connect && typeof connect.denBaseUrl === "string" ? connect.denBaseUrl : null,
};
}

Expand Down Expand Up @@ -723,6 +737,21 @@ export function createDenClient(options: { baseUrl: string; token?: string | nul
return { user: getUser(payload), token: getToken(payload) };
},

async exchangeWorkerConnectGrant(grant: string): Promise<DenWorkerConnectGrantExchange> {
const payload = await requestJson<unknown>(baseUrls, "/v1/workers/connect-grant/exchange", {
method: "POST",
body: { grant },
});

return {
openworkUrl: isRecord(payload) && typeof payload.openworkUrl === "string" ? payload.openworkUrl : null,
workspaceId: isRecord(payload) && typeof payload.workspaceId === "string" ? payload.workspaceId : null,
token: getToken(payload),
workerId: isRecord(payload) && typeof payload.workerId === "string" ? payload.workerId : null,
workerName: isRecord(payload) && typeof payload.workerName === "string" ? payload.workerName : null,
};
},

async listOrgs(): Promise<{ orgs: DenOrgSummary[]; defaultOrgId: string | null }> {
const payload = await requestJson<unknown>(baseUrls, "/v1/me/orgs", {
method: "GET",
Expand Down
48 changes: 44 additions & 4 deletions apps/app/src/app/lib/openwork-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { BundleRequest } from "../bundles/types";
export type RemoteWorkspaceDefaults = {
openworkHostUrl?: string | null;
openworkToken?: string | null;
openworkConnectGrant?: string | null;
denBaseUrl?: string | null;
directory?: string | null;
displayName?: string | null;
autoConnect?: boolean;
Expand Down Expand Up @@ -43,10 +45,11 @@ export function parseRemoteConnectDeepLink(rawUrl: string): RemoteWorkspaceDefau
}

const hostUrlRaw = url.searchParams.get("openworkHostUrl") ?? url.searchParams.get("openworkUrl") ?? "";
const tokenRaw = url.searchParams.get("openworkToken") ?? url.searchParams.get("accessToken") ?? "";
const grantRaw = url.searchParams.get("grant") ?? "";
const denBaseUrl = normalizeDenBaseUrl(url.searchParams.get("denBaseUrl")?.trim() ?? "");
const normalizedHostUrl = normalizeOpenworkServerUrl(hostUrlRaw);
const token = tokenRaw.trim();
if (!normalizedHostUrl || !token) {
const grant = grantRaw.trim();
if (!normalizedHostUrl || !grant) {
return null;
}

Expand All @@ -62,13 +65,48 @@ export function parseRemoteConnectDeepLink(rawUrl: string): RemoteWorkspaceDefau

return {
openworkHostUrl: normalizedHostUrl,
openworkToken: token,
openworkToken: null,
openworkConnectGrant: grant,
denBaseUrl: denBaseUrl ?? null,
directory: null,
displayName: displayName || null,
autoConnect,
};
}

export function buildRemoteConnectDeepLink(input: {
openworkHostUrl: string;
grant: string;
denBaseUrl?: string | null;
displayName?: string | null;
workspaceId?: string | null;
autoConnect?: boolean;
}) {
const hostUrl = normalizeOpenworkServerUrl(input.openworkHostUrl ?? "");
const grant = input.grant.trim();
if (!hostUrl || !grant) return null;

const url = new URL("openwork://connect-remote");
url.searchParams.set("openworkHostUrl", hostUrl);
url.searchParams.set("grant", grant);
const denBaseUrl = normalizeDenBaseUrl(input.denBaseUrl ?? "");
if (denBaseUrl) {
url.searchParams.set("denBaseUrl", denBaseUrl);
}
if (input.autoConnect) {
url.searchParams.set("autoConnect", "1");
}
const workspaceId = input.workspaceId?.trim() ?? "";
if (workspaceId) {
url.searchParams.set("workerId", workspaceId);
}
const displayName = input.displayName?.trim() ?? "";
if (displayName) {
url.searchParams.set("workerName", displayName);
}
return url.toString();
}

export function stripRemoteConnectQuery(rawUrl: string): string | null {
let url: URL;
try {
Expand All @@ -81,6 +119,8 @@ export function stripRemoteConnectQuery(rawUrl: string): string | null {
for (const key of [
"openworkHostUrl",
"openworkUrl",
"grant",
"denBaseUrl",
"openworkToken",
"accessToken",
"workerId",
Expand Down
Loading
Loading