# TypeScript SDK

`@21stware/raise-sdk` 是 Raise 开放接口的 TypeScript 服务端 SDK，给集成方、小程序后台、三方 SaaS 和应用后端用。换令牌、重试、分页、幂等和 Webhook 验签都在里面。

- 包名：`@21stware/raise-sdk`
- 当前版本：0.7.7（2026-11-03），遵循语义化版本
- 运行环境：Node.js 18+、Deno、Bun、Cloudflare Workers；ESM 与 CommonJS 都能引用
- 零运行时依赖

> **注意**
>
> 这是服务端 SDK。客户端密钥只放在服务端，不要打进浏览器页面或小程序前端。

## 安装

这个包不在公共 npm 注册表上，从平台的开发者门户下载安装，不用登录：

```bash
npm install "https://dev.<你的域名>/dl/sdk/21stware-raise-sdk-0.7.7.tgz"
```

文件名带版本号。先看平台提供的是哪个版本：

```bash
curl https://dev.<你的域名>/dl/sdk/
```

把命令里的文件名换成列表里的那个。

## 第一次调用

```ts title="index.ts"
import { Raise } from '@21stware/raise-sdk'

const raise = new Raise({
  clientId: process.env.RAISE_CLIENT_ID!,
  clientSecret: process.env.RAISE_CLIENT_SECRET!,
  tenant: process.env.RAISE_TENANT,
  baseUrl: `https://api.${process.env.DOMAIN}/open/v1`,
})

const me = await raise.platform.whoami()
console.log(me.tenant, me.app, me.scopes)
```

- `baseUrl`：你的部署的开放接口地址 `https://api.<你的域名>/open/v1`。请总是写上。
- `tenant`：自建应用的密钥绑定了组织，可以不写；商店应用要写你在为哪个组织调用。为多个组织调用时用 `raise.withTenant('<组织 ID>')`，共享一份令牌缓存。

令牌在第一次调用时才去换，按组织缓存，到期前 60 秒自动换新；同时发起多个调用时只换一次。

## 分页

列表接口返回 `{ data, nextCursor }`，`nextCursor` 为 `null` 就是最后一页。每个列表方法都有一个 `…All` 版本，自动翻页：用完一页才取下一页，`break` 即停止请求。

```ts
const page = await raise.tables.listRows('orders', 'all', { limit: 100 })
for await (const row of raise.tables.listRowsAll('orders', 'all')) {
  console.log(row)
}
```

## 错误与重试

状态码 400 及以上一律抛 `RaiseError`。按 `code` 分支，不要解析 `message`；联系支持时带上 `requestId`。

```ts
import { isRaiseError } from '@21stware/raise-sdk'

try {
  await raise.platform.whoami()
} catch (e) {
  if (isRaiseError(e, 'scope_required')) console.error('应用没有被授予这个范围', e.requestId)
  else throw e
}
```

自动重试的规则：

| 情况 | 处理 |
|---|---|
| 429、5xx、网络错误 | GET 等幂等方法，以及带 `Idempotency-Key` 的 POST 自动重试，默认最多 3 次（`maxRetries`） |
| POST 的幂等键 | SDK 给每个 POST 自动生成一个，重试时不变 |
| 等多久 | 有 `Retry-After` 就照它等；否则 0.5 s × 2ⁿ 加抖动，最长 8 s |
| 令牌失效（401 `invalid_token`） | 自动换一次令牌再试 |

## 以客户身份调用

会员小程序等场景：用客户的手机号断言客户，拿到一个只能调用 `/me…` 接口的客户端。

```ts
const cu = await raise.forCustomer({ phone: '+8613800000000' })
if (!cu) return '没有这个会员'
const profile = await cu.me.listMemberships()
```

没有人用这个号时返回 `null`；几个客户共用一个号时抛 `ambiguous_customer`，候选在 `details.candidates` 里，选一个再调用。

## Webhook 验签

收到事件时，用原始请求体（不要先 `JSON.parse` 再序列化）和 `X-Kwork-Signature` 请求头验签。签名不对或时间戳超过 300 秒，抛 `WebhookVerificationError`，回 400 即可。

```ts
import { webhooks } from '@21stware/raise-sdk'

const event = await webhooks.verify(rawBody, req.headers['x-kwork-signature'], process.env.RAISE_WEBHOOK_SECRET!)
if (event.type === 'app.installed') console.log('装到了', event.tenant)
```

## 测试与正式

密钥分两种：`kwo_test_…` 只能用于你开发者组织的测试企业，`kwo_live_…` 只能用于正式企业，拿错了得到 403 `wrong_environment`。代码不用改，换环境变量里的密钥和 `tenant` 即可。令牌响应和每个接口的 `Raise-Environment` 响应头会告诉你当前是哪个环境。

## 下一步

- [SDK 总览](/developers/sdk/)
- [开发者文档](/developers/)：凭证、权限范围、各接口的说明。
