其他
在 Dodo Payments 中实现软件许可证密钥激活…
适用于基于 Dodo Payments 为软件、桌面应用、CLI 或 Web 服务实现 license key 激活、校验、商户管理、设备实例跟踪和 webhook 访问控制的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 38/100 ×0.15
适用场景
在 Dodo Payments 中实现软特定
解决思路
- 配置 License Key Entitlements:商品购买后交付密钥;在 entitlement 配置中设置激活上限、可选有效期和 fulfillment mode,支持 auto 或 manual。
- 区分并使用三类资源:client.licenses.* 面向终端用户激活流程,公开且无需 API key,包含 activate()、validate()、deactivate()。
- 区分并使用 client.licenseKeys.* 面向商户端密钥管理,需要 API key,包含 list()、retrieve()、create()、update()。
- 区分并使用 client.licenseKeyInstances.* 面向每设备激活实例,需要 API key,包含 list()、retrieve()、update(),用于跟踪每个 key 的活跃设备。
- 处理激活上限:activation limit 为 blank/null 表示无限;否则为最大并发激活数;超过上限调用 activate 返回 422。
- 遵循一次性付款密钥过期语义:一次性付款密钥遵循 entitlement duration。
- 遵循订阅密钥过期语义:订阅签发的密钥没有独立过期时间,有效性跟随订阅状态;on hold 时临时禁用。
- 处理订阅取消:立即取消订阅时永久禁用密钥;当 cancel_at_next_billing_date 设置时,保持密钥有效直到订阅周期结束。
- 处理导入密钥:导入密钥使用可空 expires_at;null 表示永久有效。
- 在终端用户激活中,用 new DodoPayments() 创建无需 API key 的客户端。
- 调用 client.licenses.activate({ license_key, name: deviceName }),其中 deviceName 例如 John's MacBook Pro。
- 捕获 activate 的 422 并处理为 Activation limit reached;其他错误返回 error.message 或 Activation failed。
- 从 activate 响应中取 id,即 instance ID,以及 business_id、name、license_key_id、created_at、customer、product。
- 调用 client.licenses.validate({ license_key }) 校验 key,返回 response.valid;可传 license_key_instance_id 校验特定实例。
- 调用 client.licenses.deactivate({ license_key, license_key_instance_id: instanceId }) 释放激活槽位,并处理错误。
- 商户端创建客户端时传入 bearerToken: process.env.DODO_PAYMENTS_API_KEY,并按需设置 environment: 'test_mode'。
- 调用 client.licenseKeys.list({ customer_id }) 列出客户密钥,并映射 id、key、status、expires_at、instances_count、activations_limit。
- 调用 client.licenseKeys.retrieve('lk_abc123') 获取单个密钥详情。
- 调用 client.licenseKeys.update('lk_abc123', { disabled: true, activations_limit: 10 }) 禁用密钥或调整激活上限。
- 手动签发时调用 client.licenseKeys.create({ customer_id, key, product_id, activations_limit, expires_at }),并从 newKey.key 取实际密钥字符串。
- 调用 client.licenseKeyInstances.list({ license_key_id: 'lk_abc123' }) 并用 for await 遍历激活实例,读取 id、name、created_at。
- 分页查询可选字段:page_size 默认 10 最大 100,page_number 默认 0,license_key_id,grant_id。
- Electron 集成使用 electron-store、dodopayments、os;创建 const store = new Store() 和 const client = new DodoPayments()。
- Electron 激活:用 os.hostname() 和 os.platform() 组成 deviceName,调用 licenses.activate,成功后把 { key, instanceId: response.id, activatedAt } 存入 store。
- Electron 校验:从 store 读取 license;没有则 false;调用 licenses.validate;valid 返回 true;离线异常时基于 activatedAt 允许 30 天离线宽限期。
- Electron 停用:读取 license;不存在返回 true;调用 licenses.deactivate,传 license_key 和 license_key_instance_id;成功后 store.delete('license');失败记录错误并返回 false。
- React 激活组件:维护 licenseKey、loading、error 状态;handleActivate 调用 window.electronAPI.activateLicense(licenseKey);成功触发 onActivated;失败显示 invalid 或 activation failed。
- React 表单:提供输入框 placeholder XXXX-XXXX-XXXX-XXXX;加载中禁用输入框;按钮在 loading 或无 licenseKey 时禁用;错误时显示 error。
- CLI 集成使用 conf、dodopayments、node-machine-id;用 new Conf<CliStore>({ projectName: 'your-cli' }) 持久化 license。
- CLI activate:用 machineIdSync() 获取 machineId;deviceName 为 CLI - process.platform - machineId.substring(0,8);调用 licenses.activate;成功后 config.set('license', { key, instanceId: response.id, machineId }) 并输出成功。
- CLI activate 错误:若 422 输出 Activation limit reached,提示先停用其他设备;否则输出 error.message;最后 process.exit(1)。
- CLI checkLicense:读取 config.get('license');没有则 false;调用 licenses.validate 返回 response.valid;异常返回 false。
- CLI deactivate:读取 license;没有则提示 No active license found.;调用 licenses.deactivate,传 license_key 和 license_key_instance_id;成功后 config.delete('license') 并输出 License deactivated.;失败输出 error.message。
- CLI requireLicense:返回 preAction 中间件,调用 checkLicense;无效时输出 This command requires a valid license. 和 Run: your-cli activate <license-key>,然后 process.exit(1)。
- CLI 命令用 commander 定义:activate <license-key> 调 activate;deactivate 调 deactivate;status 调 checkLicense 并输出 Active 或 Not activated;generate 用 .hook('preAction', requireLicense()) 保护 premium feature。
- Webhook 客户端创建:new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY })。
- 读取原始 body,并调用 client.webhooks.unwrap(body, { headers: { 'webhook-id', 'webhook-signature', 'webhook-timestamp' } }) 验证 webhook。
- 处理 entitlement_grant.delivered:取 event.data 的 customer_id、id、license_key;如果 license_key 缺失,其他 entitlement 类型可能不含密钥,直接返回 received。
- 交付时将密钥写入数据库:用 prisma.license.create 保存 externalId=id、key=license_key.key、customerId=customer_id、expiresAt=license_key.expires_at ? new Date(...) : null、activationsLimit=license_key.activations_limit、status='active'。
- 交付后调用 sendLicenseEmail(customer_id, license_key.key) 发送激活说明邮件。
- 在 fulfillment 期间持久化本地 license-to-subscription 关联;取消处理必须按 subscription_id 查询该关联;不能只按 customer 过滤,否则会撤销无关产品或订阅的密钥。
- 处理 subscription.cancelled 或 subscription.expired:取 subscription_id 和 cancel_at_next_billing_date;如果是 subscription.cancelled 且 cancel_at_next_billing_date 为真,直接返回 received,不立即撤销。
- 对立即取消或订阅过期:用 getLicenseKeyIdsForSubscription(subscription_id) 查询持久化映射,然后对每个 licenseKeyId 调用 client.licenseKeys.update(licenseKeyId, { disabled: true })。
- Webhook 出错时记录错误并返回 401 和 { error: 'Invalid signature' }。
- 使用事件:entitlement_grant.delivered 为当前推荐;entitlement_grant.revoked 用于密钥撤销;license_key.created 为 legacy;subscription.cancelled 仅禁用该订阅的密钥以立即取消;subscription.expired 在期限结束时禁用该订阅密钥。
- 不要只在安装时校验一次;应周期性校验,例如每周,并在每次启动时校验,捕获撤销或过期密钥;无效时 fail closed,如 process.exit(1)。
- 必须处理 activation-limit 错误:捕获 422 后显示 You've reached your activation limit. Deactivate a device first.,并提供活跃实例列表让用户选择移除。
- 不要只信任客户端校验;在授予敏感功能前由服务端校验,例如 POST /api/validate-license 并读取 { valid }。
- 不要硬编码 license key;不要把 key 嵌入客户端代码或版本控制;使用 process.env.DODO_LICENSE_KEY 或用户输入/安全存储。
- 必须存储 instance ID;存 { key, instanceId: response.id, activatedAt },因为停用需要 instance ID。
- 校验错误时不要 fail open;网络错误或服务器宕机时默认拒绝访问;只在宽限期内信任本地缓存,例如 last_validated_at 距今 <30 天才放行。
适用边界 · 注意事项
- 不适用于不使用 Dodo Payments 的项目;需要 Dodo Payments 账号、API key 和 webhook key。
- 不适用于无需许可证、设备激活或订阅访问控制的免费应用。
- 如果无法安全存储密钥或无法进行服务端校验,不宜直接照搬依赖在线校验和 webhook 的流程。