mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
revert(assistant): drop the ChatGPT relay, open-source apps keep the loopback sign-in
OpenAI offers a fixed web redirect for Sign in with ChatGPT only to selected commercial partners; open-source apps sign in through the 127.0.0.1 loopback callback. The relay could never be switched on, so it goes again.
This commit is contained in:
@@ -12,8 +12,6 @@ export interface ChatgptAuthFlow {
|
||||
nonce: string;
|
||||
/** The client id this user's registration was issued, sent instead of registering again. */
|
||||
savedClientId: string | null;
|
||||
/** Where the sign-in ends: the loopback page to paste from, or the relay that comes back by itself. */
|
||||
redirectUri: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAppUrl, runningVersion } from '../../app-config';
|
||||
import { runningVersion } from '../../app-config';
|
||||
import { AiConnections } from '../../db/entities/AiConnections.entity';
|
||||
import { AI_CONNECTION_ACCOUNT, type AiConnectionAccount } from '../../db/json-columns';
|
||||
import type { AiConnectionRow, AiConnectionsRepository } from '../../db/repositories/AiConnections.repository';
|
||||
@@ -11,7 +11,6 @@ import { aiRefusal, providerRefusal } from './ai-connections.constants';
|
||||
import { AiRefreshLock } from './ai-refresh-lock';
|
||||
import { openSecrets, sealSecrets, type ChatgptTokens } from './ai-secrets';
|
||||
import { AiSettingsService } from './ai-settings.service';
|
||||
import { chatgptRelay, isChatgptCallback, relayState } from './chatgpt-return';
|
||||
import { ModelCatalogService, pickDefaultModel } from './model-catalog.service';
|
||||
import { AiProviderError, errorCodeOf } from './providers/provider-errors';
|
||||
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||
@@ -108,7 +107,7 @@ function readPastedCallback(pasted: string): PastedCallback {
|
||||
} catch {
|
||||
throw aiRefusal('wrong_url', 'That is not the address the sign-in ended on');
|
||||
}
|
||||
if (!isChatgptCallback(url, CHATGPT_REDIRECT_URI, getAppUrl())) {
|
||||
if (`${url.origin}${url.pathname}` !== CHATGPT_REDIRECT_URI) {
|
||||
throw aiRefusal('wrong_url', 'That is not the address the sign-in ended on');
|
||||
}
|
||||
const params = url.searchParams;
|
||||
@@ -154,13 +153,9 @@ export class ChatgptAuthService {
|
||||
async start(userId: number): Promise<AiAuthStartResponse> {
|
||||
await this.settings.assertCanConnectOwn(userId);
|
||||
const existing = await this.connections.findForUser(userId);
|
||||
const relay = chatgptRelay(getAppUrl());
|
||||
const registered = existing?.kind === 'chatgpt' ? (this.accountOf(existing).clientId ?? null) : null;
|
||||
// Through the relay every user signs in with the project's client; otherwise each registers their own.
|
||||
const savedClientId = relay?.clientId ?? registered;
|
||||
const redirectUri = relay?.redirectUri ?? CHATGPT_REDIRECT_URI;
|
||||
const savedClientId = existing?.kind === 'chatgpt' ? (this.accountOf(existing).clientId ?? null) : null;
|
||||
const verifier = randomToken();
|
||||
const state = relay ? relayState(randomToken(), relay.returnUrl) : randomToken();
|
||||
const state = randomToken();
|
||||
const nonce = randomToken();
|
||||
const params = new URLSearchParams({
|
||||
client_id: savedClientId ?? DYNAMIC_CLIENT_ID,
|
||||
@@ -168,7 +163,7 @@ export class ChatgptAuthService {
|
||||
...(savedClientId ? {} : { agent_name_hint: 'TREK' }),
|
||||
ext_agent_host_id: await this.settings.siwcHostId(),
|
||||
response_type: 'code',
|
||||
redirect_uri: redirectUri,
|
||||
redirect_uri: CHATGPT_REDIRECT_URI,
|
||||
resource: RESOURCE,
|
||||
scope: SCOPE,
|
||||
state,
|
||||
@@ -178,22 +173,11 @@ export class ChatgptAuthService {
|
||||
});
|
||||
const flowId = randomUUID();
|
||||
const expiresAt = Date.now() + AI_AUTH_FLOW_TTL_MS;
|
||||
const flow = {
|
||||
provider: 'chatgpt' as const,
|
||||
userId,
|
||||
verifier,
|
||||
state,
|
||||
nonce,
|
||||
savedClientId,
|
||||
redirectUri,
|
||||
expiresAt,
|
||||
};
|
||||
await this.flows.put(flowId, flow);
|
||||
await this.flows.put(flowId, { provider: 'chatgpt', userId, verifier, state, nonce, savedClientId, expiresAt });
|
||||
return {
|
||||
flowId,
|
||||
authorizeUrl: `${AUTHORIZE_URL}?${params.toString()}`,
|
||||
expiresAt: new Date(expiresAt).toISOString(),
|
||||
...(relay ? { returns: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,7 +205,7 @@ export class ChatgptAuthService {
|
||||
throw aiRefusal('wrong_url', 'That is not the address the sign-in ended on');
|
||||
}
|
||||
|
||||
const tokens = await this.exchangeCode(clientId, callback.code, flow.verifier, flow.redirectUri);
|
||||
const tokens = await this.exchangeCode(clientId, callback.code, flow.verifier);
|
||||
if (!tokens.id_token || !tokens.refresh_token) {
|
||||
throw aiRefusal('provider_unavailable', 'ChatGPT sent an incomplete answer');
|
||||
}
|
||||
@@ -333,19 +317,14 @@ export class ChatgptAuthService {
|
||||
});
|
||||
}
|
||||
|
||||
private async exchangeCode(
|
||||
clientId: string,
|
||||
code: string,
|
||||
verifier: string,
|
||||
redirectUri: string,
|
||||
): Promise<TokenResponse> {
|
||||
private async exchangeCode(clientId: string, code: string, verifier: string): Promise<TokenResponse> {
|
||||
try {
|
||||
return await this.tokenRequest({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
code,
|
||||
code_verifier: verifier,
|
||||
redirect_uri: redirectUri,
|
||||
redirect_uri: CHATGPT_REDIRECT_URI,
|
||||
resource: RESOURCE,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Sign in with ChatGPT that comes back to TREK by itself. OpenAI sends a
|
||||
* sign-in only to an address registered with the client id; an install has
|
||||
* its own address, so TREK registers one small page (the relay, a static page
|
||||
* of the TREK project) that forwards the browser to the install named in the
|
||||
* sign-in's `state`. The relay sees the code but cannot use it: redeeming it
|
||||
* takes the PKCE verifier only the install that started the sign-in holds.
|
||||
*
|
||||
* Until the project's client id is registered, or for an install without an
|
||||
* https address, the sign-in ends on the loopback page and the user pastes
|
||||
* its address, as before.
|
||||
*/
|
||||
|
||||
/** The client id the TREK project registered with OpenAI for the relay; null until it is. */
|
||||
export const PROJECT_CHATGPT_CLIENT_ID: string | null = null;
|
||||
|
||||
/** The relay page, registered with that client id as its one redirect address. */
|
||||
export const CHATGPT_RELAY_URL = 'https://liketrek.github.io/chatgpt-callback/';
|
||||
|
||||
/** Where a sign-in through the relay lands in the install: the settings, which finish it. */
|
||||
const RETURN_PATH = '/settings';
|
||||
const RETURN_QUERY = '?tab=integrations&ai=chatgpt';
|
||||
|
||||
/** The address the relay sends the browser back to, or null where the install cannot take it. */
|
||||
export function chatgptReturnUrl(appUrl: string): string | null {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(appUrl);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
|
||||
if (url.protocol !== 'https:' && !local) return null;
|
||||
return `${url.origin}${url.pathname.replace(/\/+$/, '')}${RETURN_PATH}${RETURN_QUERY}`;
|
||||
}
|
||||
|
||||
/** The relay to sign in through, or null to keep the pasted address. */
|
||||
export function chatgptRelay(appUrl: string): { clientId: string; redirectUri: string; returnUrl: string } | null {
|
||||
const returnUrl = chatgptReturnUrl(appUrl);
|
||||
if (!PROJECT_CHATGPT_CLIENT_ID || !returnUrl) return null;
|
||||
return { clientId: PROJECT_CHATGPT_CLIENT_ID, redirectUri: CHATGPT_RELAY_URL, returnUrl };
|
||||
}
|
||||
|
||||
/**
|
||||
* The `state` of a sign-in through the relay: the random check value, a dot,
|
||||
* and the address to come back to (base64url). The relay reads only the part
|
||||
* after the dot; the install compares the whole value with the one it kept.
|
||||
*/
|
||||
export function relayState(check: string, returnUrl: string): string {
|
||||
return `${check}.${Buffer.from(returnUrl, 'utf8').toString('base64url')}`;
|
||||
}
|
||||
|
||||
/** Whether a pasted or returned address is one the sign-in can end on: the loopback page or this install's settings. */
|
||||
export function isChatgptCallback(url: URL, loopback: string, appUrl: string): boolean {
|
||||
if (`${url.origin}${url.pathname}` === loopback) return true;
|
||||
const back = chatgptReturnUrl(appUrl);
|
||||
if (!back) return false;
|
||||
const expected = new URL(back);
|
||||
return (
|
||||
url.origin === expected.origin && url.pathname === expected.pathname && url.searchParams.get('ai') === 'chatgpt'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user