import type { OAuth2Tokens } from "arctic"; import type { Context } from "hono"; import { OauthError } from "../error.js"; import { getRelativeUrl } from "../util.js"; import type { 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, }); }); }, }; }