import { hash } from "node:crypto" import { fetch } from "undici" export interface GetJsApiAccessTokenOptions { appId: string appSecret: string } export interface GetJsApiAccessTokenResult { accessToken: string /** * @description In milliseconds */ accessTokenExpiresAt: number } /** * @see {@link https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/getStableAccessToken.html} * @see {@link https://mp.weixin.qq.com/debug | 微信公众平台 - 接口调试工具} */ export const getJsApiAccessToken = async ( options: GetJsApiAccessTokenOptions, ): Promise => { const { appId, appSecret } = options interface StableTokenResponse { access_token: string expires_in: number } try { const now = Date.now() const result = await fetch("https://api.weixin.qq.com/cgi-bin/stable_token", { method: "post", body: JSON.stringify({ grant_type: "client_credential", appid: appId, secret: appSecret, force_refresh: false, }), }) const json = (await result.json()) as StableTokenResponse return { accessToken: json.access_token, accessTokenExpiresAt: now + json.expires_in * 1_000, } } catch (exception) { console.error(exception) return undefined } } export interface GetJsApiTicketOptions { accessToken: string } export interface GetJsApiTicketResult { jsApiTicket: string /** * @description In milliseconds */ expiresAt: number } /** * @see {@link https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62 | 附录 1 - JS-SDK 使用权限签名算法} */ export const getJsApiTicket = async ( options: GetJsApiTicketOptions, ): Promise => { const { accessToken } = options interface JsApiTicketResponse { errcode: number errmsg: string ticket: string expires_in: number } try { const now = Date.now() const result = await fetch( `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`, { method: "get", }, ) const json = (await result.json()) as JsApiTicketResponse return { jsApiTicket: json.ticket, expiresAt: now + json.expires_in * 1_000, } } catch (exception) { console.error(exception) return undefined } } export interface SignJsSdkConfigOptions { url: string jsApiTicket: string } export interface SignJsSdkConfigResult { timestamp: number nonceStr: string signature: string } /** * @see {@link https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62 | 签名算法} * @see {@link https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign | 微信公众平台 - 微信 JS 接口签名校验工具} */ export const signJsSdkConfig = (options: SignJsSdkConfigOptions): SignJsSdkConfigResult => { const { url, jsApiTicket } = options const nonceStr = Math.random().toString(36).slice(2, 17) const timestamp = Math.floor(Date.now() / 1_000) const rawString = `jsapi_ticket=${jsApiTicket}&noncestr=${nonceStr}×tamp=${timestamp}&url=${url}` const signature = hash("sha1", rawString) return { timestamp, nonceStr, signature, } }