import { z } from "zod"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import { LARK_HOST, getAppKeys, isExpired } from "../common"; const log = logger.getSubLogger({ prefix: [`[[LarkAppCredential]`] }); function makePoolingPromise( promiseCreator: () => Promise, times = 24, delay = 5 * 1000 ): Promise { return new Promise((resolve, reject) => { promiseCreator() .then(resolve) .catch((err) => { if (times <= 0) { reject(err); return; } setTimeout(() => { makePoolingPromise(promiseCreator, times - 1, delay) .then(resolve) .catch(reject); }, delay); }); }); } const appKeysSchema = z.object({ app_id: z.string().min(1), app_secret: z.string().min(1), app_access_token: z.string().optional(), app_ticket: z.string().optional(), expire_date: z.number().optional(), open_verification_token: z.string().min(1), }); const getValidAppKeys = async (): Promise> => { const appKeys = await getAppKeys(); const validAppKeys = appKeysSchema.parse(appKeys); return validAppKeys; }; const getAppTicketFromKeys = async (): Promise => { const appKeys = await getValidAppKeys(); const appTicketNew = appKeys?.app_ticket; if (appTicketNew) { return appTicketNew; } throw Error("lark appTicketNew not found in getAppTicketFromKeys"); }; const getAppTicket = async (): Promise => { log.debug("get app ticket invoked"); const appKeys = await getValidAppKeys(); if (typeof appKeys.app_ticket === "string" && appKeys.app_ticket !== "") { log.debug("has app ticket", appKeys.app_ticket); return appKeys.app_ticket; } /** * Trigger app-ticket resend. app ticket can only be obtained from * app_ticket event. * see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/application-v6/event/app_ticket-events */ log.info("Invoke app-ticket resend", appKeys.app_ticket); fetch(`https://${LARK_HOST}/open-apis/auth/v3/app_ticket/resend`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ app_id: appKeys.app_id, app_secret: appKeys.app_secret, }), }); /** * 1. App_ticket is only valid for 1 hr. * 2. The we cannot retrieve app_ticket by calling a API. * 3. App_ticket can only be retrieved in app_ticket event, which is push from lark every hour. * 4. We can trigger lark to push a new app_ticket * 5. Therefore, after trigger resend app_ticket ticket, we have to * pooling DB, as app_ticket will update ticket in DB * see * https://open.larksuite.com/document/ugTN1YjL4UTN24CO1UjN/uQjN1YjL0YTN24CN2UjN * https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/auth-v3/auth/app_ticket_resend */ const appTicketNew = await makePoolingPromise(getAppTicketFromKeys); if (appTicketNew) { log.debug("has new app ticket", appTicketNew); return appTicketNew; } log.error("app ticket not found"); throw new Error("No app ticket found"); }; export const getAppAccessToken: () => Promise = async () => { log.debug("get app access token invoked"); const appKeys = await getValidAppKeys(); const appAccessToken = appKeys.app_access_token; const expireDate = appKeys.expire_date; if (appAccessToken && expireDate && !isExpired(expireDate)) { log.debug("get app access token not expired"); return appAccessToken; } const appTicket = await getAppTicket(); const fetchAppAccessToken = (app_ticket: string) => fetch(`https://${LARK_HOST}/open-apis/auth/v3/app_access_token`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ app_id: appKeys.app_id, app_secret: appKeys.app_secret, app_ticket, }), }); const resp = await fetchAppAccessToken(appTicket); const data = await resp.json(); if (!resp.ok || data.code !== 0) { logger.error("lark error with error: ", data, ", logid is:", resp.headers.get("X-Tt-Logid")); // appticket invalid, mostly outdated, delete and renew one if (data.code === 10012) { await prisma.app.update({ where: { slug: "lark-calendar" }, data: { keys: { ...appKeys, app_ticket: "" } }, }); throw new Error("app_ticket invalid, please try again"); } } const newAppAccessToken = data.app_access_token; const newExpireDate = Math.round(Number(new Date()) / 1000 + data.expire); await prisma.app.update({ where: { slug: "lark-calendar" }, data: { keys: { ...appKeys, app_access_token: newAppAccessToken, expire_date: newExpireDate, }, }, }); return newAppAccessToken; };