From f9ed07551f079e6d3c42e70aa67bb84de2ed4474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 21 Oct 2026 19:13:36 +0200 Subject: [PATCH] refactor(polar): read the token through the shared OAuth lifecycle --- src/lib/polar/credentials.ts | 18 +++++++++--------- src/lib/polar/oauth-descriptor.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 src/lib/polar/oauth-descriptor.ts diff --git a/src/lib/polar/credentials.ts b/src/lib/polar/credentials.ts index 50d866bc8..5151a1889 100644 --- a/src/lib/polar/credentials.ts +++ b/src/lib/polar/credentials.ts @@ -11,7 +11,9 @@ */ import { prisma } from "@/lib/db"; import { decrypt, encrypt } from "@/lib/crypto"; +import { obtainAccessToken } from "@/lib/integrations/oauth-kit/token"; import { getPolarCredentials, type PolarCredentials } from "./client"; +import { polarTokenDescriptor } from "./oauth-descriptor"; /** * Resolve the user's Polar OAuth client id/secret, DB-first then env. @@ -80,24 +82,22 @@ export interface PolarConnection { } /** - * Fetch + decrypt the user's stored Polar token. Returns null when the user has + * Fetch + decrypt the user's stored Polar token, read through the shared OAuth + * token lifecycle like every other provider's. Returns null when the user has * not connected Polar (no access token) or the member id is missing. */ export async function getPolarConnection( userId: string, ): Promise { + const grant = await obtainAccessToken(polarTokenDescriptor, userId); + if (grant.kind !== "token") return null; const user = await prisma.user.findUnique({ where: { id: userId }, - select: { - polarAccessTokenEncrypted: true, - polarUserIdEncrypted: true, - }, + select: { polarUserIdEncrypted: true }, }); - if (!user?.polarAccessTokenEncrypted || !user?.polarUserIdEncrypted) { - return null; - } + if (!user?.polarUserIdEncrypted) return null; return { - accessToken: decrypt(user.polarAccessTokenEncrypted), + accessToken: grant.accessToken, polarUserId: decrypt(user.polarUserIdEncrypted), }; } diff --git a/src/lib/polar/oauth-descriptor.ts b/src/lib/polar/oauth-descriptor.ts new file mode 100644 index 000000000..6ce074fdb --- /dev/null +++ b/src/lib/polar/oauth-descriptor.ts @@ -0,0 +1,31 @@ +/** + * Where a Polar grant lives, for the shared token lifecycle + * (`src/lib/integrations/oauth-kit/token.ts`). + * + * Polar AccessLink tokens do not expire and come without a refresh token, so + * the kit only reads them: there is nothing to refresh and nothing to write + * back. A revoked grant surfaces as a refused read, and the person reconnects. + */ +import type { OAuthTokenDescriptor } from "@/lib/integrations/oauth-kit/token"; + +export const polarTokenDescriptor: OAuthTokenDescriptor = { + provider: "polar", + tracksExpiry: false, + expiryBufferMs: 0, + async readGrant(db, userId) { + const row = await db.user.findUnique({ + where: { id: userId }, + select: { polarAccessTokenEncrypted: true }, + }); + if (!row?.polarAccessTokenEncrypted) return null; + return { + accessCiphertext: row.polarAccessTokenEncrypted, + refreshCiphertext: null, + expiresAt: null, + }; + }, + // Never reached: a grant without a refresh token is never refreshed. + writeGrant: async () => 0, + loadCredentials: async () => null, + refresh: null, +};