import { createHash } from "node:crypto"; import type { Context } from "@yaakapp/api"; export async function storeToken( ctx: Context, args: TokenStoreArgs, response: AccessTokenRawResponse, tokenName: "access_token" | "id_token" = "access_token", ) { if (!response[tokenName]) { throw new Error(`${tokenName} not found in response ${Object.keys(response).join(", ")}`); } const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1000 : null; const token: AccessToken = { response, expiresAt, }; await ctx.store.set(tokenStoreKey(args), token); return token; } export async function getToken(ctx: Context, args: TokenStoreArgs) { return ctx.store.get(tokenStoreKey(args)); } export async function deleteToken(ctx: Context, args: TokenStoreArgs) { return ctx.store.delete(tokenStoreKey(args)); } export async function resetDataDirKey(ctx: Context, contextId: string) { const key = new Date().toISOString(); return ctx.store.set(dataDirStoreKey(contextId), key); } export async function getDataDirKey(ctx: Context, contextId: string) { const key = (await ctx.store.get(dataDirStoreKey(contextId))) ?? "default"; return `${contextId}::${key}`; } export interface TokenStoreArgs { contextId: string; clientId: string; accessTokenUrl: string | null; authorizationUrl: string | null; } /** * Generate a store key to use based on some arguments. The arguments will be normalized a bit to * account for slight variations (like domains with and without a protocol scheme). */ function tokenStoreKey(args: TokenStoreArgs) { const hash = createHash("md5"); if (args.contextId) hash.update(args.contextId.trim()); if (args.clientId) hash.update(args.clientId.trim()); if (args.accessTokenUrl) hash.update(args.accessTokenUrl.trim().replace(/^https?:\/\//, "")); if (args.authorizationUrl) hash.update(args.authorizationUrl.trim().replace(/^https?:\/\//, "")); const key = hash.digest("hex"); return ["token", key].join("::"); } function dataDirStoreKey(contextId: string) { return ["data_dir", contextId].join("::"); } export interface AccessToken { response: AccessTokenRawResponse; expiresAt: number | null; } export interface AccessTokenRawResponse { access_token: string; id_token?: string; token_type?: string; expires_in?: number; refresh_token?: string; error?: string; error_description?: string; scope?: string; }