mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-09-26 15:39:09 +00:00
feat: implement basic oauth api
This commit is contained in:
parent
887838bec6
commit
5b0dfd7b36
8 changed files with 497 additions and 9 deletions
|
@ -110,7 +110,7 @@ export type SidecarMessage = z.infer<typeof SidecarMessageSchema>;
|
||||||
|
|
||||||
export const PreferenceSchema = z.object({
|
export const PreferenceSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
title: z.string(),
|
title: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
type: z.enum(['textfield', 'dropdown', 'checkbox', 'directory']),
|
type: z.enum(['textfield', 'dropdown', 'checkbox', 'directory']),
|
||||||
required: z.boolean().optional(),
|
required: z.boolean().optional(),
|
||||||
|
@ -249,6 +249,45 @@ const ClipboardClearMessageSchema = z.object({
|
||||||
payload: ClipboardClearPayloadSchema
|
payload: ClipboardClearPayloadSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const OauthAuthorizePayloadSchema = z.object({
|
||||||
|
url: z.string(),
|
||||||
|
providerName: z.string(),
|
||||||
|
providerIcon: z.string().optional(),
|
||||||
|
description: z.string().optional()
|
||||||
|
});
|
||||||
|
const OauthAuthorizeMessageSchema = z.object({
|
||||||
|
type: z.literal('oauth-authorize'),
|
||||||
|
payload: OauthAuthorizePayloadSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
const OauthGetTokensPayloadSchema = z.object({
|
||||||
|
requestId: z.string(),
|
||||||
|
providerId: z.string()
|
||||||
|
});
|
||||||
|
const OauthGetTokensMessageSchema = z.object({
|
||||||
|
type: z.literal('oauth-get-tokens'),
|
||||||
|
payload: OauthGetTokensPayloadSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
const OauthSetTokensPayloadSchema = z.object({
|
||||||
|
requestId: z.string(),
|
||||||
|
providerId: z.string(),
|
||||||
|
tokens: z.record(z.string(), z.unknown())
|
||||||
|
});
|
||||||
|
const OauthSetTokensMessageSchema = z.object({
|
||||||
|
type: z.literal('oauth-set-tokens'),
|
||||||
|
payload: OauthSetTokensPayloadSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
const OauthRemoveTokensPayloadSchema = z.object({
|
||||||
|
requestId: z.string(),
|
||||||
|
providerId: z.string()
|
||||||
|
});
|
||||||
|
const OauthRemoveTokensMessageSchema = z.object({
|
||||||
|
type: z.literal('oauth-remove-tokens'),
|
||||||
|
payload: OauthRemoveTokensPayloadSchema
|
||||||
|
});
|
||||||
|
|
||||||
export const SidecarMessageWithPluginsSchema = z.union([
|
export const SidecarMessageWithPluginsSchema = z.union([
|
||||||
BatchUpdateSchema,
|
BatchUpdateSchema,
|
||||||
CommandSchema,
|
CommandSchema,
|
||||||
|
@ -264,6 +303,10 @@ export const SidecarMessageWithPluginsSchema = z.union([
|
||||||
ClipboardPasteMessageSchema,
|
ClipboardPasteMessageSchema,
|
||||||
ClipboardReadMessageSchema,
|
ClipboardReadMessageSchema,
|
||||||
ClipboardReadTextMessageSchema,
|
ClipboardReadTextMessageSchema,
|
||||||
ClipboardClearMessageSchema
|
ClipboardClearMessageSchema,
|
||||||
|
OauthAuthorizeMessageSchema,
|
||||||
|
OauthGetTokensMessageSchema,
|
||||||
|
OauthSetTokensMessageSchema,
|
||||||
|
OauthRemoveTokensMessageSchema
|
||||||
]);
|
]);
|
||||||
export type SidecarMessageWithPlugins = z.infer<typeof SidecarMessageWithPluginsSchema>;
|
export type SidecarMessageWithPlugins = z.infer<typeof SidecarMessageWithPluginsSchema>;
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { preferencesStore } from '../preferences';
|
||||||
import { showToast } from './toast';
|
import { showToast } from './toast';
|
||||||
import { BrowserExtensionAPI } from './browserExtension';
|
import { BrowserExtensionAPI } from './browserExtension';
|
||||||
import { Clipboard } from './clipboard';
|
import { Clipboard } from './clipboard';
|
||||||
|
import * as OAuth from './oauth';
|
||||||
|
|
||||||
let currentPluginName: string | null = null;
|
let currentPluginName: string | null = null;
|
||||||
let currentPluginPreferences: Array<{
|
let currentPluginPreferences: Array<{
|
||||||
|
@ -45,6 +46,7 @@ export const getRaycastApi = () => {
|
||||||
Icon,
|
Icon,
|
||||||
LaunchType,
|
LaunchType,
|
||||||
Toast,
|
Toast,
|
||||||
|
OAuth,
|
||||||
Action,
|
Action,
|
||||||
ActionPanel,
|
ActionPanel,
|
||||||
Detail,
|
Detail,
|
||||||
|
|
279
sidecar/src/api/oauth.ts
Normal file
279
sidecar/src/api/oauth.ts
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import { writeOutput, writeLog } from '../io';
|
||||||
|
|
||||||
|
export enum RedirectMethod {
|
||||||
|
Web = 'web',
|
||||||
|
App = 'app',
|
||||||
|
AppURI = 'app-uri'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PKCEClientOptions {
|
||||||
|
redirectMethod: RedirectMethod;
|
||||||
|
providerName: string;
|
||||||
|
providerIcon?: string;
|
||||||
|
description?: string;
|
||||||
|
providerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizationRequestOptions {
|
||||||
|
endpoint: string;
|
||||||
|
clientId: string;
|
||||||
|
scope: string;
|
||||||
|
extraParameters?: { [key: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizationRequest {
|
||||||
|
url: string;
|
||||||
|
codeVerifier: string;
|
||||||
|
codeChallenge: string;
|
||||||
|
redirectURI: string;
|
||||||
|
state: string;
|
||||||
|
toURL: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizationOptions {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizationResponse {
|
||||||
|
authorizationCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenResponse {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
expires_in?: number;
|
||||||
|
scope?: string;
|
||||||
|
id_token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenSetOptions {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
expiresIn?: number;
|
||||||
|
scope?: string;
|
||||||
|
idToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenSet {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
expiresIn?: number;
|
||||||
|
scope?: string;
|
||||||
|
idToken?: string;
|
||||||
|
updatedAt: Date;
|
||||||
|
isExpired: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingAuthorizationRequests = new Map<
|
||||||
|
string,
|
||||||
|
{ resolve: (value: AuthorizationResponse) => void; reject: (reason?: any) => void }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const pendingTokenRequests = new Map<
|
||||||
|
string,
|
||||||
|
{ resolve: (value: any) => void; reject: (reason?: any) => void }
|
||||||
|
>();
|
||||||
|
|
||||||
|
export function handleOAuthResponse(
|
||||||
|
_requestId: string,
|
||||||
|
code: string,
|
||||||
|
state: string,
|
||||||
|
error?: string
|
||||||
|
) {
|
||||||
|
const promise = pendingAuthorizationRequests.get(state);
|
||||||
|
if (promise) {
|
||||||
|
if (error) {
|
||||||
|
promise.reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
promise.resolve({ authorizationCode: code });
|
||||||
|
}
|
||||||
|
pendingAuthorizationRequests.delete(state);
|
||||||
|
} else {
|
||||||
|
writeLog(`OAuth state mismatch. Request ID (state): ${state} not found in pending requests.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleTokenResponse(requestId: string, result: any, error?: string) {
|
||||||
|
const promise = pendingTokenRequests.get(requestId);
|
||||||
|
if (promise) {
|
||||||
|
if (error) {
|
||||||
|
promise.reject(new Error(error));
|
||||||
|
} else {
|
||||||
|
promise.resolve(result);
|
||||||
|
}
|
||||||
|
pendingTokenRequests.delete(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendTokenRequest<T>(type: string, payload: object): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const requestId = crypto.randomUUID();
|
||||||
|
pendingTokenRequests.set(requestId, { resolve, reject });
|
||||||
|
writeOutput({
|
||||||
|
type,
|
||||||
|
payload: { requestId, ...payload }
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingTokenRequests.has(requestId)) {
|
||||||
|
pendingTokenRequests.delete(requestId);
|
||||||
|
reject(new Error(`Token request for ${type} timed out`));
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PKCEClient {
|
||||||
|
private options: PKCEClientOptions;
|
||||||
|
|
||||||
|
constructor(options: PKCEClientOptions) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getProviderId(): string {
|
||||||
|
return this.options.providerId ?? this.options.providerName.toLowerCase().replace(/\s/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
async authorizationRequest(options: AuthorizationRequestOptions): Promise<AuthorizationRequest> {
|
||||||
|
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
||||||
|
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
||||||
|
const state = JSON.stringify({
|
||||||
|
providerName: this.options.providerName,
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
flavor: 'release'
|
||||||
|
});
|
||||||
|
|
||||||
|
let redirectURI: string;
|
||||||
|
const packageName = 'Extension'; // TODO: what does this mean, and is it always the same?
|
||||||
|
switch (this.options.redirectMethod) {
|
||||||
|
case RedirectMethod.Web:
|
||||||
|
redirectURI = `https://raycast.com/redirect?packageName=${packageName}`;
|
||||||
|
break;
|
||||||
|
case RedirectMethod.App:
|
||||||
|
redirectURI = `raycast://oauth?package_name=${packageName}`;
|
||||||
|
break;
|
||||||
|
case RedirectMethod.AppURI:
|
||||||
|
redirectURI = `com.raycast:/oauth?package_name=${packageName}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams({
|
||||||
|
response_type: 'code',
|
||||||
|
client_id: options.clientId,
|
||||||
|
scope: options.scope,
|
||||||
|
redirect_uri: redirectURI,
|
||||||
|
state: state,
|
||||||
|
code_challenge: codeChallenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
...options.extraParameters
|
||||||
|
});
|
||||||
|
|
||||||
|
const authRequest: AuthorizationRequest = {
|
||||||
|
url: `${options.endpoint}?${urlParams.toString()}`,
|
||||||
|
codeVerifier,
|
||||||
|
codeChallenge,
|
||||||
|
redirectURI,
|
||||||
|
state,
|
||||||
|
toURL: () => authRequest.url
|
||||||
|
};
|
||||||
|
|
||||||
|
return authRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async authorize(
|
||||||
|
authRequest: AuthorizationRequest | AuthorizationOptions
|
||||||
|
): Promise<AuthorizationResponse> {
|
||||||
|
const state =
|
||||||
|
'state' in authRequest
|
||||||
|
? authRequest.state
|
||||||
|
: new URL(authRequest.url).searchParams.get('state');
|
||||||
|
|
||||||
|
if (!state) {
|
||||||
|
throw new Error('State parameter is missing from authorization request.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pendingAuthorizationRequests.set(state, { resolve, reject });
|
||||||
|
|
||||||
|
writeOutput({
|
||||||
|
type: 'oauth-authorize',
|
||||||
|
payload: {
|
||||||
|
url: authRequest.url,
|
||||||
|
providerName: this.options.providerName,
|
||||||
|
providerIcon: this.options.providerIcon,
|
||||||
|
description: this.options.description
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
if (pendingAuthorizationRequests.has(state)) {
|
||||||
|
pendingAuthorizationRequests.delete(state);
|
||||||
|
reject(new Error('OAuth authorization timed out'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
5 * 60 * 1000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTokens(): Promise<TokenSet | undefined> {
|
||||||
|
const tokenData = await sendTokenRequest<any | undefined>('oauth-get-tokens', {
|
||||||
|
providerId: this.getProviderId()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAt = new Date(tokenData.updatedAt);
|
||||||
|
const expiresIn = tokenData.expiresIn;
|
||||||
|
|
||||||
|
const tokenSet: TokenSet = {
|
||||||
|
...tokenData,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
isExpired: () => {
|
||||||
|
if (!expiresIn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
const expiryDate = new Date(updatedAt.getTime() + expiresIn * 1000);
|
||||||
|
return now.getTime() > expiryDate.getTime() - 60000;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return tokenSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setTokens(tokens: TokenSetOptions | TokenResponse): Promise<void> {
|
||||||
|
let tokenSetOptions: TokenSetOptions;
|
||||||
|
|
||||||
|
if ('access_token' in tokens) {
|
||||||
|
tokenSetOptions = {
|
||||||
|
accessToken: tokens.access_token,
|
||||||
|
refreshToken: tokens.refresh_token,
|
||||||
|
expiresIn: tokens.expires_in,
|
||||||
|
scope: tokens.scope,
|
||||||
|
idToken: tokens.id_token
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
tokenSetOptions = tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...tokenSetOptions,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendTokenRequest<void>('oauth-set-tokens', {
|
||||||
|
providerId: this.getProviderId(),
|
||||||
|
tokens: payload
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTokens(): Promise<void> {
|
||||||
|
await sendTokenRequest<void>('oauth-remove-tokens', {
|
||||||
|
providerId: this.getProviderId()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from './api/environment';
|
} from './api/environment';
|
||||||
import { handleBrowserExtensionResponse } from './api/browserExtension';
|
import { handleBrowserExtensionResponse } from './api/browserExtension';
|
||||||
import { handleClipboardResponse } from './api/clipboard';
|
import { handleClipboardResponse } from './api/clipboard';
|
||||||
|
import { handleOAuthResponse, handleTokenResponse } from './api/oauth';
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason: unknown) => {
|
process.on('unhandledRejection', (reason: unknown) => {
|
||||||
writeLog(`--- UNHANDLED PROMISE REJECTION ---`);
|
writeLog(`--- UNHANDLED PROMISE REJECTION ---`);
|
||||||
|
@ -151,6 +152,26 @@ rl.on('line', (line) => {
|
||||||
browserExtensionState.isConnected = isConnected;
|
browserExtensionState.isConnected = isConnected;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'oauth-authorize-response': {
|
||||||
|
const { code, state, error } = command.payload as {
|
||||||
|
code: string;
|
||||||
|
state: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
handleOAuthResponse(state, code, state, error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'oauth-get-tokens-response':
|
||||||
|
case 'oauth-set-tokens-response':
|
||||||
|
case 'oauth-remove-tokens-response': {
|
||||||
|
const { requestId, result, error } = command.payload as {
|
||||||
|
requestId: string;
|
||||||
|
result?: any;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
handleTokenResponse(requestId, result, error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'clipboard-read-text-response':
|
case 'clipboard-read-text-response':
|
||||||
case 'clipboard-read-response':
|
case 'clipboard-read-response':
|
||||||
case 'clipboard-copy-response':
|
case 'clipboard-copy-response':
|
||||||
|
|
|
@ -6,6 +6,7 @@ mod desktop;
|
||||||
mod error;
|
mod error;
|
||||||
mod extensions;
|
mod extensions;
|
||||||
mod filesystem;
|
mod filesystem;
|
||||||
|
mod oauth;
|
||||||
|
|
||||||
use crate::{app::App, cache::AppCache};
|
use crate::{app::App, cache::AppCache};
|
||||||
use browser_extension::WsState;
|
use browser_extension::WsState;
|
||||||
|
@ -135,7 +136,10 @@ pub fn run() {
|
||||||
clipboard::clipboard_read,
|
clipboard::clipboard_read,
|
||||||
clipboard::clipboard_copy,
|
clipboard::clipboard_copy,
|
||||||
clipboard::clipboard_paste,
|
clipboard::clipboard_paste,
|
||||||
clipboard::clipboard_clear
|
clipboard::clipboard_clear,
|
||||||
|
oauth::oauth_set_tokens,
|
||||||
|
oauth::oauth_get_tokens,
|
||||||
|
oauth::oauth_remove_tokens
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
|
|
87
src-tauri/src/oauth.rs
Normal file
87
src-tauri/src/oauth.rs
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct StoredTokenSet {
|
||||||
|
access_token: String,
|
||||||
|
refresh_token: Option<String>,
|
||||||
|
expires_in: Option<u64>,
|
||||||
|
scope: Option<String>,
|
||||||
|
id_token: Option<String>,
|
||||||
|
updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenStore = HashMap<String, StoredTokenSet>;
|
||||||
|
|
||||||
|
fn get_storage_path(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
||||||
|
let data_dir = app
|
||||||
|
.path()
|
||||||
|
.app_local_data_dir()
|
||||||
|
.map_err(|_| "Failed to get app local data dir".to_string())?;
|
||||||
|
|
||||||
|
if !data_dir.exists() {
|
||||||
|
fs::create_dir_all(&data_dir).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data_dir.join("oauth_tokens.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_store(path: &Path) -> Result<TokenStore, String> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
}
|
||||||
|
let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||||
|
if content.trim().is_empty() {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
}
|
||||||
|
serde_json::from_str(&content).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_store(path: &Path, store: &TokenStore) -> Result<(), String> {
|
||||||
|
let content = serde_json::to_string_pretty(store).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(path, content).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn oauth_set_tokens(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
provider_id: String,
|
||||||
|
tokens: serde_json::Value,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let path = get_storage_path(&app)?;
|
||||||
|
let mut store = read_store(&path)?;
|
||||||
|
|
||||||
|
let token_set: StoredTokenSet =
|
||||||
|
serde_json::from_value(tokens).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
store.insert(provider_id, token_set);
|
||||||
|
write_store(&path, &store)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn oauth_get_tokens(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
provider_id: String,
|
||||||
|
) -> Result<Option<serde_json::Value>, String> {
|
||||||
|
let path = get_storage_path(&app)?;
|
||||||
|
let store = read_store(&path)?;
|
||||||
|
if let Some(token_set) = store.get(&provider_id) {
|
||||||
|
let value = serde_json::to_value(token_set).map_err(|e| e.to_string())?;
|
||||||
|
Ok(Some(value))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn oauth_remove_tokens(app: tauri::AppHandle, provider_id: String) -> Result<(), String> {
|
||||||
|
let path = get_storage_path(&app)?;
|
||||||
|
let mut store = read_store(&path)?;
|
||||||
|
store.remove(&provider_id);
|
||||||
|
write_store(&path, &store)
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { uiStore } from '$lib/ui.svelte';
|
||||||
import { SidecarMessageWithPluginsSchema } from '@raycast-linux/protocol';
|
import { SidecarMessageWithPluginsSchema } from '@raycast-linux/protocol';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { appCacheDir, appLocalDataDir } from '@tauri-apps/api/path';
|
import { appCacheDir, appLocalDataDir } from '@tauri-apps/api/path';
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||||
|
|
||||||
class SidecarService {
|
class SidecarService {
|
||||||
#sidecarChild: Child | null = $state(null);
|
#sidecarChild: Child | null = $state(null);
|
||||||
|
@ -171,6 +172,41 @@ class SidecarService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typedMessage.type.startsWith('oauth-')) {
|
||||||
|
if (typedMessage.type === 'oauth-authorize') {
|
||||||
|
const { url } = typedMessage.payload;
|
||||||
|
openUrl(url).catch((err) => {
|
||||||
|
this.#log(`ERROR: Failed to open OAuth URL '${url}': ${err}`);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { requestId, ...params } = typedMessage.payload as {
|
||||||
|
requestId: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandMap: Record<string, string> = {
|
||||||
|
'oauth-get-tokens': 'oauth_get_tokens',
|
||||||
|
'oauth-set-tokens': 'oauth_set_tokens',
|
||||||
|
'oauth-remove-tokens': 'oauth_remove_tokens'
|
||||||
|
};
|
||||||
|
const command = commandMap[typedMessage.type];
|
||||||
|
|
||||||
|
if (command) {
|
||||||
|
const responseType = `${typedMessage.type}-response`;
|
||||||
|
try {
|
||||||
|
const result = await invoke(command, params);
|
||||||
|
this.dispatchEvent(responseType, { requestId, result });
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
this.#log(`ERROR from ${command}: ${errorMessage}`);
|
||||||
|
this.dispatchEvent(responseType, { requestId, error: errorMessage });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typedMessage.type === 'plugin-list') {
|
if (typedMessage.type === 'plugin-list') {
|
||||||
uiStore.setPluginList(typedMessage.payload);
|
uiStore.setPluginList(typedMessage.payload);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -70,12 +70,28 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (urlObj.protocol === 'raycast:') {
|
if (urlObj.protocol === 'raycast:') {
|
||||||
switch (urlObj.host) {
|
if (urlObj.host === 'oauth-callback' || urlObj.pathname.startsWith('/redirect')) {
|
||||||
case 'extensions':
|
const params = urlObj.searchParams;
|
||||||
viewState = 'extensions-store';
|
const code = params.get('code');
|
||||||
break;
|
const state = params.get('state');
|
||||||
default:
|
if (code && state) {
|
||||||
viewState = 'plugin-list';
|
sidecarService.dispatchEvent('oauth-authorize-response', { code, state });
|
||||||
|
} else {
|
||||||
|
const error = params.get('error') || 'Unknown OAuth error';
|
||||||
|
const errorDescription = params.get('error_description');
|
||||||
|
sidecarService.dispatchEvent('oauth-authorize-response', {
|
||||||
|
state,
|
||||||
|
error: `${error}: ${errorDescription}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (urlObj.host) {
|
||||||
|
case 'extensions':
|
||||||
|
viewState = 'extensions-store';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
viewState = 'plugin-list';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue