import type { OAuth2Tokens } from 'arctic'; import { Context } from 'hono'; import { OauthError } from '../error.js'; import { getRelativeUrl } from '../util.js'; import { Provider } from './provider.js'; export interface ArcticProviderOptions { scopes: string[]; clientID: string; clientSecret: string; query?: Record; } interface ProviderState { state: string; } export function ArcticProvider( provider: new ( clientID: string, clientSecret: string, callback: string ) => { createAuthorizationURL(state: string, scopes: string[]): URL; validateAuthorizationCode(code: string): Promise; refreshAccessToken(refreshToken: string): Promise; }, config: ArcticProviderOptions ): Provider<{ tokenset: OAuth2Tokens; }> { function getClient(c: Context) { const callback = new URL(c.req.url); const pathname = callback.pathname.replace(/authorize.*$/, 'callback'); const url = getRelativeUrl(c, pathname); return new provider(config.clientID, config.clientSecret, url); } return { type: 'arctic', init(routes, ctx) { routes.get('/authorize', async (c) => { const client = getClient(c); const state = crypto.randomUUID(); await ctx.set(c, 'provider', 60 * 10, { state }); return c.redirect(client.createAuthorizationURL(state, config.scopes)); }); routes.get('/callback', async (c) => { const client = getClient(c); const provider = (await ctx.get(c, 'provider')) as ProviderState; if (!provider) return c.redirect('../authorize'); const code = c.req.query('code'); const state = c.req.query('state'); if (!code) throw new Error('Missing code'); if (state !== provider.state) throw new OauthError('invalid_request', 'Invalid state'); const tokens = await client.validateAuthorizationCode(code); return ctx.success(c, { tokenset: tokens }); }); } }; }