import { buildConnectClerkAuthorizeUrl, connectCallbackUrl, connectLoopbackRedirectUri, CONNECT_OAUTH_SCOPES, type ConnectAuthorizeRequest,} from "@t3tools/shared/connectAuth";import { clerkFrontendApiUrlFromPublishableKey } from "@t3tools/shared/relayAuth";import { configuredHostedAppUrl, isHostedStaticApp } from "../hostedPairing";import { hasCloudPublicConfig, resolveCloudPublicConfig, trimNonEmpty } from "./publicConfig";const CONNECT_CLI_AUTH_STATE_STORAGE_KEY = "t3code-connect-cli-auth-state";function resolveConnectCliOAuthClientId(): string | null { return trimNonEmpty(import.meta.env.VITE_CLERK_CLI_OAUTH_CLIENT_ID as string | undefined);}export function hasConnectCliAuthConfig(): boolean { return Boolean( resolveCloudPublicConfig().clerkPublishableKey && resolveConnectCliOAuthClientId(), );}/** * Gate for the /connect routes: the CLI handshake only exists on the hosted * deployment (the same bundle ships inside local instances) and needs the * Clerk CLI OAuth client configured at build time. */export function connectCliAuthRoutesEnabled(): boolean { return isHostedStaticApp() && hasCloudPublicConfig() && hasConnectCliAuthConfig();}/** * Builds the Clerk authorize URL for a CLI-initiated connect request. The * state is mirrored into sessionStorage so the callback page can verify the * response matches a request this browser actually started. * * A request carrying a loopback port came from a CLI with a local callback * listener: the authorization code must return to `127.0.0.1` directly, so * the hosted callback page never sees it. Clerk enforces its registered * redirect URI allowlist either way. */export function buildConnectCliClerkAuthorizeUrl(request: ConnectAuthorizeRequest): string | null { const { clerkPublishableKey } = resolveCloudPublicConfig(); const clientId = resolveConnectCliOAuthClientId(); if (!clerkPublishableKey || !clientId) { return null; } return buildConnectClerkAuthorizeUrl({ authorizationEndpoint: `${clerkFrontendApiUrlFromPublishableKey(clerkPublishableKey)}/oauth/authorize`, clientId, redirectUri: request.loopbackPort === undefined ? connectCallbackUrl(configuredHostedAppUrl()) : connectLoopbackRedirectUri(request.loopbackPort), scopes: CONNECT_OAUTH_SCOPES, state: request.state, challenge: request.challenge, });}/** * Where Clerk sends the browser once the sign-in modal on /connect completes. * It has to be the authorize endpoint rather than this page: /connect carries * the CLI request in its fragment, so navigating back to the same URL is a * same-document fragment navigation the browser never reloads — and Clerk * treats any post-sign-in navigation as a page unload and skips the state emit * that would otherwise re-render the surface, so the session never arrives * either. Falls back to the current URL when the authorize URL cannot be * built, which only happens on a deployment without the CLI OAuth config. */export function connectCliSignInRedirectUrl( request: ConnectAuthorizeRequest, currentHref: string,): string { return buildConnectCliClerkAuthorizeUrl(request) ?? currentHref;}export function rememberConnectCliAuthState(state: string): void { try { window.sessionStorage.setItem(CONNECT_CLI_AUTH_STATE_STORAGE_KEY, state); } catch { // Session storage can be unavailable (e.g. blocked). The callback page // then falls back to trusting the state Clerk echoed back. }}/** * Read-only on purpose: this runs during render, where a removal would be * consumed by React's double-invoked/discarded renders (StrictMode) and * silently disable the state check. The value is not a secret and is * overwritten by the next /connect visit. */export function readConnectCliAuthState(): string | null { try { return window.sessionStorage.getItem(CONNECT_CLI_AUTH_STATE_STORAGE_KEY); } catch { return null; }}export interface ConnectCliCallbackResult { readonly code: string; readonly state: string;}export function readConnectCliCallbackResult( url: URL = new URL(window.location.href),): ConnectCliCallbackResult | null { const code = url.searchParams.get("code")?.trim() ?? ""; const state = url.searchParams.get("state")?.trim() ?? ""; if (!code || !state) { return null; } return { code, state };}
import {
buildConnectClerkAuthorizeUrl,
connectCallbackUrl,
connectLoopbackRedirectUri,
CONNECT_OAUTH_SCOPES,
type ConnectAuthorizeRequest,
} from "@t3tools/shared/connectAuth";
import { clerkFrontendApiUrlFromPublishableKey } from "@t3tools/shared/relayAuth";
import { configuredHostedAppUrl, isHostedStaticApp } from "../hostedPairing";
import { hasCloudPublicConfig, resolveCloudPublicConfig, trimNonEmpty } from "./publicConfig";
const CONNECT_CLI_AUTH_STATE_STORAGE_KEY = "t3code-connect-cli-auth-state";
function resolveConnectCliOAuthClientId(): string | null {
return trimNonEmpty(import.meta.env.VITE_CLERK_CLI_OAUTH_CLIENT_ID as string | undefined);
}
export function hasConnectCliAuthConfig(): boolean {
return Boolean(
resolveCloudPublicConfig().clerkPublishableKey && resolveConnectCliOAuthClientId(),
);
}
/**
* Gate for the /connect routes: the CLI handshake only exists on the hosted
* deployment (the same bundle ships inside local instances) and needs the
* Clerk CLI OAuth client configured at build time.
*/
export function connectCliAuthRoutesEnabled(): boolean {
return isHostedStaticApp() && hasCloudPublicConfig() && hasConnectCliAuthConfig();
}
/**
* Builds the Clerk authorize URL for a CLI-initiated connect request. The
* state is mirrored into sessionStorage so the callback page can verify the
* response matches a request this browser actually started.
*
* A request carrying a loopback port came from a CLI with a local callback
* listener: the authorization code must return to `127.0.0.1` directly, so
* the hosted callback page never sees it. Clerk enforces its registered
* redirect URI allowlist either way.
*/
export function buildConnectCliClerkAuthorizeUrl(request: ConnectAuthorizeRequest): string | null {
const { clerkPublishableKey } = resolveCloudPublicConfig();
const clientId = resolveConnectCliOAuthClientId();
if (!clerkPublishableKey || !clientId) {
return null;
}
return buildConnectClerkAuthorizeUrl({
authorizationEndpoint: `${clerkFrontendApiUrlFromPublishableKey(clerkPublishableKey)}/oauth/authorize`,
clientId,
redirectUri:
request.loopbackPort === undefined
? connectCallbackUrl(configuredHostedAppUrl())
: connectLoopbackRedirectUri(request.loopbackPort),
scopes: CONNECT_OAUTH_SCOPES,
state: request.state,
challenge: request.challenge,
});
}
/**
* Where Clerk sends the browser once the sign-in modal on /connect completes.
* It has to be the authorize endpoint rather than this page: /connect carries
* the CLI request in its fragment, so navigating back to the same URL is a
* same-document fragment navigation the browser never reloads — and Clerk
* treats any post-sign-in navigation as a page unload and skips the state emit
* that would otherwise re-render the surface, so the session never arrives
* either. Falls back to the current URL when the authorize URL cannot be
* built, which only happens on a deployment without the CLI OAuth config.
*/
export function connectCliSignInRedirectUrl(
request: ConnectAuthorizeRequest,
currentHref: string,
): string {
return buildConnectCliClerkAuthorizeUrl(request) ?? currentHref;
}
export function rememberConnectCliAuthState(state: string): void {
try {
window.sessionStorage.setItem(CONNECT_CLI_AUTH_STATE_STORAGE_KEY, state);
} catch {
// Session storage can be unavailable (e.g. blocked). The callback page
// then falls back to trusting the state Clerk echoed back.
}
}
/**
* Read-only on purpose: this runs during render, where a removal would be
* consumed by React's double-invoked/discarded renders (StrictMode) and
* silently disable the state check. The value is not a secret and is
* overwritten by the next /connect visit.
*/
export function readConnectCliAuthState(): string | null {
try {
return window.sessionStorage.getItem(CONNECT_CLI_AUTH_STATE_STORAGE_KEY);
} catch {
return null;
}
}
export interface ConnectCliCallbackResult {
readonly code: string;
readonly state: string;
}
export function readConnectCliCallbackResult(
url: URL = new URL(window.location.href),
): ConnectCliCallbackResult | null {
const code = url.searchParams.get("code")?.trim() ?? "";
const state = url.searchParams.get("state")?.trim() ?? "";
if (!code || !state) {
return null;
}
return { code, state };
}