Polar TypeScript SDK 实战指南:从快速接入到 Webhook 签名安全验证

Polar TypeScript SDK 实战指南:从快速接入到 Webhook 签名安全验证 Polar TypeScript SDK 实战指南从快速接入到 Webhook 签名安全验证【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polarPolar 官方 TypeScript SDK 为 Node.js / TypeScript 开发者提供了对 Polar 计费与支付 API 的类型安全封装。本文以 SDK 自带的 README 文档位于 sdk/generator/typescript/template/README.md为主体结合仓库内模板源码与测试用例系统讲解安装方式、客户端初始化、超时与令牌覆盖、tree-shaking 友好的函数级导入以及 Webhook 签名验证的完整实现原理。读完本文你将能在一小时内完成 Polar SDK 的接入并独立实现一套生产级、防重放攻击的 Webhook 接收端点。安装预发布标签nextPolar TypeScript SDK 目前以next预发布标签对外发布推荐使用pnpm或npm安装pnpm add polar-sh/sdknext或使用 npmnpm install polar-sh/sdknext从模板的 package.json 可以看到该包为纯 ESM 风格type: module、sideEffects: false要求 Node.js22提供dist/index.cjsCommonJS与dist/index.mjsESM双入口并通过exports字段暴露了版本化子路径下文详述。构建工具链为tsdown测试框架为vitest。快速开始三行代码调用 Polar API创建组织访问令牌Organization Access Token即polar_oat_开头的令牌后从当前 API 版本的子路径导入createPolar并初始化客户端import { createPolar } from polar-sh/sdk/{{ ir.versions[0].version }}; const polar createPolar({ accessToken: polar_oat_xxx, }); const customerState await polar.customers.getStateExternal(customer_external_id); console.log(customerState);上例中{{ ir.versions[0].version }}是 SDK 生成器位于 sdk/generator/typescript在生成代码时替换的模板变量指向当前 API 版本仓库文档中可以看到如2026-04、2026-10等版本目录见 docs/api-reference/2026-10 与 docs/api-reference/2026-04。版本化导入的核心价值在于API 演进时你可以显式锁定某个版本迁移成本可控。关于运行环境与令牌安全README 明确了两条铁律环境选择客户端默认使用生产环境https://api.polar.sh一类服务器地址由模板 src/version/client.ts 中的SERVERS映射决定。如需接入沙箱在createPolar中传入environment: sandbox即可沙箱与生产环境的访问令牌彼此独立不可混用。令牌保密组织访问令牌必须保存在服务端严禁出现在浏览器或任何客户端代码中——否则等同于把支付与计费 API 的钥匙公之于众。客户端初始化背后的版本解析逻辑在 src/version/client.ts 中createPolarCore调用了resolveBaseUrl实现见 src/base.ts若显式传入baseUrl则优先使用否则从SERVERS映射按environment取值遇到未知环境会抛出包含所有合法环境名的错误。createPolar则在其之上实例化所有服务的 API 客户端对象如customers、orders、subscriptions等并以Polar类型导出完整客户端形状。请求超时全局默认值与单请求覆盖SDK 允许在创建客户端时设置所有请求的默认超时时间单位为秒const polar createPolar({ accessToken: polar_oat_xxx, timeout: 30, });针对单个请求可通过最后一个requestOptions参数覆盖超时const customerState await polar.customers.getStateExternal(customer_external_id, { timeout: 60, });同样也可以按请求覆盖访问令牌const customerState await polar.customers.getStateExternal(customer_external_id, { accessToken: polar_at_u_override, });超时实现的底层细节从 src/base.ts 的源码看ClientBase构造时默认超时为5.0秒任何显式传入的timeout都会覆盖它。sendRequest在发起fetch前会做三件事校验超时必须为有限、非负的数且换算成毫秒后不超过AbortSignal的上限2_147_483_647毫秒否则抛出RangeError合并信号用AbortSignal.timeout()生成超时信号若请求本身已携带signal则通过AbortSignal.any()合并网络错误包装fetch抛出的任何异常都会被包装为PolarNetworkError统一错误入口。此外每次请求都会自动携带Content-Type: application/json、Polar-Version: 当前API版本以及Authorization: Bearer accessToken三个请求头按请求覆盖令牌时会基于原 Headers 克隆后重新setAuthorization不会污染客户端全局配置。单个 API 函数导入为 tree-shaking 而生如果你只想使用个别接口、希望构建产物尽可能小可以创建核心客户端core client并把单个 API 函数传入其中import { createPolarCore } from polar-sh/sdk/{{ ir.versions[0].version }}; import { getStateExternalCustomers } from polar-sh/sdk/{{ ir.versions[0].version }}/services/customers; const polar createPolarCore({ accessToken: polar_oat_xxx, }); const customerState await getStateExternalCustomers(polar)(customer_external_id); console.log(customerState);这一设计的实现原理同样来自模板源码在 src/version/services/service.ts 中每个 API 操作都被生成为一个高阶函数——exported_operation_name(client)返回一个接收路径参数、查询参数、请求体与requestOptions的异步函数。函数内部依次调用client.buildRequest()组装 URL、路径参数编码、查询参数展开、client.sendRequest()携带超时与令牌覆盖和client.parseResponse()按状态码分发结果与错误。由于函数直接引用ClientBase而非整个客户端对象打包器可以轻松摇树tree-shake掉未使用的函数与模型。包内exports也为此提供了./version/services/*通配子路径见 package.json保证按服务导入也有独立的类型声明与产物文件。查询参数与分页AsyncGenerator 的原生支持service.ts模板还揭示了一个 README 未展开的能力对于支持分页的列表接口SDK 会额外生成paginator变体返回AsyncGeneratorfor await (const item of polar.orders.list(...)) { console.log(item); }分页器内部从query.page ?? 1开始每轮请求后遍历response.items逐条 yield直到page response.pagination.max_page才结束从而把翻页循环完全封装掉调用方只需for await即可消费全部数据。在 code_sample.ts.jinja 中也能看到生成器为分页接口自动生成for await示例代码的逻辑。错误处理分层的异常体系模板 src/index.ts 从base导出了一套层次分明的错误类异常类触发场景PolarError所有 Polar 错误的基类PolarNetworkError网络层故障DNS 失败、连接中断等PolarServerError服务端返回 5xxPolarClientErrorT客户端请求错误4xx携带statusCode与错误体PolarRateLimitError状态码 429含retryAfter字段parseResponse见 src/base.ts的分发逻辑是5xx 一律抛PolarServerError429 优先读取Retry-After响应头构造PolarRateLimitError其余 4xx 若 API 定义了对应错误模型由生成器在 src/version/errors.ts 中生成具体的xxxError extends PolarClientErrorT类则反序列化错误体抛出精确类型未定义时退化为携带响应文本的通用PolarClientError。Webhook签名验证与类型化事件Webhook 是支付平台最关键的集成点之一。Polar SDK 提供了webhooks.validateEvent它同时完成两件事验证请求确实来自 Polar并把原始请求体解析为所选 API 版本的类型化事件负载。以下是一个完整的 Express 接收端点示例来自 READMEimport express from express; import { webhooks } from polar-sh/sdk/{{ ir.versions[0].version }}; const app express(); const webhookSecret process.env.POLAR_WEBHOOK_SECRET; if (!webhookSecret) { throw new Error(POLAR_WEBHOOK_SECRET is required); } const rawBody express.raw({ type: application/json }); app.post(/webhooks/polar, rawBody, async (request, response) { try { const event await webhooks.validateEvent( request.body, { webhook-id: request.header(webhook-id) ?? , webhook-timestamp: request.header(webhook-timestamp) ?? , webhook-signature: request.header(webhook-signature) ?? , }, webhookSecret, ); if (event.type order.created) { console.log(event.data.id); } response.status(200).json({ received: true }); } catch (error) { if (error instanceof webhooks.PolarWebhookVerificationError) { response.status(403).json({ error: Invalid webhook signature }); return; } if (error instanceof webhooks.PolarWebhookError) { response.status(400).json({ error: Invalid webhook payload }); return; } throw error; } });使用时有三个关键点需要理解必须使用express.raw中间件签名校验针对的是未解析的原始请求体。若先用 JSON 中间件解析再传给validateEvent字节流已改变签名必然校验失败。README 特别强调signature must be checked against the body before it is parsed。必须透传三个签名头webhook-id、webhook-timestamp、webhook-signature缺一不可全部原样传递。异常必须区分处理PolarWebhookVerificationError签名无效返回 403与PolarWebhookUnknownTypeError事件类型不在当前 API 版本支持列表中都继承自PolarWebhookError后者适合返回 400。业务处理异常应继续向上抛交由全局错误处理中间件兜底。签名验证的源码级原理在 src/webhooks.ts 中validateWebhook的执行流水线如下空密钥防护secret为空直接抛PolarWebhookVerificationError请求头规范化与校验将请求头 key 转为小写后逐一取用缺失任一必需头即抛错时间戳防重放时间容忍窗口为5 * 60秒webhookToleranceSeconds请求时间早于或晚于当前时间 5 分钟都会被拒绝——这既是防重放攻击的第一道防线也是时钟漂移的容忍边界构造签名内容signedContent ${webhookId}.${Math.floor(timestamp)}.${body}这是 Webhook 签名协议Standard Webhooks的标准格式双密钥兼容验证hmacKeys(secret)会生成两个候选 HMAC 密钥——Polar 原始的 HMAC 密钥完整 secret 的 UTF-8 字节含whsec_前缀以及 Standard Webhooks 格式密钥去掉whsec_前缀后 base64 解码的结果。两者字节不同时都参与验证逐签名尝试对webhook-signature按空格拆分的每个版本化签名格式v1,base64签名用 Web Cryptocrypto.subtle.verifyHMAC-SHA-256逐一比对任一命中即通过全部失败则抛PolarWebhookVerificationError(No matching signature found)。事件类型校验则发生在签名验证之后在 src/version/webhooks.ts 中SDK 为当前 API 版本生成了knownEventTypes集合如order.created、order.paid等负载中的type字段不在集合内即抛PolarWebhookUnknownTypeError从而避免把未知事件误当作已知类型处理。测试用例印证模板自带的 src/webhooks.test.ts 使用standardwebhooks库生成合法签名来验证validateWebhook分别以string、Buffer、Uint8Array三种原始体形态调用均能通过验证——说明validateEvent对原始请求体的字节来源不敏感使用whsec_ base64 随机密钥签名的Standard Webhooks 格式密钥也能被正确验证直接印证了上文双密钥兼容的设计。总结与最佳实践清单Polar TypeScript SDK 以版本化子路径 可摇树的函数级导出 分层错误 标准 Webhooks 验证四个特性支撑起生产级集成体验。落地时请记住以下清单使用polar-sh/sdknext并显式指定 API 版本子路径导入锁定 API 契约服务端持有polar_oat_令牌timeout按业务耗时设置全局默认值个别慢接口用requestOptions覆盖只需少量接口时改用createPolarCore 服务级函数导入减小打包体积Webhook 端点必须用express.raw保留原始体、透传三个签名头并对PolarWebhookVerificationError403与PolarWebhookUnknownTypeError400分别应答同时在 5 分钟时间窗口内拒绝过期请求以防重放。相关实现与测试均可在仓库内继续深挖模板 README、核心请求层、Webhook 验证实现、版本化客户端、服务函数生成模板 以及 Webhook 测试。【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考