mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-11 07:12:55 +02:00
Better external OAuth callback format
This commit is contained in:
+30
-6
@@ -18,7 +18,12 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
|||||||
|
|
||||||
export type EncryptedKey = { encryptedKey: string, };
|
export type EncryptedKey = { encryptedKey: string, };
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||||
|
/**
|
||||||
|
* Variables defined in this environment scope.
|
||||||
|
* Child environments override parent variables by name.
|
||||||
|
*/
|
||||||
|
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
@@ -34,9 +39,17 @@ export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, up
|
|||||||
|
|
||||||
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
||||||
|
|
||||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
|
||||||
|
/**
|
||||||
|
* Server URL (http for plaintext or https for secure)
|
||||||
|
*/
|
||||||
|
url: string, };
|
||||||
|
|
||||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
||||||
|
/**
|
||||||
|
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||||
|
*/
|
||||||
|
urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
@@ -55,11 +68,18 @@ export type HttpResponseHeader = { name: string, value: string, };
|
|||||||
|
|
||||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||||
|
|
||||||
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpUrlParameter = { enabled?: boolean,
|
||||||
|
/**
|
||||||
|
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||||
|
* Other entries are appended as query parameters
|
||||||
|
*/
|
||||||
|
name: string, value: string, id?: string, };
|
||||||
|
|
||||||
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
||||||
|
|
||||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
|
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
|
||||||
|
|
||||||
|
export type PluginSource = "bundled" | "filesystem" | "registry";
|
||||||
|
|
||||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
||||||
|
|
||||||
@@ -77,7 +97,11 @@ export type WebsocketEvent = { model: "websocket_event", id: string, createdAt:
|
|||||||
|
|
||||||
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
|
||||||
|
/**
|
||||||
|
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||||
|
*/
|
||||||
|
urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import type { Context } from '@yaakapp/api';
|
import type { Context } from '@yaakapp/api';
|
||||||
|
|
||||||
export const HOSTED_CALLBACK_URL = 'https://oauth.yaak.app/redirect';
|
export const HOSTED_CALLBACK_URL_BASE = 'https://oauth.yaak.app/redirect';
|
||||||
export const DEFAULT_LOCALHOST_PORT = 8765;
|
export const DEFAULT_LOCALHOST_PORT = 8765;
|
||||||
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
@@ -176,12 +176,15 @@ export function startCallbackServer(options: {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the redirect URI for the hosted callback page.
|
* Build the redirect URI for the hosted callback page.
|
||||||
* The hosted page will redirect to the local server with the OAuth response.
|
* The port is encoded in the URL path so the hosted page can redirect
|
||||||
|
* to the local server without relying on query params (which some OAuth
|
||||||
|
* providers strip). The default port is omitted for a cleaner URL.
|
||||||
*/
|
*/
|
||||||
export function buildHostedCallbackRedirectUri(localPort: number, localPath: string): string {
|
export function buildHostedCallbackRedirectUri(localPort: number): string {
|
||||||
const localRedirectUri = `http://127.0.0.1:${localPort}${localPath}`;
|
if (localPort === DEFAULT_LOCALHOST_PORT) {
|
||||||
// The hosted callback page will read params and redirect to the local server
|
return HOSTED_CALLBACK_URL_BASE;
|
||||||
return `${HOSTED_CALLBACK_URL}?redirect_to=${encodeURIComponent(localRedirectUri)}`;
|
}
|
||||||
|
return `${HOSTED_CALLBACK_URL_BASE}/${localPort}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,14 +216,9 @@ export async function getRedirectUrlViaExternalBrowser(
|
|||||||
): Promise<{ callbackUrl: string; redirectUri: string }> {
|
): Promise<{ callbackUrl: string; redirectUri: string }> {
|
||||||
const { callbackType, callbackPort } = options;
|
const { callbackType, callbackPort } = options;
|
||||||
|
|
||||||
// Determine port based on callback type:
|
const port = callbackPort ?? DEFAULT_LOCALHOST_PORT;
|
||||||
// - localhost: use specified port or default stable port
|
|
||||||
// - hosted: use random port (0) since hosted page redirects to local
|
|
||||||
const port = callbackType === 'localhost' ? (callbackPort ?? DEFAULT_LOCALHOST_PORT) : 0;
|
|
||||||
|
|
||||||
console.log(
|
console.log(`[oauth2] Starting callback server (type: ${callbackType}, port: ${port})`);
|
||||||
`[oauth2] Starting callback server (type: ${callbackType}, port: ${port || 'random'})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const server = await startCallbackServer({
|
const server = await startCallbackServer({
|
||||||
port,
|
port,
|
||||||
@@ -232,7 +230,7 @@ export async function getRedirectUrlViaExternalBrowser(
|
|||||||
let oauthRedirectUri: string;
|
let oauthRedirectUri: string;
|
||||||
|
|
||||||
if (callbackType === 'hosted') {
|
if (callbackType === 'hosted') {
|
||||||
oauthRedirectUri = buildHostedCallbackRedirectUri(server.port, '/callback');
|
oauthRedirectUri = buildHostedCallbackRedirectUri(server.port);
|
||||||
console.log('[oauth2] Using hosted callback redirect:', oauthRedirectUri);
|
console.log('[oauth2] Using hosted callback redirect:', oauthRedirectUri);
|
||||||
} else {
|
} else {
|
||||||
oauthRedirectUri = server.redirectUri;
|
oauthRedirectUri = server.redirectUri;
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import type {
|
|||||||
PluginDefinition,
|
PluginDefinition,
|
||||||
} from '@yaakapp/api';
|
} from '@yaakapp/api';
|
||||||
import type { Algorithm } from 'jsonwebtoken';
|
import type { Algorithm } from 'jsonwebtoken';
|
||||||
import { DEFAULT_LOCALHOST_PORT, HOSTED_CALLBACK_URL, stopActiveServer } from './callbackServer';
|
import {
|
||||||
|
buildHostedCallbackRedirectUri,
|
||||||
|
DEFAULT_LOCALHOST_PORT,
|
||||||
|
HOSTED_CALLBACK_URL_BASE,
|
||||||
|
stopActiveServer,
|
||||||
|
} from './callbackServer';
|
||||||
import {
|
import {
|
||||||
type CallbackType,
|
type CallbackType,
|
||||||
DEFAULT_PKCE_METHOD,
|
DEFAULT_PKCE_METHOD,
|
||||||
@@ -300,8 +305,7 @@ export const plugin: PluginDefinition = {
|
|||||||
optional: true,
|
optional: true,
|
||||||
dynamic: hiddenIfNot(
|
dynamic: hiddenIfNot(
|
||||||
['authorization_code', 'implicit'],
|
['authorization_code', 'implicit'],
|
||||||
({ useExternalBrowser, callbackType }) =>
|
({ useExternalBrowser }) => !!useExternalBrowser,
|
||||||
!!useExternalBrowser && callbackType === 'localhost',
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -328,11 +332,11 @@ export const plugin: PluginDefinition = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compute the redirect URI based on callback type
|
// Compute the redirect URI based on callback type
|
||||||
|
const port = intArg(values, 'callbackPort') || DEFAULT_LOCALHOST_PORT;
|
||||||
let redirectUri: string;
|
let redirectUri: string;
|
||||||
if (callbackType === 'hosted') {
|
if (callbackType === 'hosted') {
|
||||||
redirectUri = HOSTED_CALLBACK_URL;
|
redirectUri = buildHostedCallbackRedirectUri(port);
|
||||||
} else {
|
} else {
|
||||||
const port = intArg(values, 'callbackPort') || DEFAULT_LOCALHOST_PORT;
|
|
||||||
redirectUri = `http://127.0.0.1:${port}/callback`;
|
redirectUri = `http://127.0.0.1:${port}/callback`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user