diff --git a/packages/plugin-runtime-types/src/bindings/gen_search.ts b/packages/plugin-runtime-types/src/bindings/gen_search.ts index 9dbe0103..dd3c1ad7 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_search.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_search.ts @@ -2,4 +2,4 @@ export type PluginMetadata = { version: string, name: string, displayName: string, description: string | null, homepageUrl: string | null, repositoryUrl: string | null, }; -export type PluginVersion = { id: string, version: string, description: string | null, name: string, displayName: string, homepageUrl: string | null, repositoryUrl: string | null, checksum: string, readme: string | null, yanked: boolean, }; +export type PluginVersion = { id: string, version: string, url: string, description: string | null, name: string, displayName: string, homepageUrl: string | null, repositoryUrl: string | null, checksum: string, readme: string | null, yanked: boolean, }; diff --git a/packages/plugin-runtime/src/index.ts b/packages/plugin-runtime/src/index.ts index afdd0e22..b3e6c74b 100644 --- a/packages/plugin-runtime/src/index.ts +++ b/packages/plugin-runtime/src/index.ts @@ -53,3 +53,7 @@ async function handleIncoming(msg: string) { plugin.sendToWorker(pluginEvent); } + +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); +}); diff --git a/plugins/auth-oauth2/src/getAccessToken.ts b/plugins/auth-oauth2/src/fetchAccessToken.ts similarity index 92% rename from plugins/auth-oauth2/src/getAccessToken.ts rename to plugins/auth-oauth2/src/fetchAccessToken.ts index 480a17b2..8e337a4e 100644 --- a/plugins/auth-oauth2/src/getAccessToken.ts +++ b/plugins/auth-oauth2/src/fetchAccessToken.ts @@ -1,8 +1,8 @@ -import { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api'; +import type { Context, HttpRequest, HttpUrlParameter } from '@yaakapp/api'; import { readFileSync } from 'node:fs'; -import { AccessTokenRawResponse } from './store'; +import type { AccessTokenRawResponse } from './store'; -export async function getAccessToken( +export async function fetchAccessToken( ctx: Context, { accessTokenUrl, diff --git a/plugins/auth-oauth2/src/getAccessTokenIfNotExpired.ts b/plugins/auth-oauth2/src/getAccessTokenIfNotExpired.ts new file mode 100644 index 00000000..fed45b9e --- /dev/null +++ b/plugins/auth-oauth2/src/getAccessTokenIfNotExpired.ts @@ -0,0 +1,19 @@ +import type { Context } from '@yaakapp/api'; +import type { AccessToken } from './store'; +import { getToken } from './store'; + +export async function getAccessTokenIfNotExpired( + ctx: Context, + contextId: string, +): Promise { + const token = await getToken(ctx, contextId); + if (token == null || isTokenExpired(token)) { + return null; + } + + return token; +} + +export function isTokenExpired(token: AccessToken) { + return token.expiresAt && Date.now() > token.expiresAt; +} diff --git a/plugins/auth-oauth2/src/getOrRefreshAccessToken.ts b/plugins/auth-oauth2/src/getOrRefreshAccessToken.ts index 12400963..0b1b37e3 100644 --- a/plugins/auth-oauth2/src/getOrRefreshAccessToken.ts +++ b/plugins/auth-oauth2/src/getOrRefreshAccessToken.ts @@ -1,29 +1,34 @@ -import { Context, HttpRequest } from '@yaakapp/api'; +import type { Context, HttpRequest } from '@yaakapp/api'; import { readFileSync } from 'node:fs'; -import { AccessToken, AccessTokenRawResponse, deleteToken, getToken, storeToken } from './store'; +import { isTokenExpired } from './getAccessTokenIfNotExpired'; +import type { AccessToken, AccessTokenRawResponse } from './store'; +import { deleteToken, getToken, storeToken } from './store'; -export async function getOrRefreshAccessToken(ctx: Context, contextId: string, { - scope, - accessTokenUrl, - credentialsInBody, - clientId, - clientSecret, - forceRefresh, -}: { - scope: string | null; - accessTokenUrl: string; - credentialsInBody: boolean; - clientId: string; - clientSecret: string; - forceRefresh?: boolean; -}): Promise { +export async function getOrRefreshAccessToken( + ctx: Context, + contextId: string, + { + scope, + accessTokenUrl, + credentialsInBody, + clientId, + clientSecret, + forceRefresh, + }: { + scope: string | null; + accessTokenUrl: string; + credentialsInBody: boolean; + clientId: string; + clientSecret: string; + forceRefresh?: boolean; + }, +): Promise { const token = await getToken(ctx, contextId); if (token == null) { return null; } - const now = Date.now(); - const isExpired = token.expiresAt && now > token.expiresAt; + const isExpired = isTokenExpired(token); // Return the current access token if it's still valid if (!isExpired && !forceRefresh) { @@ -79,7 +84,9 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, { console.log('[oauth2] Got refresh token response', resp.status); if (resp.status < 200 || resp.status >= 300) { - throw new Error('Failed to refresh access token with status=' + resp.status + ' and body=' + body); + throw new Error( + 'Failed to refresh access token with status=' + resp.status + ' and body=' + body, + ); } let response; @@ -90,7 +97,9 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, { } if (response.error) { - throw new Error(`Failed to fetch access token with ${response.error} -> ${response.error_description}`); + throw new Error( + `Failed to fetch access token with ${response.error} -> ${response.error_description}`, + ); } const newResponse: AccessTokenRawResponse = { diff --git a/plugins/auth-oauth2/src/grants/authorizationCode.ts b/plugins/auth-oauth2/src/grants/authorizationCode.ts index b8da7618..abafcbe3 100644 --- a/plugins/auth-oauth2/src/grants/authorizationCode.ts +++ b/plugins/auth-oauth2/src/grants/authorizationCode.ts @@ -1,8 +1,9 @@ -import { Context } from '@yaakapp/api'; +import type { Context } from '@yaakapp/api'; import { createHash, randomBytes } from 'node:crypto'; -import { getAccessToken } from '../getAccessToken'; +import { fetchAccessToken } from '../fetchAccessToken'; import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken'; -import { AccessToken, getDataDirKey, storeToken } from '../store'; +import type { AccessToken } from '../store'; +import { getDataDirKey, storeToken } from '../store'; export const PKCE_SHA256 = 'S256'; export const PKCE_PLAIN = 'plain'; @@ -34,8 +35,8 @@ export async function getAuthorizationCode( audience: string | null; credentialsInBody: boolean; pkce: { - challengeMethod: string | null; - codeVerifier: string | null; + challengeMethod: string; + codeVerifier: string; } | null; tokenName: 'access_token' | 'id_token'; }, @@ -59,26 +60,25 @@ export async function getAuthorizationCode( if (state) authorizationUrl.searchParams.set('state', state); if (audience) authorizationUrl.searchParams.set('audience', audience); if (pkce) { - const verifier = pkce.codeVerifier || createPkceCodeVerifier(); - const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD; authorizationUrl.searchParams.set( 'code_challenge', - createPkceCodeChallenge(verifier, challengeMethod), + pkceCodeChallenge(pkce.codeVerifier, pkce.challengeMethod), ); - authorizationUrl.searchParams.set('code_challenge_method', challengeMethod); + authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod); } - return new Promise(async (resolve, reject) => { - const authorizationUrlStr = authorizationUrl.toString(); - const logsEnabled = (await ctx.store.get('enable_logs')) ?? false; - console.log('[oauth2] Authorizing', authorizationUrlStr); + const logsEnabled = (await ctx.store.get('enable_logs')) ?? false; + const dataDirKey = await getDataDirKey(ctx, contextId); + const authorizationUrlStr = authorizationUrl.toString(); + console.log('[oauth2] Authorizing', authorizationUrlStr); + // eslint-disable-next-line no-async-promise-executor + const code = await new Promise(async (resolve, reject) => { let foundCode = false; - - let { close } = await ctx.window.openUrl({ + const { close } = await ctx.window.openUrl({ url: authorizationUrlStr, label: 'oauth-authorization-url', - dataDirKey: await getDataDirKey(ctx, contextId), + dataDirKey, async onClose() { if (!foundCode) { reject(new Error('Authorization window closed')); @@ -89,6 +89,7 @@ export async function getAuthorizationCode( if (logsEnabled) console.log('[oauth2] Navigated to', urlStr); if (url.searchParams.has('error')) { + close(); return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`)); } @@ -101,37 +102,35 @@ export async function getAuthorizationCode( // Close the window here, because we don't need it anymore! foundCode = true; close(); - - console.log('[oauth2] Code found'); - const response = await getAccessToken(ctx, { - grantType: 'authorization_code', - accessTokenUrl, - clientId, - clientSecret, - scope, - audience, - credentialsInBody, - params: [ - { name: 'code', value: code }, - ...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []), - ], - }); - - try { - resolve(await storeToken(ctx, contextId, response, tokenName)); - } catch (err) { - reject(err); - } + resolve(code); }, }); }); + + console.log('[oauth2] Code found'); + const response = await fetchAccessToken(ctx, { + grantType: 'authorization_code', + accessTokenUrl, + clientId, + clientSecret, + scope, + audience, + credentialsInBody, + params: [ + { name: 'code', value: code }, + ...(pkce ? [{ name: 'code_verifier', value: pkce.codeVerifier }] : []), + ...(redirectUri ? [{ name: 'redirect_uri', value: redirectUri }] : []), + ], + }); + + return storeToken(ctx, contextId, response, tokenName); } -function createPkceCodeVerifier() { +export function genPkceCodeVerifier() { return encodeForPkce(randomBytes(32)); } -function createPkceCodeChallenge(verifier: string, method: string) { +function pkceCodeChallenge(verifier: string, method: string) { if (method === 'plain') { return verifier; } diff --git a/plugins/auth-oauth2/src/grants/clientCredentials.ts b/plugins/auth-oauth2/src/grants/clientCredentials.ts index 9543d9b7..d565bd91 100644 --- a/plugins/auth-oauth2/src/grants/clientCredentials.ts +++ b/plugins/auth-oauth2/src/grants/clientCredentials.ts @@ -1,5 +1,6 @@ -import { Context } from '@yaakapp/api'; -import { getAccessToken } from '../getAccessToken'; +import type { Context } from '@yaakapp/api'; +import { fetchAccessToken } from '../fetchAccessToken'; +import { isTokenExpired } from '../getAccessTokenIfNotExpired'; import { getToken, storeToken } from '../store'; export async function getClientCredentials( @@ -22,13 +23,11 @@ export async function getClientCredentials( }, ) { const token = await getToken(ctx, contextId); - if (token) { - // resolve(token.response.access_token); - // TODO: Refresh token if expired - // return; + if (token && !isTokenExpired(token)) { + return token; } - const response = await getAccessToken(ctx, { + const response = await fetchAccessToken(ctx, { grantType: 'client_credentials', accessTokenUrl, audience, diff --git a/plugins/auth-oauth2/src/grants/implicit.ts b/plugins/auth-oauth2/src/grants/implicit.ts index 347fbfcd..043d5379 100644 --- a/plugins/auth-oauth2/src/grants/implicit.ts +++ b/plugins/auth-oauth2/src/grants/implicit.ts @@ -1,7 +1,9 @@ -import { Context } from '@yaakapp/api'; -import { AccessToken, AccessTokenRawResponse, getToken, storeToken } from '../store'; +import type { Context } from '@yaakapp/api'; +import { isTokenExpired } from '../getAccessTokenIfNotExpired'; +import type { AccessToken, AccessTokenRawResponse} from '../store'; +import { getToken, storeToken } from '../store'; -export function getImplicit( +export async function getImplicit( ctx: Context, contextId: string, { @@ -24,31 +26,30 @@ export function getImplicit( tokenName: 'access_token' | 'id_token'; }, ): Promise { - return new Promise(async (resolve, reject) => { - const token = await getToken(ctx, contextId); - if (token) { - // resolve(token.response.access_token); - // TODO: Refresh token if expired - // return; - } + const token = await getToken(ctx, contextId); + if (token != null && !isTokenExpired(token)) { + return token; + } - const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`); - authorizationUrl.searchParams.set('response_type', 'token'); - authorizationUrl.searchParams.set('client_id', clientId); - if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri); - if (scope) authorizationUrl.searchParams.set('scope', scope); - if (state) authorizationUrl.searchParams.set('state', state); - if (audience) authorizationUrl.searchParams.set('audience', audience); - if (responseType.includes('id_token')) { - authorizationUrl.searchParams.set( - 'nonce', - String(Math.floor(Math.random() * 9999999999999) + 1), - ); - } + const authorizationUrl = new URL(`${authorizationUrlRaw ?? ''}`); + authorizationUrl.searchParams.set('response_type', 'token'); + authorizationUrl.searchParams.set('client_id', clientId); + if (redirectUri) authorizationUrl.searchParams.set('redirect_uri', redirectUri); + if (scope) authorizationUrl.searchParams.set('scope', scope); + if (state) authorizationUrl.searchParams.set('state', state); + if (audience) authorizationUrl.searchParams.set('audience', audience); + if (responseType.includes('id_token')) { + authorizationUrl.searchParams.set( + 'nonce', + String(Math.floor(Math.random() * 9999999999999) + 1), + ); + } - const authorizationUrlStr = authorizationUrl.toString(); + // eslint-disable-next-line no-async-promise-executor + const newToken = await new Promise(async (resolve, reject) => { let foundAccessToken = false; - let { close } = await ctx.window.openUrl({ + const authorizationUrlStr = authorizationUrl.toString(); + const { close } = await ctx.window.openUrl({ url: authorizationUrlStr, label: 'oauth-authorization-url', async onClose() { @@ -76,11 +77,13 @@ export function getImplicit( const response = Object.fromEntries(params) as unknown as AccessTokenRawResponse; try { - resolve(await storeToken(ctx, contextId, response)); + resolve(storeToken(ctx, contextId, response)); } catch (err) { reject(err); } }, }); }); + + return newToken; } diff --git a/plugins/auth-oauth2/src/grants/password.ts b/plugins/auth-oauth2/src/grants/password.ts index 2192345f..f2a4d401 100644 --- a/plugins/auth-oauth2/src/grants/password.ts +++ b/plugins/auth-oauth2/src/grants/password.ts @@ -1,7 +1,8 @@ -import { Context } from '@yaakapp/api'; -import { getAccessToken } from '../getAccessToken'; +import type { Context } from '@yaakapp/api'; +import { fetchAccessToken } from '../fetchAccessToken'; import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken'; -import { AccessToken, storeToken } from '../store'; +import type { AccessToken} from '../store'; +import { storeToken } from '../store'; export async function getPassword( ctx: Context, @@ -37,7 +38,7 @@ export async function getPassword( return token; } - const response = await getAccessToken(ctx, { + const response = await fetchAccessToken(ctx, { accessTokenUrl, clientId, clientSecret, diff --git a/plugins/auth-oauth2/src/index.ts b/plugins/auth-oauth2/src/index.ts index 6ba52649..95bb71ef 100644 --- a/plugins/auth-oauth2/src/index.ts +++ b/plugins/auth-oauth2/src/index.ts @@ -1,4 +1,4 @@ -import { +import type { Context, FormInputSelectOption, GetHttpAuthenticationConfigRequest, @@ -6,6 +6,7 @@ import { PluginDefinition, } from '@yaakapp/api'; import { + genPkceCodeVerifier, DEFAULT_PKCE_METHOD, getAuthorizationCode, PKCE_PLAIN, @@ -14,7 +15,8 @@ import { import { getClientCredentials } from './grants/clientCredentials'; import { getImplicit } from './grants/implicit'; import { getPassword } from './grants/password'; -import { AccessToken, deleteToken, getToken, resetDataDirKey } from './store'; +import type { AccessToken } from './store'; +import { deleteToken, getToken, resetDataDirKey } from './store'; type GrantType = 'authorization_code' | 'implicit' | 'password' | 'client_credentials'; @@ -219,9 +221,9 @@ export const plugin: PluginDefinition = { }, { type: 'text', - name: 'pkceCodeVerifier', + name: 'pkceCodeChallenge', label: 'Code Verifier', - placeholder: 'Automatically generated if not provided', + placeholder: 'Automatically generated when not set', optional: true, dynamic: hiddenIfNot(['authorization_code'], ({ usePkce }) => !!usePkce), }, @@ -325,8 +327,8 @@ export const plugin: PluginDefinition = { credentialsInBody, pkce: values.usePkce ? { - challengeMethod: stringArg(values, 'pkceChallengeMethod'), - codeVerifier: stringArgOrNull(values, 'pkceCodeVerifier'), + challengeMethod: stringArg(values, 'pkceChallengeMethod') || DEFAULT_PKCE_METHOD, + codeVerifier: stringArg(values, 'pkceCodeVerifier') || genPkceCodeVerifier(), } : null, tokenName: tokenName,